comparison src/fswatcher.cpp @ 588:9b300409c184 easyhg_v1.2

Merge from branch "fswatcher"
author Chris Cannam
date Wed, 14 Mar 2012 12:14:50 +0000
parents 687d9e700e81
children 0b0444762a5d
comparison
equal deleted inserted replaced
582:f3a61f28896e 588:9b300409c184
13 published by the Free Software Foundation; either version 2 of the 13 published by the Free Software Foundation; either version 2 of the
14 License, or (at your option) any later version. See the file 14 License, or (at your option) any later version. See the file
15 COPYING included with this distribution for more information. 15 COPYING included with this distribution for more information.
16 */ 16 */
17 17
18 #include <QMutexLocker>
19 #include <QDir>
20
21 #ifdef Q_OS_MAC
22 // Must include this before debug.h
23 #include <CoreServices/CoreServices.h>
24 #endif
25
18 #include "fswatcher.h" 26 #include "fswatcher.h"
19 #include "debug.h" 27 #include "debug.h"
20 28
21 #include <QMutexLocker>
22 #include <QDir>
23
24 #include <deque> 29 #include <deque>
25 30
26 //#define DEBUG_FSWATCHER 1 31 #define DEBUG_FSWATCHER 1
27 32
28 /* 33 /*
29 * Watching the filesystem is trickier than it seems at first glance. 34 * Watching the filesystem is trickier than it seems at first glance.
30 * 35 *
31 * We ideally should watch every directory, and every file that is 36 * We ideally should watch every directory, and every file that is
43 * 48 *
44 * This means that, when a directory changes, we need to rescan the 49 * This means that, when a directory changes, we need to rescan the
45 * directory to learn whether the set of files in it _excluding_ files 50 * directory to learn whether the set of files in it _excluding_ files
46 * matching our ignore patterns differs from the previous scan, and 51 * matching our ignore patterns differs from the previous scan, and
47 * ignore the change if it doesn't. 52 * ignore the change if it doesn't.
53 *
54 */
55
56 /*
57 * 20120312 -- Another complication. The documentation for
58 * QFileSystemWatcher says:
59 *
60 * On Mac OS X 10.4 [...] an open file descriptor is required for
61 * each monitored file. [...] This means that addPath() and
62 * addPaths() will fail if your process tries to add more than 256
63 * files or directories to the file system monitor [...] Mac OS X
64 * 10.5 and up use a different backend and do not suffer from this
65 * issue.
66 *
67 * Unfortunately, the last sentence above is not true:
68 * http://qt.gitorious.org/qt/qt/commit/6d1baf9979346d6f15da81a535becb4046278962
69 * ("Removing the usage of FSEvents-based backend for now as it has a
70 * few bugs..."). It can't be restored without hacking the Qt source,
71 * which we don't want to do in this context. The commit log doesn't
72 * make clear how serious the bugs were -- an example is given but it
73 * doesn't indicate whether it's an edge case or a common case and
74 * whether the result was a crash or failure to notify.
75 *
76 * This means the Qt class uses kqueue instead on OS/X, but that
77 * doesn't really work for us -- it can only monitor 256 files (or
78 * whatever the fd ulimit is set to, but that's the default) and it
79 * doesn't notify if a file within a directory is modified unless the
80 * metadata changes. The main limitation of FSEvents is that it only
81 * notifies with directory granularity, but that might be OK for us so
82 * long as notifications are actually provoked by file changes as
83 * well. (In OS/X 10.7 there appear to be file-level notifications
84 * too, but that doesn't help us.)
85 *
86 * One other problem with FSEvents is that the API only exists on OS/X
87 * 10.5 or newer -- on older versions we would have no option but to
88 * use kqueue via QFileSystemWatcher. But we can't ship a binary
89 * linked with the FSEvents API to run on 10.4 without some fiddling,
90 * and I'm not really keen to do that either. That may be our cue to
91 * drop 10.4 support for EasyMercurial.
48 */ 92 */
49 93
50 FsWatcher::FsWatcher() : 94 FsWatcher::FsWatcher() :
51 m_lastToken(0), 95 m_lastToken(0),
52 m_lastCounter(0) 96 m_lastCounter(0)
53 { 97 {
98 #ifdef Q_OS_MAC
99 m_stream = 0; // create when we have a path
100 #else
54 connect(&m_watcher, SIGNAL(directoryChanged(QString)), 101 connect(&m_watcher, SIGNAL(directoryChanged(QString)),
55 this, SLOT(fsDirectoryChanged(QString))); 102 this, SLOT(fsDirectoryChanged(QString)));
56 connect(&m_watcher, SIGNAL(fileChanged(QString)), 103 connect(&m_watcher, SIGNAL(fileChanged(QString)),
57 this, SLOT(fsFileChanged(QString))); 104 this, SLOT(fsFileChanged(QString)));
105 #endif
58 } 106 }
59 107
60 FsWatcher::~FsWatcher() 108 FsWatcher::~FsWatcher()
61 { 109 {
62 } 110 }
64 void 112 void
65 FsWatcher::setWorkDirPath(QString path) 113 FsWatcher::setWorkDirPath(QString path)
66 { 114 {
67 QMutexLocker locker(&m_mutex); 115 QMutexLocker locker(&m_mutex);
68 if (m_workDirPath == path) return; 116 if (m_workDirPath == path) return;
117 clearWatchedPaths();
118 m_workDirPath = path;
119 addWorkDirectory(path);
120 debugPrint();
121 }
122
123 void
124 FsWatcher::clearWatchedPaths()
125 {
126 #ifdef Q_OS_MAC
127 FSEventStreamRef stream = (FSEventStreamRef)m_stream;
128 if (stream) {
129 FSEventStreamStop(stream);
130 FSEventStreamInvalidate(stream);
131 FSEventStreamRelease(stream);
132 }
133 m_stream = 0;
134 #else
69 // annoyingly, removePaths prints a warning if given an empty list 135 // annoyingly, removePaths prints a warning if given an empty list
70 if (!m_watcher.directories().empty()) { 136 if (!m_watcher.directories().empty()) {
71 m_watcher.removePaths(m_watcher.directories()); 137 m_watcher.removePaths(m_watcher.directories());
72 } 138 }
73 if (!m_watcher.files().empty()) { 139 if (!m_watcher.files().empty()) {
74 m_watcher.removePaths(m_watcher.files()); 140 m_watcher.removePaths(m_watcher.files());
75 } 141 }
76 m_workDirPath = path; 142 #endif
77 addWorkDirectory(path); 143 }
78 debugPrint(); 144
79 } 145 #ifdef Q_OS_MAC
80 146 static void
81 void 147 fsEventsCallback(ConstFSEventStreamRef streamRef,
82 FsWatcher::setTrackedFilePaths(QStringList paths) 148 void *clientCallBackInfo,
83 { 149 size_t numEvents,
84 QMutexLocker locker(&m_mutex); 150 void *paths,
85 151 const FSEventStreamEventFlags eventFlags[],
86 QSet<QString> alreadyWatched = 152 const FSEventStreamEventId eventIDs[])
87 QSet<QString>::fromList(m_watcher.files()); 153 {
88 154 FsWatcher *watcher = reinterpret_cast<FsWatcher *>(clientCallBackInfo);
89 foreach (QString path, paths) { 155 const char *const *cpaths = reinterpret_cast<const char *const *>(paths);
90 path = m_workDirPath + QDir::separator() + path; 156 for (size_t i = 0; i < numEvents; ++i) {
91 if (!alreadyWatched.contains(path)) { 157 std::cerr << "path " << i << " = " << cpaths[i] << std::endl;
92 m_watcher.addPath(path); 158 watcher->fsDirectoryChanged(QString::fromLocal8Bit(cpaths[i]));
93 } else { 159 }
94 alreadyWatched.remove(path); 160 }
95 } 161 #endif
96 }
97
98 // Remove the remaining paths, those that were being watched
99 // before but that are not in the list we were given
100 foreach (QString path, alreadyWatched) {
101 m_watcher.removePath(path);
102 }
103
104 debugPrint();
105 }
106 162
107 void 163 void
108 FsWatcher::addWorkDirectory(QString path) 164 FsWatcher::addWorkDirectory(QString path)
109 { 165 {
166 #ifdef Q_OS_MAC
167
168 CFStringRef cfPath = CFStringCreateWithCharacters
169 (0, reinterpret_cast<const UniChar *>(path.unicode()),
170 path.length());
171
172 CFArrayRef cfPaths = CFArrayCreate(0, (const void **)&cfPath, 1, 0);
173
174 FSEventStreamContext ctx = { 0, 0, 0, 0, 0 };
175 ctx.info = this;
176
177 FSEventStreamRef stream =
178 FSEventStreamCreate(kCFAllocatorDefault,
179 &fsEventsCallback,
180 &ctx,
181 cfPaths,
182 kFSEventStreamEventIdSinceNow,
183 1.0, // latency, seconds
184 kFSEventStreamCreateFlagNone);
185
186 m_stream = stream;
187
188 FSEventStreamScheduleWithRunLoop(stream,
189 CFRunLoopGetCurrent(),
190 kCFRunLoopDefaultMode);
191
192 if (!FSEventStreamStart(stream)) {
193 std::cerr << "ERROR: FsWatcher::addWorkDirectory: Failed to start FSEvent stream" << std::endl;
194 }
195 #else
110 // QFileSystemWatcher will refuse to add a file or directory to 196 // QFileSystemWatcher will refuse to add a file or directory to
111 // its watch list that it is already watching -- fine -- but it 197 // its watch list that it is already watching -- fine -- but it
112 // prints a warning when this happens, which we wouldn't want. So 198 // prints a warning when this happens, which we wouldn't want. So
113 // we'll check for duplicates ourselves. 199 // we'll check for duplicates ourselves.
114 QSet<QString> alreadyWatched = 200 QSet<QString> alreadyWatched =
135 QString entryPath = d.absoluteFilePath(entry); 221 QString entryPath = d.absoluteFilePath(entry);
136 pending.push_back(entryPath); 222 pending.push_back(entryPath);
137 } 223 }
138 } 224 }
139 } 225 }
226 #endif
227 }
228
229 void
230 FsWatcher::setTrackedFilePaths(QStringList paths)
231 {
232 #ifdef Q_OS_MAC
233
234 // FSEvents will notify when any file in the directory changes,
235 // but we need to be able to check whether the file change was
236 // meaningful to us if it didn't result in any files being added
237 // or removed -- and we have to do that by examining timestamps on
238 // the files we care about
239 foreach (QString p, paths) {
240 m_trackedFileUpdates[p] = QDateTime::currentDateTime();
241 }
242
243 #else
244
245 QMutexLocker locker(&m_mutex);
246
247 QSet<QString> alreadyWatched =
248 QSet<QString>::fromList(m_watcher.files());
249
250 foreach (QString path, paths) {
251 path = m_workDirPath + QDir::separator() + path;
252 if (!alreadyWatched.contains(path)) {
253 m_watcher.addPath(path);
254 } else {
255 alreadyWatched.remove(path);
256 }
257 }
258
259 // Remove the remaining paths, those that were being watched
260 // before but that are not in the list we were given
261 foreach (QString path, alreadyWatched) {
262 m_watcher.removePath(path);
263 }
264
265 debugPrint();
266
267 #endif
140 } 268 }
141 269
142 void 270 void
143 FsWatcher::setIgnoredFilePrefixes(QStringList prefixes) 271 FsWatcher::setIgnoredFilePrefixes(QStringList prefixes)
144 { 272 {
179 } 307 }
180 308
181 void 309 void
182 FsWatcher::fsDirectoryChanged(QString path) 310 FsWatcher::fsDirectoryChanged(QString path)
183 { 311 {
312 bool haveChanges = false;
313
184 { 314 {
185 QMutexLocker locker(&m_mutex); 315 QMutexLocker locker(&m_mutex);
186 316
187 if (shouldIgnore(path)) return; 317 if (shouldIgnore(path)) return;
188 318
189 QSet<QString> files = scanDirectory(path); 319 QSet<QString> files = scanDirectory(path);
320
190 if (files == m_dirContents[path]) { 321 if (files == m_dirContents[path]) {
322
191 #ifdef DEBUG_FSWATCHER 323 #ifdef DEBUG_FSWATCHER
192 std::cerr << "FsWatcher: Directory " << path << " has changed, but not in a way that we are monitoring" << std::endl; 324 std::cerr << "FsWatcher: Directory " << path << " has changed, but not in a way that we are monitoring" << std::endl;
193 #endif 325 #endif
194 return; 326
327 #ifdef Q_OS_MAC
328 haveChanges = manuallyCheckTrackedFiles();
329 #endif
330
195 } else { 331 } else {
332
196 #ifdef DEBUG_FSWATCHER 333 #ifdef DEBUG_FSWATCHER
197 std::cerr << "FsWatcher: Directory " << path << " has changed" << std::endl; 334 std::cerr << "FsWatcher: Directory " << path << " has changed" << std::endl;
198 #endif 335 #endif
199 m_dirContents[path] = files; 336 m_dirContents[path] = files;
200 } 337 size_t counter = ++m_lastCounter;
201 338 m_changes[path] = counter;
202 size_t counter = ++m_lastCounter; 339 haveChanges = true;
203 m_changes[path] = counter; 340 }
204 } 341 }
205 342
206 emit changed(); 343 if (haveChanges) {
344 emit changed();
345 }
207 } 346 }
208 347
209 void 348 void
210 FsWatcher::fsFileChanged(QString path) 349 FsWatcher::fsFileChanged(QString path)
211 { 350 {
213 QMutexLocker locker(&m_mutex); 352 QMutexLocker locker(&m_mutex);
214 353
215 // We don't check whether the file matches an ignore pattern, 354 // We don't check whether the file matches an ignore pattern,
216 // because we are only notified for file changes if we are 355 // because we are only notified for file changes if we are
217 // watching the file explicitly, i.e. the file is in the 356 // watching the file explicitly, i.e. the file is in the
218 // tracked file paths list. So we never want to ignore them 357 // tracked file paths list. So we never want to ignore these
219 358
220 #ifdef DEBUG_FSWATCHER 359 #ifdef DEBUG_FSWATCHER
221 std::cerr << "FsWatcher: Tracked file " << path << " has changed" << std::endl; 360 std::cerr << "FsWatcher: Tracked file " << path << " has changed" << std::endl;
222 #endif 361 #endif
223 362
225 m_changes[path] = counter; 364 m_changes[path] = counter;
226 } 365 }
227 366
228 emit changed(); 367 emit changed();
229 } 368 }
369
370 #ifdef Q_OS_MAC
371 bool
372 FsWatcher::manuallyCheckTrackedFiles()
373 {
374 bool foundChanges = false;
375
376 for (PathTimeMap::iterator i = m_trackedFileUpdates.begin();
377 i != m_trackedFileUpdates.end(); ++i) {
378
379 QString path = i.key();
380 QDateTime prevUpdate = i.value();
381
382 QFileInfo fi(path);
383 QDateTime currUpdate = fi.lastModified();
384
385 if (currUpdate > prevUpdate) {
386
387 #ifdef DEBUG_FSWATCHER
388 std::cerr << "FsWatcher: Tracked file " << path << " has been changed since last check" << std::endl;
389 #endif
390 i.value() = currUpdate;
391
392 size_t counter = ++m_lastCounter;
393 m_changes[path] = counter;
394 foundChanges = true;
395 }
396 }
397
398 return foundChanges;
399 }
400 #endif
230 401
231 bool 402 bool
232 FsWatcher::shouldIgnore(QString path) 403 FsWatcher::shouldIgnore(QString path)
233 { 404 {
234 QFileInfo fi(path); 405 QFileInfo fi(path);
264 if (entry.startsWith('.')) continue; 435 if (entry.startsWith('.')) continue;
265 if (shouldIgnore(entry)) continue; 436 if (shouldIgnore(entry)) continue;
266 files.insert(entry); 437 files.insert(entry);
267 } 438 }
268 } 439 }
269 // std::cerr << "scanDirectory:" << std::endl;
270 // foreach (QString f, files) std::cerr << f << std::endl;
271 return files; 440 return files;
272 } 441 }
273 442
274 void 443 void
275 FsWatcher::debugPrint() 444 FsWatcher::debugPrint()
276 { 445 {
277 #ifdef DEBUG_FSWATCHER 446 #ifdef DEBUG_FSWATCHER
447 #ifndef Q_OS_MAC
278 std::cerr << "FsWatcher: Now watching " << m_watcher.directories().size() 448 std::cerr << "FsWatcher: Now watching " << m_watcher.directories().size()
279 << " directories and " << m_watcher.files().size() 449 << " directories and " << m_watcher.files().size()
280 << " files" << std::endl; 450 << " files" << std::endl;
281 #endif 451 #endif
282 } 452 #endif
453 }