Fixed issue #3307: Abort Merge on a single file always results in a parameter error...
[TortoiseGit.git] / src / Utils / PathWatcher.cpp
blobe973109d2659ea1d75c4c1a3db520fd28f699069
1 // TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2013, 2016, 2018 - TortoiseGit
4 // External Cache Copyright (C) 2007-2012 - TortoiseSVN
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 "PathWatcher.h"
24 CPathWatcher::CPathWatcher(void)
25 : m_hCompPort(nullptr)
26 , m_bRunning(TRUE)
27 , m_bLimitReached(false)
29 // enable the required privileges for this process
30 LPCTSTR arPrivelegeNames[] = { SE_BACKUP_NAME,
31 SE_RESTORE_NAME,
32 SE_CHANGE_NOTIFY_NAME
35 for (int i=0; i<(sizeof(arPrivelegeNames)/sizeof(LPCTSTR)); ++i)
37 CAutoGeneralHandle hToken;
38 if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, hToken.GetPointer()))
40 TOKEN_PRIVILEGES tp = { 1 };
42 if (LookupPrivilegeValue(nullptr, arPrivelegeNames[i], &tp.Privileges[0].Luid))
44 tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
46 AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), nullptr, nullptr);
51 unsigned int threadId = 0;
52 m_hThread = (HANDLE)_beginthreadex(nullptr, 0, ThreadEntry, this, 0, &threadId);
55 CPathWatcher::~CPathWatcher(void)
57 Stop();
58 AutoLocker lock(m_critSec);
59 ClearInfoMap();
62 void CPathWatcher::Stop()
64 InterlockedExchange(&m_bRunning, FALSE);
65 if (m_hCompPort)
67 PostQueuedCompletionStatus(m_hCompPort, 0, NULL, nullptr);
68 m_hCompPort.CloseHandle();
71 if (m_hThread)
73 // the background thread sleeps for 200ms,
74 // so lets wait for it to finish for 1000 ms.
76 WaitForSingleObject(m_hThread, 1000);
77 m_hThread.CloseHandle();
81 bool CPathWatcher::RemovePathAndChildren(const CTGitPath& path)
83 bool bRemoved = false;
84 AutoLocker lock(m_critSec);
85 repeat:
86 for (int i=0; i<watchedPaths.GetCount(); ++i)
88 if (path.IsAncestorOf(watchedPaths[i]))
90 watchedPaths.RemovePath(watchedPaths[i]);
91 bRemoved = true;
92 goto repeat;
95 return bRemoved;
98 bool CPathWatcher::AddPath(const CTGitPath& path)
100 AutoLocker lock(m_critSec);
101 for (int i=0; i<watchedPaths.GetCount(); ++i)
103 if (watchedPaths[i].IsAncestorOf(path))
104 return false; // already watched (recursively)
107 // now check if with the new path we might have a new root
108 CTGitPath newroot;
109 for (int i=0; i<watchedPaths.GetCount(); ++i)
111 const CString& watched = watchedPaths[i].GetWinPathString();
112 const CString& sPath = path.GetWinPathString();
113 int minlen = min(sPath.GetLength(), watched.GetLength());
114 int len = 0;
115 for (len = 0; len < minlen; ++len)
117 if (watched.GetAt(len) != sPath.GetAt(len))
119 if ((len > 1)&&(len < minlen))
121 if (sPath.GetAt(len)=='\\')
123 newroot = CTGitPath(sPath.Left(len));
125 else if (watched.GetAt(len)=='\\')
127 newroot = CTGitPath(watched.Left(len));
130 break;
133 if (len == minlen)
135 if (sPath.GetLength() == minlen)
137 if (watched.GetLength() > minlen)
139 if (watched.GetAt(len)=='\\')
141 newroot = path;
143 else if (sPath.GetLength() == 3 && sPath[1] == ':')
145 newroot = path;
149 else
151 if (sPath.GetLength() > minlen)
153 if (sPath.GetAt(len)=='\\')
155 newroot = CTGitPath(watched);
157 else if (watched.GetLength() == 3 && watched[1] == ':')
159 newroot = CTGitPath(watched);
165 if (!newroot.IsEmpty())
167 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": add path to watch %s\n", newroot.GetWinPath());
168 watchedPaths.AddPath(newroot);
169 watchedPaths.RemoveChildren();
170 m_hCompPort.CloseHandle();
171 return true;
173 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": add path to watch %s\n", path.GetWinPath());
174 watchedPaths.AddPath(path);
175 m_hCompPort.CloseHandle();
176 return true;
180 unsigned int CPathWatcher::ThreadEntry(void* pContext)
182 reinterpret_cast<CPathWatcher*>(pContext)->WorkerThread();
183 return 0;
186 void CPathWatcher::WorkerThread()
188 DWORD numBytes;
189 CDirWatchInfo* pdi = nullptr;
190 LPOVERLAPPED lpOverlapped;
191 const int bufferSize = MAX_PATH * 4;
192 TCHAR buf[bufferSize] = {0};
193 while (m_bRunning)
195 if (!watchedPaths.IsEmpty())
197 if (!m_hCompPort || !GetQueuedCompletionStatus(m_hCompPort,
198 &numBytes,
199 (PULONG_PTR) &pdi,
200 &lpOverlapped,
201 INFINITE))
203 // Error retrieving changes
204 // Clear the list of watched objects and recreate that list
205 if (!m_bRunning)
206 return;
208 AutoLocker lock(m_critSec);
209 ClearInfoMap();
211 DWORD lasterr = GetLastError();
212 if ((m_hCompPort)&&(lasterr!=ERROR_SUCCESS)&&(lasterr!=ERROR_INVALID_HANDLE))
214 m_hCompPort.CloseHandle();
216 for (int i=0; i<watchedPaths.GetCount(); ++i)
218 CAutoFile hDir = CreateFile(watchedPaths[i].GetWinPath(),
219 FILE_LIST_DIRECTORY,
220 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
221 nullptr, //security attributes
222 OPEN_EXISTING,
223 FILE_FLAG_BACKUP_SEMANTICS | //required privileges: SE_BACKUP_NAME and SE_RESTORE_NAME.
224 FILE_FLAG_OVERLAPPED,
225 nullptr);
226 if (!hDir)
228 // this could happen if a watched folder has been removed/renamed
229 m_hCompPort.CloseHandle();
230 AutoLocker lock(m_critSec);
231 watchedPaths.RemovePath(watchedPaths[i]);
232 i--; if (i<0) i=0;
233 break;
236 CDirWatchInfo * pDirInfo = new CDirWatchInfo(hDir.Detach(), watchedPaths[i]); // the new CDirWatchInfo object owns the handle now
237 m_hCompPort = CreateIoCompletionPort(pDirInfo->m_hDir, m_hCompPort, (ULONG_PTR)pDirInfo, 0);
238 if (!m_hCompPort)
240 AutoLocker lock(m_critSec);
241 ClearInfoMap();
242 delete pDirInfo;
243 pDirInfo = nullptr;
244 watchedPaths.RemovePath(watchedPaths[i]);
245 i--; if (i<0) i=0;
246 break;
248 if (!ReadDirectoryChangesW(pDirInfo->m_hDir,
249 pDirInfo->m_Buffer,
250 bufferSize,
251 TRUE,
252 FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE,
253 &numBytes,// not used
254 &pDirInfo->m_Overlapped,
255 nullptr)) //no completion routine!
257 AutoLocker lock(m_critSec);
258 ClearInfoMap();
259 delete pDirInfo;
260 pDirInfo = nullptr;
261 watchedPaths.RemovePath(watchedPaths[i]);
262 i--; if (i<0) i=0;
263 break;
265 AutoLocker lock(m_critSec);
266 watchInfoMap[pDirInfo->m_hDir] = pDirInfo;
267 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": watching path %s\n", pDirInfo->m_DirName.GetWinPath());
270 else
272 if (!m_bRunning)
273 return;
274 // NOTE: the longer this code takes to execute until ReadDirectoryChangesW
275 // is called again, the higher the chance that we miss some
276 // changes in the file system!
277 if (pdi)
279 if (numBytes == 0)
281 goto continuewatching;
283 PFILE_NOTIFY_INFORMATION pnotify = (PFILE_NOTIFY_INFORMATION)pdi->m_Buffer;
284 if ((ULONG_PTR)pnotify - (ULONG_PTR)pdi->m_Buffer > bufferSize)
285 goto continuewatching;
286 DWORD nOffset = pnotify->NextEntryOffset;
289 nOffset = pnotify->NextEntryOffset;
290 SecureZeroMemory(buf, bufferSize*sizeof(TCHAR));
291 wcsncpy_s(buf, bufferSize, pdi->m_DirPath, bufferSize - 1);
292 errno_t err = wcsncat_s(buf + pdi->m_DirPath.GetLength(), bufferSize - pdi->m_DirPath.GetLength(), pnotify->FileName, min(bufferSize - pdi->m_DirPath.GetLength(), int(pnotify->FileNameLength / sizeof(TCHAR))));
293 if (err == STRUNCATE)
295 pnotify = (PFILE_NOTIFY_INFORMATION)((LPBYTE)pnotify + nOffset);
296 continue;
298 buf[min((decltype((pnotify->FileNameLength / sizeof(WCHAR)))) bufferSize - 1, pdi->m_DirPath.GetLength() + (pnotify->FileNameLength / sizeof(WCHAR)))] = L'\0';
299 pnotify = (PFILE_NOTIFY_INFORMATION)((LPBYTE)pnotify + nOffset);
300 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": change notification: %s\n", buf);
302 AutoLocker lock(m_critSec);
303 if (m_changedPaths.GetCount() < MAX_CHANGED_PATHS)
304 m_changedPaths.AddPath(CTGitPath(buf));
305 else
306 m_bLimitReached = true;
308 if ((ULONG_PTR)pnotify - (ULONG_PTR)pdi->m_Buffer > bufferSize)
309 break;
310 } while (nOffset);
311 continuewatching:
313 AutoLocker lock(m_critSec);
314 m_changedPaths.RemoveDuplicates();
316 SecureZeroMemory(pdi->m_Buffer, sizeof(pdi->m_Buffer));
317 SecureZeroMemory(&pdi->m_Overlapped, sizeof(OVERLAPPED));
318 if (!ReadDirectoryChangesW(pdi->m_hDir,
319 pdi->m_Buffer,
320 bufferSize,
321 TRUE,
322 FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE,
323 &numBytes,// not used
324 &pdi->m_Overlapped,
325 nullptr)) //no completion routine!
327 // Since the call to ReadDirectoryChangesW failed, just
328 // wait a while. We don't want to have this thread
329 // running using 100% CPU if something goes completely
330 // wrong.
331 Sleep(200);
335 }// if (!watchedPaths.IsEmpty())
336 else
337 Sleep(200);
338 }// while (m_bRunning)
341 void CPathWatcher::ClearInfoMap()
343 if (!watchInfoMap.empty())
345 AutoLocker lock(m_critSec);
346 for (std::map<HANDLE, CDirWatchInfo *>::iterator I = watchInfoMap.begin(); I != watchInfoMap.end(); ++I)
348 CPathWatcher::CDirWatchInfo * info = I->second;
349 delete info;
350 info = nullptr;
353 watchInfoMap.clear();
354 m_hCompPort.CloseHandle();
357 CPathWatcher::CDirWatchInfo::CDirWatchInfo(HANDLE hDir, const CTGitPath& DirectoryName)
358 : m_hDir(std::move(hDir))
359 , m_DirName(DirectoryName)
361 ATLASSERT( hDir && !DirectoryName.IsEmpty());
362 m_Buffer[0] = '\0';
363 memset(&m_Overlapped, 0, sizeof(m_Overlapped));
364 m_DirPath = m_DirName.GetWinPathString();
365 if (m_DirPath.GetAt(m_DirPath.GetLength()-1) != '\\')
366 m_DirPath += L'\\';
369 CPathWatcher::CDirWatchInfo::~CDirWatchInfo()
371 CloseDirectoryHandle();
374 bool CPathWatcher::CDirWatchInfo::CloseDirectoryHandle()
376 return m_hDir.CloseHandle();