Chris@538
|
1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
|
Chris@538
|
2
|
Chris@538
|
3 /*
|
Chris@538
|
4 EasyMercurial
|
Chris@538
|
5
|
Chris@538
|
6 Based on hgExplorer by Jari Korhonen
|
Chris@538
|
7 Copyright (c) 2010 Jari Korhonen
|
Chris@560
|
8 Copyright (c) 2012 Chris Cannam
|
Chris@560
|
9 Copyright (c) 2012 Queen Mary, University of London
|
Chris@538
|
10
|
Chris@538
|
11 This program is free software; you can redistribute it and/or
|
Chris@538
|
12 modify it under the terms of the GNU General Public License as
|
Chris@538
|
13 published by the Free Software Foundation; either version 2 of the
|
Chris@538
|
14 License, or (at your option) any later version. See the file
|
Chris@538
|
15 COPYING included with this distribution for more information.
|
Chris@538
|
16 */
|
Chris@538
|
17
|
Chris@538
|
18 #include "fswatcher.h"
|
Chris@540
|
19 #include "debug.h"
|
Chris@538
|
20
|
Chris@538
|
21 #include <QMutexLocker>
|
Chris@538
|
22 #include <QDir>
|
Chris@538
|
23
|
Chris@538
|
24 #include <deque>
|
Chris@538
|
25
|
Chris@562
|
26 //#define DEBUG_FSWATCHER 1
|
Chris@540
|
27
|
Chris@539
|
28 /*
|
Chris@539
|
29 * Watching the filesystem is trickier than it seems at first glance.
|
Chris@539
|
30 *
|
Chris@539
|
31 * We ideally should watch every directory, and every file that is
|
Chris@539
|
32 * tracked by Hg. If a new file is created in a directory, then we
|
Chris@539
|
33 * need to respond in order to show it as a potential candidate to be
|
Chris@539
|
34 * added.
|
Chris@539
|
35 *
|
Chris@539
|
36 * Complicating matters though is that Hg itself might modify the
|
Chris@539
|
37 * filesystem. This can happen even in "read-only" operations: for
|
Chris@539
|
38 * example, hg stat creates files called hg-checklink and hg-checkexec
|
Chris@539
|
39 * to test properties of the filesystem. So we need to know to ignore
|
Chris@539
|
40 * those files; unfortunately, when watching a directory (which is how
|
Chris@539
|
41 * we find out about the creation of new files) we are notified only
|
Chris@540
|
42 * that the directory has changed -- we aren't told what changed.
|
Chris@540
|
43 *
|
Chris@540
|
44 * This means that, when a directory changes, we need to rescan the
|
Chris@540
|
45 * directory to learn whether the set of files in it _excluding_ files
|
Chris@540
|
46 * matching our ignore patterns differs from the previous scan, and
|
Chris@540
|
47 * ignore the change if it doesn't.
|
Chris@539
|
48 */
|
Chris@539
|
49
|
Chris@538
|
50 FsWatcher::FsWatcher() :
|
Chris@538
|
51 m_lastToken(0),
|
Chris@538
|
52 m_lastCounter(0)
|
Chris@538
|
53 {
|
Chris@538
|
54 connect(&m_watcher, SIGNAL(directoryChanged(QString)),
|
Chris@538
|
55 this, SLOT(fsDirectoryChanged(QString)));
|
Chris@538
|
56 connect(&m_watcher, SIGNAL(fileChanged(QString)),
|
Chris@538
|
57 this, SLOT(fsFileChanged(QString)));
|
Chris@538
|
58 }
|
Chris@538
|
59
|
Chris@538
|
60 FsWatcher::~FsWatcher()
|
Chris@538
|
61 {
|
Chris@538
|
62 }
|
Chris@538
|
63
|
Chris@538
|
64 void
|
Chris@538
|
65 FsWatcher::setWorkDirPath(QString path)
|
Chris@538
|
66 {
|
Chris@538
|
67 QMutexLocker locker(&m_mutex);
|
Chris@541
|
68 if (m_workDirPath == path) return;
|
Chris@562
|
69 // annoyingly, removePaths prints a warning if given an empty list
|
Chris@562
|
70 if (!m_watcher.directories().empty()) {
|
Chris@562
|
71 m_watcher.removePaths(m_watcher.directories());
|
Chris@562
|
72 }
|
Chris@562
|
73 if (!m_watcher.files().empty()) {
|
Chris@562
|
74 m_watcher.removePaths(m_watcher.files());
|
Chris@562
|
75 }
|
Chris@538
|
76 m_workDirPath = path;
|
Chris@538
|
77 addWorkDirectory(path);
|
Chris@540
|
78 debugPrint();
|
Chris@538
|
79 }
|
Chris@538
|
80
|
Chris@538
|
81 void
|
Chris@539
|
82 FsWatcher::setTrackedFilePaths(QStringList paths)
|
Chris@539
|
83 {
|
Chris@539
|
84 QMutexLocker locker(&m_mutex);
|
Chris@541
|
85
|
Chris@541
|
86 QSet<QString> alreadyWatched =
|
Chris@541
|
87 QSet<QString>::fromList(m_watcher.files());
|
Chris@541
|
88
|
Chris@539
|
89 foreach (QString path, paths) {
|
Chris@542
|
90 path = m_workDirPath + QDir::separator() + path;
|
Chris@541
|
91 if (!alreadyWatched.contains(path)) {
|
Chris@541
|
92 m_watcher.addPath(path);
|
Chris@541
|
93 } else {
|
Chris@541
|
94 alreadyWatched.remove(path);
|
Chris@541
|
95 }
|
Chris@539
|
96 }
|
Chris@541
|
97
|
Chris@541
|
98 // Remove the remaining paths, those that were being watched
|
Chris@541
|
99 // before but that are not in the list we were given
|
Chris@541
|
100 foreach (QString path, alreadyWatched) {
|
Chris@541
|
101 m_watcher.removePath(path);
|
Chris@541
|
102 }
|
Chris@541
|
103
|
Chris@540
|
104 debugPrint();
|
Chris@539
|
105 }
|
Chris@539
|
106
|
Chris@539
|
107 void
|
Chris@538
|
108 FsWatcher::addWorkDirectory(QString path)
|
Chris@538
|
109 {
|
Chris@538
|
110 // QFileSystemWatcher will refuse to add a file or directory to
|
Chris@538
|
111 // its watch list that it is already watching -- fine -- but it
|
Chris@538
|
112 // prints a warning when this happens, which we wouldn't want. So
|
Chris@538
|
113 // we'll check for duplicates ourselves.
|
Chris@538
|
114 QSet<QString> alreadyWatched =
|
Chris@538
|
115 QSet<QString>::fromList(m_watcher.directories());
|
Chris@538
|
116
|
Chris@538
|
117 std::deque<QString> pending;
|
Chris@538
|
118 pending.push_back(path);
|
Chris@538
|
119
|
Chris@538
|
120 while (!pending.empty()) {
|
Chris@538
|
121
|
Chris@538
|
122 QString path = pending.front();
|
Chris@538
|
123 pending.pop_front();
|
Chris@538
|
124 if (!alreadyWatched.contains(path)) {
|
Chris@538
|
125 m_watcher.addPath(path);
|
Chris@540
|
126 m_dirContents[path] = scanDirectory(path);
|
Chris@538
|
127 }
|
Chris@538
|
128
|
Chris@538
|
129 QDir d(path);
|
Chris@538
|
130 if (d.exists()) {
|
Chris@538
|
131 d.setFilter(QDir::Dirs | QDir::NoDotAndDotDot |
|
Chris@538
|
132 QDir::Readable | QDir::NoSymLinks);
|
Chris@538
|
133 foreach (QString entry, d.entryList()) {
|
Chris@538
|
134 if (entry.startsWith('.')) continue;
|
Chris@538
|
135 QString entryPath = d.absoluteFilePath(entry);
|
Chris@538
|
136 pending.push_back(entryPath);
|
Chris@538
|
137 }
|
Chris@538
|
138 }
|
Chris@538
|
139 }
|
Chris@538
|
140 }
|
Chris@538
|
141
|
Chris@538
|
142 void
|
Chris@538
|
143 FsWatcher::setIgnoredFilePrefixes(QStringList prefixes)
|
Chris@538
|
144 {
|
Chris@538
|
145 QMutexLocker locker(&m_mutex);
|
Chris@538
|
146 m_ignoredPrefixes = prefixes;
|
Chris@538
|
147 }
|
Chris@538
|
148
|
Chris@538
|
149 void
|
Chris@538
|
150 FsWatcher::setIgnoredFileSuffixes(QStringList suffixes)
|
Chris@538
|
151 {
|
Chris@538
|
152 QMutexLocker locker(&m_mutex);
|
Chris@538
|
153 m_ignoredSuffixes = suffixes;
|
Chris@538
|
154 }
|
Chris@538
|
155
|
Chris@538
|
156 int
|
Chris@538
|
157 FsWatcher::getNewToken()
|
Chris@538
|
158 {
|
Chris@538
|
159 QMutexLocker locker(&m_mutex);
|
Chris@538
|
160 int token = ++m_lastToken;
|
Chris@538
|
161 m_tokenMap[token] = m_lastCounter;
|
Chris@538
|
162 return token;
|
Chris@538
|
163 }
|
Chris@538
|
164
|
Chris@538
|
165 QSet<QString>
|
Chris@538
|
166 FsWatcher::getChangedPaths(int token)
|
Chris@538
|
167 {
|
Chris@538
|
168 QMutexLocker locker(&m_mutex);
|
Chris@538
|
169 size_t lastUpdatedAt = m_tokenMap[token];
|
Chris@538
|
170 QSet<QString> changed;
|
Chris@538
|
171 for (QHash<QString, size_t>::const_iterator i = m_changes.begin();
|
Chris@538
|
172 i != m_changes.end(); ++i) {
|
Chris@538
|
173 if (i.value() > lastUpdatedAt) {
|
Chris@538
|
174 changed.insert(i.key());
|
Chris@538
|
175 }
|
Chris@538
|
176 }
|
Chris@538
|
177 m_tokenMap[token] = m_lastCounter;
|
Chris@538
|
178 return changed;
|
Chris@538
|
179 }
|
Chris@538
|
180
|
Chris@538
|
181 void
|
Chris@538
|
182 FsWatcher::fsDirectoryChanged(QString path)
|
Chris@538
|
183 {
|
Chris@538
|
184 {
|
Chris@538
|
185 QMutexLocker locker(&m_mutex);
|
Chris@540
|
186
|
Chris@538
|
187 if (shouldIgnore(path)) return;
|
Chris@540
|
188
|
Chris@540
|
189 QSet<QString> files = scanDirectory(path);
|
Chris@540
|
190 if (files == m_dirContents[path]) {
|
Chris@540
|
191 #ifdef DEBUG_FSWATCHER
|
Chris@540
|
192 std::cerr << "FsWatcher: Directory " << path << " has changed, but not in a way that we are monitoring" << std::endl;
|
Chris@540
|
193 #endif
|
Chris@540
|
194 return;
|
Chris@541
|
195 } else {
|
Chris@541
|
196 #ifdef DEBUG_FSWATCHER
|
Chris@541
|
197 std::cerr << "FsWatcher: Directory " << path << " has changed" << std::endl;
|
Chris@541
|
198 #endif
|
Chris@541
|
199 m_dirContents[path] = files;
|
Chris@540
|
200 }
|
Chris@540
|
201
|
Chris@540
|
202 size_t counter = ++m_lastCounter;
|
Chris@540
|
203 m_changes[path] = counter;
|
Chris@538
|
204 }
|
Chris@540
|
205
|
Chris@538
|
206 emit changed();
|
Chris@538
|
207 }
|
Chris@538
|
208
|
Chris@538
|
209 void
|
Chris@538
|
210 FsWatcher::fsFileChanged(QString path)
|
Chris@538
|
211 {
|
Chris@540
|
212 {
|
Chris@540
|
213 QMutexLocker locker(&m_mutex);
|
Chris@540
|
214
|
Chris@540
|
215 // We don't check whether the file matches an ignore pattern,
|
Chris@540
|
216 // because we are only notified for file changes if we are
|
Chris@540
|
217 // watching the file explicitly, i.e. the file is in the
|
Chris@540
|
218 // tracked file paths list. So we never want to ignore them
|
Chris@540
|
219
|
Chris@563
|
220 #ifdef DEBUG_FSWATCHER
|
Chris@541
|
221 std::cerr << "FsWatcher: Tracked file " << path << " has changed" << std::endl;
|
Chris@563
|
222 #endif
|
Chris@541
|
223
|
Chris@540
|
224 size_t counter = ++m_lastCounter;
|
Chris@540
|
225 m_changes[path] = counter;
|
Chris@540
|
226 }
|
Chris@540
|
227
|
Chris@540
|
228 emit changed();
|
Chris@538
|
229 }
|
Chris@538
|
230
|
Chris@538
|
231 bool
|
Chris@538
|
232 FsWatcher::shouldIgnore(QString path)
|
Chris@538
|
233 {
|
Chris@540
|
234 QFileInfo fi(path);
|
Chris@540
|
235 QString fn(fi.fileName());
|
Chris@540
|
236 foreach (QString pfx, m_ignoredPrefixes) {
|
Chris@541
|
237 if (fn.startsWith(pfx)) {
|
Chris@563
|
238 #ifdef DEBUG_FSWATCHER
|
Chris@541
|
239 std::cerr << "(ignoring: " << path << ")" << std::endl;
|
Chris@563
|
240 #endif
|
Chris@541
|
241 return true;
|
Chris@541
|
242 }
|
Chris@540
|
243 }
|
Chris@540
|
244 foreach (QString sfx, m_ignoredSuffixes) {
|
Chris@541
|
245 if (fn.endsWith(sfx)) {
|
Chris@563
|
246 #ifdef DEBUG_FSWATCHER
|
Chris@541
|
247 std::cerr << "(ignoring: " << path << ")" << std::endl;
|
Chris@563
|
248 #endif
|
Chris@541
|
249 return true;
|
Chris@541
|
250 }
|
Chris@540
|
251 }
|
Chris@540
|
252 return false;
|
Chris@538
|
253 }
|
Chris@538
|
254
|
Chris@540
|
255 QSet<QString>
|
Chris@540
|
256 FsWatcher::scanDirectory(QString path)
|
Chris@540
|
257 {
|
Chris@540
|
258 QSet<QString> files;
|
Chris@540
|
259 QDir d(path);
|
Chris@540
|
260 if (d.exists()) {
|
Chris@540
|
261 d.setFilter(QDir::Files | QDir::NoDotAndDotDot |
|
Chris@540
|
262 QDir::Readable | QDir::NoSymLinks);
|
Chris@540
|
263 foreach (QString entry, d.entryList()) {
|
Chris@540
|
264 if (entry.startsWith('.')) continue;
|
Chris@540
|
265 if (shouldIgnore(entry)) continue;
|
Chris@540
|
266 files.insert(entry);
|
Chris@540
|
267 }
|
Chris@540
|
268 }
|
Chris@541
|
269 // std::cerr << "scanDirectory:" << std::endl;
|
Chris@541
|
270 // foreach (QString f, files) std::cerr << f << std::endl;
|
Chris@540
|
271 return files;
|
Chris@540
|
272 }
|
Chris@540
|
273
|
Chris@540
|
274 void
|
Chris@540
|
275 FsWatcher::debugPrint()
|
Chris@540
|
276 {
|
Chris@540
|
277 #ifdef DEBUG_FSWATCHER
|
Chris@540
|
278 std::cerr << "FsWatcher: Now watching " << m_watcher.directories().size()
|
Chris@540
|
279 << " directories and " << m_watcher.files().size()
|
Chris@540
|
280 << " files" << std::endl;
|
Chris@540
|
281 #endif
|
Chris@540
|
282 }
|