Mercurial > hg > easyhg
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 } |