annotate src/fswatcher.cpp @ 584:09b9849b9800 fswatcher

Make a start with explicit FSEvents support on OS/X (see long comment added in this commit). Note that this branch doesn't currently build.
author Chris Cannam
date Mon, 12 Mar 2012 17:25:41 +0000
parents 0a094020c2d4
children fa242885a233
rev   line source
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@584 18 #include <QMutexLocker>
Chris@584 19 #include <QDir>
Chris@584 20
Chris@584 21 #ifdef Q_OS_MAC
Chris@584 22 // Must include this before debug.h
Chris@584 23 #include <CoreServices/CoreServices.h>
Chris@584 24 #endif
Chris@584 25
Chris@538 26 #include "fswatcher.h"
Chris@540 27 #include "debug.h"
Chris@538 28
Chris@538 29 #include <deque>
Chris@538 30
Chris@562 31 //#define DEBUG_FSWATCHER 1
Chris@540 32
Chris@539 33 /*
Chris@539 34 * Watching the filesystem is trickier than it seems at first glance.
Chris@539 35 *
Chris@539 36 * We ideally should watch every directory, and every file that is
Chris@539 37 * tracked by Hg. If a new file is created in a directory, then we
Chris@539 38 * need to respond in order to show it as a potential candidate to be
Chris@539 39 * added.
Chris@539 40 *
Chris@539 41 * Complicating matters though is that Hg itself might modify the
Chris@539 42 * filesystem. This can happen even in "read-only" operations: for
Chris@539 43 * example, hg stat creates files called hg-checklink and hg-checkexec
Chris@539 44 * to test properties of the filesystem. So we need to know to ignore
Chris@539 45 * those files; unfortunately, when watching a directory (which is how
Chris@539 46 * we find out about the creation of new files) we are notified only
Chris@540 47 * that the directory has changed -- we aren't told what changed.
Chris@540 48 *
Chris@540 49 * This means that, when a directory changes, we need to rescan the
Chris@540 50 * directory to learn whether the set of files in it _excluding_ files
Chris@540 51 * matching our ignore patterns differs from the previous scan, and
Chris@540 52 * ignore the change if it doesn't.
Chris@584 53 *
Chris@584 54 */
Chris@584 55
Chris@584 56 /*
Chris@584 57 * 20120312 -- Another complication. The documentation for
Chris@584 58 * QFileSystemWatcher says:
Chris@584 59 *
Chris@584 60 * On Mac OS X 10.4 [...] an open file descriptor is required for
Chris@584 61 * each monitored file. [...] This means that addPath() and
Chris@584 62 * addPaths() will fail if your process tries to add more than 256
Chris@584 63 * files or directories to the file system monitor [...] Mac OS X
Chris@584 64 * 10.5 and up use a different backend and do not suffer from this
Chris@584 65 * issue.
Chris@584 66 *
Chris@584 67 * Unfortunately, the last sentence above is not true:
Chris@584 68 * http://qt.gitorious.org/qt/qt/commit/6d1baf9979346d6f15da81a535becb4046278962
Chris@584 69 * ("Removing the usage of FSEvents-based backend for now as it has a
Chris@584 70 * few bugs..."). It can't be restored without hacking the Qt source,
Chris@584 71 * which we don't want to do in this context. The commit log doesn't
Chris@584 72 * make clear how serious the bugs were -- an example is given but it
Chris@584 73 * doesn't indicate whether it's an edge case or a common case and
Chris@584 74 * whether the result was a crash or failure to notify.
Chris@584 75 *
Chris@584 76 * This means the Qt class uses kqueue instead on OS/X, but that
Chris@584 77 * doesn't really work for us -- it can only monitor 256 files (or
Chris@584 78 * whatever the fd ulimit is set to, but that's the default) and it
Chris@584 79 * doesn't notify if a file within a directory is modified unless the
Chris@584 80 * metadata changes. The main limitation of FSEvents is that it only
Chris@584 81 * notifies with directory granularity, but that's OK for us so long
Chris@584 82 * as notifications are actually provoked by file changes as well.
Chris@584 83 *
Chris@584 84 * One other problem with FSEvents is that the API only exists on OS/X
Chris@584 85 * 10.5 or newer -- on older versions we would have no option but to
Chris@584 86 * use kqueue via QFileSystemWatcher. But we can't ship a binary
Chris@584 87 * linked with the FSEvents API to run on 10.4 without some fiddling,
Chris@584 88 * and I'm not really keen to do that either. That may be our cue to
Chris@584 89 * drop 10.4 support for EasyMercurial.
Chris@539 90 */
Chris@539 91
Chris@538 92 FsWatcher::FsWatcher() :
Chris@538 93 m_lastToken(0),
Chris@538 94 m_lastCounter(0)
Chris@538 95 {
Chris@584 96 #ifdef Q_OS_MAC
Chris@584 97 m_stream = 0; // create when we have a path
Chris@584 98 #else
Chris@538 99 connect(&m_watcher, SIGNAL(directoryChanged(QString)),
Chris@538 100 this, SLOT(fsDirectoryChanged(QString)));
Chris@538 101 connect(&m_watcher, SIGNAL(fileChanged(QString)),
Chris@538 102 this, SLOT(fsFileChanged(QString)));
Chris@584 103 #endif
Chris@538 104 }
Chris@538 105
Chris@538 106 FsWatcher::~FsWatcher()
Chris@538 107 {
Chris@538 108 }
Chris@538 109
Chris@538 110 void
Chris@538 111 FsWatcher::setWorkDirPath(QString path)
Chris@538 112 {
Chris@538 113 QMutexLocker locker(&m_mutex);
Chris@541 114 if (m_workDirPath == path) return;
Chris@584 115 clearWatchedPaths();
Chris@584 116 m_workDirPath = path;
Chris@584 117 addWorkDirectory(path);
Chris@584 118 debugPrint();
Chris@584 119 }
Chris@584 120
Chris@584 121 void
Chris@584 122 FsWatcher::clearWatchedPaths()
Chris@584 123 {
Chris@584 124 #ifdef Q_OS_MAC
Chris@584 125 FSEventStreamRef stream = (FSEventStreamRef)m_stream;
Chris@584 126 if (stream) {
Chris@584 127 FSEventStreamStop(stream);
Chris@584 128 FSEventStreamInvalidate(stream);
Chris@584 129 FSEventStreamRelease(stream);
Chris@584 130 }
Chris@584 131 m_stream = 0;
Chris@584 132 #else
Chris@562 133 // annoyingly, removePaths prints a warning if given an empty list
Chris@562 134 if (!m_watcher.directories().empty()) {
Chris@562 135 m_watcher.removePaths(m_watcher.directories());
Chris@562 136 }
Chris@562 137 if (!m_watcher.files().empty()) {
Chris@562 138 m_watcher.removePaths(m_watcher.files());
Chris@562 139 }
Chris@584 140 #endif
Chris@538 141 }
Chris@538 142
Chris@584 143 #ifdef Q_OS_MAC
Chris@584 144 static void
Chris@584 145 fsEventsCallback(FSEventStreamRef streamRef,
Chris@584 146 void *clientCallBackInfo,
Chris@584 147 int numEvents,
Chris@584 148 const char *const eventPaths[],
Chris@584 149 const FSEventStreamEventFlags *eventFlags,
Chris@584 150 const uint64_t *eventIDs)
Chris@539 151 {
Chris@539 152 }
Chris@584 153 #endif
Chris@539 154
Chris@539 155 void
Chris@538 156 FsWatcher::addWorkDirectory(QString path)
Chris@538 157 {
Chris@584 158 #ifdef Q_OS_MAC
Chris@584 159 FSEventStreamRef stream =
Chris@584 160 FSEventStreamCreate(kCFAllocatorDefault,
Chris@584 161 (FSEventStreamCallback)&fsEventsCallback,
Chris@584 162 this,
Chris@584 163 cfPaths,
Chris@584 164 kFSEventStreamEventIdSinceNow,
Chris@584 165 1.0, // latency, seconds
Chris@584 166 kFSEventStreamCreateFlagNone);
Chris@584 167
Chris@584 168 m_stream = stream;
Chris@584 169
Chris@584 170 FSEventStreamScheduleWithRunLoop(stream,
Chris@584 171 CFRunLoopGetCurrent(),
Chris@584 172 kCFRunLoopDefaultMode);
Chris@584 173
Chris@584 174 if (!FSEventStreamStart(stream)) {
Chris@584 175 std::cerr << "ERROR: FsWatcher::addWorkDirectory: Failed to start FSEvent stream" << std::endl;
Chris@584 176 }
Chris@584 177 #else
Chris@538 178 // QFileSystemWatcher will refuse to add a file or directory to
Chris@538 179 // its watch list that it is already watching -- fine -- but it
Chris@538 180 // prints a warning when this happens, which we wouldn't want. So
Chris@538 181 // we'll check for duplicates ourselves.
Chris@538 182 QSet<QString> alreadyWatched =
Chris@538 183 QSet<QString>::fromList(m_watcher.directories());
Chris@538 184
Chris@538 185 std::deque<QString> pending;
Chris@538 186 pending.push_back(path);
Chris@538 187
Chris@538 188 while (!pending.empty()) {
Chris@538 189
Chris@538 190 QString path = pending.front();
Chris@538 191 pending.pop_front();
Chris@538 192 if (!alreadyWatched.contains(path)) {
Chris@538 193 m_watcher.addPath(path);
Chris@540 194 m_dirContents[path] = scanDirectory(path);
Chris@538 195 }
Chris@538 196
Chris@538 197 QDir d(path);
Chris@538 198 if (d.exists()) {
Chris@538 199 d.setFilter(QDir::Dirs | QDir::NoDotAndDotDot |
Chris@538 200 QDir::Readable | QDir::NoSymLinks);
Chris@538 201 foreach (QString entry, d.entryList()) {
Chris@538 202 if (entry.startsWith('.')) continue;
Chris@538 203 QString entryPath = d.absoluteFilePath(entry);
Chris@538 204 pending.push_back(entryPath);
Chris@538 205 }
Chris@538 206 }
Chris@538 207 }
Chris@584 208 #endif
Chris@584 209 }
Chris@584 210
Chris@584 211 void
Chris@584 212 FsWatcher::setTrackedFilePaths(QStringList paths)
Chris@584 213 {
Chris@584 214 #ifdef Q_OS_MAC
Chris@584 215 // We don't need to do anything here, so long as addWorkDirectory
Chris@584 216 // has been called -- FSEvents monitors files within directories
Chris@584 217 // as well as the directories themselves (even though it only
Chris@584 218 // notifies with directory granularity)
Chris@584 219 #else
Chris@584 220 QMutexLocker locker(&m_mutex);
Chris@584 221
Chris@584 222 QSet<QString> alreadyWatched =
Chris@584 223 QSet<QString>::fromList(m_watcher.files());
Chris@584 224
Chris@584 225 foreach (QString path, paths) {
Chris@584 226 path = m_workDirPath + QDir::separator() + path;
Chris@584 227 if (!alreadyWatched.contains(path)) {
Chris@584 228 m_watcher.addPath(path);
Chris@584 229 } else {
Chris@584 230 alreadyWatched.remove(path);
Chris@584 231 }
Chris@584 232 }
Chris@584 233
Chris@584 234 // Remove the remaining paths, those that were being watched
Chris@584 235 // before but that are not in the list we were given
Chris@584 236 foreach (QString path, alreadyWatched) {
Chris@584 237 m_watcher.removePath(path);
Chris@584 238 }
Chris@584 239
Chris@584 240 debugPrint();
Chris@584 241 #endif
Chris@538 242 }
Chris@538 243
Chris@538 244 void
Chris@538 245 FsWatcher::setIgnoredFilePrefixes(QStringList prefixes)
Chris@538 246 {
Chris@538 247 QMutexLocker locker(&m_mutex);
Chris@538 248 m_ignoredPrefixes = prefixes;
Chris@538 249 }
Chris@538 250
Chris@538 251 void
Chris@538 252 FsWatcher::setIgnoredFileSuffixes(QStringList suffixes)
Chris@538 253 {
Chris@538 254 QMutexLocker locker(&m_mutex);
Chris@538 255 m_ignoredSuffixes = suffixes;
Chris@538 256 }
Chris@538 257
Chris@538 258 int
Chris@538 259 FsWatcher::getNewToken()
Chris@538 260 {
Chris@538 261 QMutexLocker locker(&m_mutex);
Chris@538 262 int token = ++m_lastToken;
Chris@538 263 m_tokenMap[token] = m_lastCounter;
Chris@538 264 return token;
Chris@538 265 }
Chris@538 266
Chris@538 267 QSet<QString>
Chris@538 268 FsWatcher::getChangedPaths(int token)
Chris@538 269 {
Chris@538 270 QMutexLocker locker(&m_mutex);
Chris@538 271 size_t lastUpdatedAt = m_tokenMap[token];
Chris@538 272 QSet<QString> changed;
Chris@538 273 for (QHash<QString, size_t>::const_iterator i = m_changes.begin();
Chris@538 274 i != m_changes.end(); ++i) {
Chris@538 275 if (i.value() > lastUpdatedAt) {
Chris@538 276 changed.insert(i.key());
Chris@538 277 }
Chris@538 278 }
Chris@538 279 m_tokenMap[token] = m_lastCounter;
Chris@538 280 return changed;
Chris@538 281 }
Chris@538 282
Chris@538 283 void
Chris@538 284 FsWatcher::fsDirectoryChanged(QString path)
Chris@538 285 {
Chris@538 286 {
Chris@538 287 QMutexLocker locker(&m_mutex);
Chris@540 288
Chris@538 289 if (shouldIgnore(path)) return;
Chris@540 290
Chris@540 291 QSet<QString> files = scanDirectory(path);
Chris@540 292 if (files == m_dirContents[path]) {
Chris@540 293 #ifdef DEBUG_FSWATCHER
Chris@540 294 std::cerr << "FsWatcher: Directory " << path << " has changed, but not in a way that we are monitoring" << std::endl;
Chris@540 295 #endif
Chris@540 296 return;
Chris@541 297 } else {
Chris@541 298 #ifdef DEBUG_FSWATCHER
Chris@541 299 std::cerr << "FsWatcher: Directory " << path << " has changed" << std::endl;
Chris@541 300 #endif
Chris@541 301 m_dirContents[path] = files;
Chris@540 302 }
Chris@540 303
Chris@540 304 size_t counter = ++m_lastCounter;
Chris@540 305 m_changes[path] = counter;
Chris@538 306 }
Chris@540 307
Chris@538 308 emit changed();
Chris@538 309 }
Chris@538 310
Chris@538 311 void
Chris@538 312 FsWatcher::fsFileChanged(QString path)
Chris@538 313 {
Chris@540 314 {
Chris@540 315 QMutexLocker locker(&m_mutex);
Chris@540 316
Chris@540 317 // We don't check whether the file matches an ignore pattern,
Chris@540 318 // because we are only notified for file changes if we are
Chris@540 319 // watching the file explicitly, i.e. the file is in the
Chris@540 320 // tracked file paths list. So we never want to ignore them
Chris@540 321
Chris@563 322 #ifdef DEBUG_FSWATCHER
Chris@541 323 std::cerr << "FsWatcher: Tracked file " << path << " has changed" << std::endl;
Chris@563 324 #endif
Chris@541 325
Chris@540 326 size_t counter = ++m_lastCounter;
Chris@540 327 m_changes[path] = counter;
Chris@540 328 }
Chris@540 329
Chris@540 330 emit changed();
Chris@538 331 }
Chris@538 332
Chris@538 333 bool
Chris@538 334 FsWatcher::shouldIgnore(QString path)
Chris@538 335 {
Chris@540 336 QFileInfo fi(path);
Chris@540 337 QString fn(fi.fileName());
Chris@540 338 foreach (QString pfx, m_ignoredPrefixes) {
Chris@541 339 if (fn.startsWith(pfx)) {
Chris@563 340 #ifdef DEBUG_FSWATCHER
Chris@541 341 std::cerr << "(ignoring: " << path << ")" << std::endl;
Chris@563 342 #endif
Chris@541 343 return true;
Chris@541 344 }
Chris@540 345 }
Chris@540 346 foreach (QString sfx, m_ignoredSuffixes) {
Chris@541 347 if (fn.endsWith(sfx)) {
Chris@563 348 #ifdef DEBUG_FSWATCHER
Chris@541 349 std::cerr << "(ignoring: " << path << ")" << std::endl;
Chris@563 350 #endif
Chris@541 351 return true;
Chris@541 352 }
Chris@540 353 }
Chris@540 354 return false;
Chris@538 355 }
Chris@538 356
Chris@540 357 QSet<QString>
Chris@540 358 FsWatcher::scanDirectory(QString path)
Chris@540 359 {
Chris@540 360 QSet<QString> files;
Chris@540 361 QDir d(path);
Chris@540 362 if (d.exists()) {
Chris@540 363 d.setFilter(QDir::Files | QDir::NoDotAndDotDot |
Chris@540 364 QDir::Readable | QDir::NoSymLinks);
Chris@540 365 foreach (QString entry, d.entryList()) {
Chris@540 366 if (entry.startsWith('.')) continue;
Chris@540 367 if (shouldIgnore(entry)) continue;
Chris@540 368 files.insert(entry);
Chris@540 369 }
Chris@540 370 }
Chris@541 371 // std::cerr << "scanDirectory:" << std::endl;
Chris@541 372 // foreach (QString f, files) std::cerr << f << std::endl;
Chris@540 373 return files;
Chris@540 374 }
Chris@540 375
Chris@540 376 void
Chris@540 377 FsWatcher::debugPrint()
Chris@540 378 {
Chris@540 379 #ifdef DEBUG_FSWATCHER
Chris@540 380 std::cerr << "FsWatcher: Now watching " << m_watcher.directories().size()
Chris@540 381 << " directories and " << m_watcher.files().size()
Chris@540 382 << " files" << std::endl;
Chris@540 383 #endif
Chris@540 384 }