annotate src/fswatcher.cpp @ 633:db62a0cb3037

* Added setting to specify that diff command should be run once for each selected file, rather than passing all file names to diff command (p4merge doesn't like being given many files)
author Sam Izzo <sam@humbug.net>
date Mon, 27 Aug 2012 01:26:57 +1000
parents 10ef94e51f44
children ae67ea0af696
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@603 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@586 81 * notifies with directory granularity, but that might be OK for us so
Chris@586 82 * long as notifications are actually provoked by file changes as
Chris@586 83 * well. (In OS/X 10.7 there appear to be file-level notifications
Chris@586 84 * too, but that doesn't help us.)
Chris@584 85 *
Chris@584 86 * One other problem with FSEvents is that the API only exists on OS/X
Chris@584 87 * 10.5 or newer -- on older versions we would have no option but to
Chris@584 88 * use kqueue via QFileSystemWatcher. But we can't ship a binary
Chris@584 89 * linked with the FSEvents API to run on 10.4 without some fiddling,
Chris@584 90 * and I'm not really keen to do that either. That may be our cue to
Chris@584 91 * drop 10.4 support for EasyMercurial.
Chris@539 92 */
Chris@539 93
Chris@538 94 FsWatcher::FsWatcher() :
Chris@538 95 m_lastToken(0),
Chris@538 96 m_lastCounter(0)
Chris@538 97 {
Chris@584 98 #ifdef Q_OS_MAC
Chris@584 99 m_stream = 0; // create when we have a path
Chris@584 100 #else
Chris@538 101 connect(&m_watcher, SIGNAL(directoryChanged(QString)),
Chris@538 102 this, SLOT(fsDirectoryChanged(QString)));
Chris@538 103 connect(&m_watcher, SIGNAL(fileChanged(QString)),
Chris@538 104 this, SLOT(fsFileChanged(QString)));
Chris@584 105 #endif
Chris@538 106 }
Chris@538 107
Chris@538 108 FsWatcher::~FsWatcher()
Chris@538 109 {
Chris@538 110 }
Chris@538 111
Chris@538 112 void
Chris@538 113 FsWatcher::setWorkDirPath(QString path)
Chris@538 114 {
Chris@538 115 QMutexLocker locker(&m_mutex);
Chris@541 116 if (m_workDirPath == path) return;
Chris@584 117 clearWatchedPaths();
Chris@584 118 m_workDirPath = path;
Chris@584 119 addWorkDirectory(path);
Chris@584 120 debugPrint();
Chris@584 121 }
Chris@584 122
Chris@584 123 void
Chris@584 124 FsWatcher::clearWatchedPaths()
Chris@584 125 {
Chris@584 126 #ifdef Q_OS_MAC
Chris@584 127 FSEventStreamRef stream = (FSEventStreamRef)m_stream;
Chris@584 128 if (stream) {
Chris@584 129 FSEventStreamStop(stream);
Chris@584 130 FSEventStreamInvalidate(stream);
Chris@584 131 FSEventStreamRelease(stream);
Chris@584 132 }
Chris@584 133 m_stream = 0;
Chris@584 134 #else
Chris@562 135 // annoyingly, removePaths prints a warning if given an empty list
Chris@562 136 if (!m_watcher.directories().empty()) {
Chris@562 137 m_watcher.removePaths(m_watcher.directories());
Chris@562 138 }
Chris@562 139 if (!m_watcher.files().empty()) {
Chris@562 140 m_watcher.removePaths(m_watcher.files());
Chris@562 141 }
Chris@584 142 #endif
Chris@538 143 }
Chris@538 144
Chris@584 145 #ifdef Q_OS_MAC
Chris@584 146 static void
Chris@585 147 fsEventsCallback(ConstFSEventStreamRef streamRef,
Chris@584 148 void *clientCallBackInfo,
Chris@585 149 size_t numEvents,
Chris@585 150 void *paths,
Chris@585 151 const FSEventStreamEventFlags eventFlags[],
Chris@585 152 const FSEventStreamEventId eventIDs[])
Chris@539 153 {
Chris@585 154 FsWatcher *watcher = reinterpret_cast<FsWatcher *>(clientCallBackInfo);
Chris@585 155 const char *const *cpaths = reinterpret_cast<const char *const *>(paths);
Chris@585 156 for (size_t i = 0; i < numEvents; ++i) {
Chris@586 157 std::cerr << "path " << i << " = " << cpaths[i] << std::endl;
Chris@585 158 watcher->fsDirectoryChanged(QString::fromLocal8Bit(cpaths[i]));
Chris@585 159 }
Chris@539 160 }
Chris@584 161 #endif
Chris@539 162
Chris@539 163 void
Chris@538 164 FsWatcher::addWorkDirectory(QString path)
Chris@538 165 {
Chris@584 166 #ifdef Q_OS_MAC
Chris@585 167
Chris@585 168 CFStringRef cfPath = CFStringCreateWithCharacters
Chris@585 169 (0, reinterpret_cast<const UniChar *>(path.unicode()),
Chris@585 170 path.length());
Chris@585 171
Chris@585 172 CFArrayRef cfPaths = CFArrayCreate(0, (const void **)&cfPath, 1, 0);
Chris@585 173
Chris@585 174 FSEventStreamContext ctx = { 0, 0, 0, 0, 0 };
Chris@585 175 ctx.info = this;
Chris@585 176
Chris@584 177 FSEventStreamRef stream =
Chris@584 178 FSEventStreamCreate(kCFAllocatorDefault,
Chris@585 179 &fsEventsCallback,
Chris@585 180 &ctx,
Chris@584 181 cfPaths,
Chris@584 182 kFSEventStreamEventIdSinceNow,
Chris@584 183 1.0, // latency, seconds
Chris@584 184 kFSEventStreamCreateFlagNone);
Chris@584 185
Chris@584 186 m_stream = stream;
Chris@584 187
Chris@584 188 FSEventStreamScheduleWithRunLoop(stream,
Chris@584 189 CFRunLoopGetCurrent(),
Chris@584 190 kCFRunLoopDefaultMode);
Chris@584 191
Chris@584 192 if (!FSEventStreamStart(stream)) {
Chris@584 193 std::cerr << "ERROR: FsWatcher::addWorkDirectory: Failed to start FSEvent stream" << std::endl;
Chris@584 194 }
Chris@584 195 #else
Chris@538 196 // QFileSystemWatcher will refuse to add a file or directory to
Chris@538 197 // its watch list that it is already watching -- fine -- but it
Chris@538 198 // prints a warning when this happens, which we wouldn't want. So
Chris@538 199 // we'll check for duplicates ourselves.
Chris@538 200 QSet<QString> alreadyWatched =
Chris@538 201 QSet<QString>::fromList(m_watcher.directories());
Chris@538 202
Chris@538 203 std::deque<QString> pending;
Chris@538 204 pending.push_back(path);
Chris@538 205
Chris@538 206 while (!pending.empty()) {
Chris@538 207
Chris@538 208 QString path = pending.front();
Chris@538 209 pending.pop_front();
Chris@538 210 if (!alreadyWatched.contains(path)) {
Chris@538 211 m_watcher.addPath(path);
Chris@540 212 m_dirContents[path] = scanDirectory(path);
Chris@538 213 }
Chris@538 214
Chris@538 215 QDir d(path);
Chris@538 216 if (d.exists()) {
Chris@538 217 d.setFilter(QDir::Dirs | QDir::NoDotAndDotDot |
Chris@538 218 QDir::Readable | QDir::NoSymLinks);
Chris@538 219 foreach (QString entry, d.entryList()) {
Chris@538 220 if (entry.startsWith('.')) continue;
Chris@538 221 QString entryPath = d.absoluteFilePath(entry);
Chris@538 222 pending.push_back(entryPath);
Chris@538 223 }
Chris@538 224 }
Chris@538 225 }
Chris@584 226 #endif
Chris@584 227 }
Chris@584 228
Chris@584 229 void
Chris@584 230 FsWatcher::setTrackedFilePaths(QStringList paths)
Chris@584 231 {
Chris@584 232 #ifdef Q_OS_MAC
Chris@586 233
Chris@586 234 // FSEvents will notify when any file in the directory changes,
Chris@586 235 // but we need to be able to check whether the file change was
Chris@586 236 // meaningful to us if it didn't result in any files being added
Chris@586 237 // or removed -- and we have to do that by examining timestamps on
Chris@586 238 // the files we care about
Chris@586 239 foreach (QString p, paths) {
Chris@586 240 m_trackedFileUpdates[p] = QDateTime::currentDateTime();
Chris@586 241 }
Chris@586 242
Chris@584 243 #else
Chris@586 244
Chris@584 245 QMutexLocker locker(&m_mutex);
Chris@584 246
Chris@584 247 QSet<QString> alreadyWatched =
Chris@584 248 QSet<QString>::fromList(m_watcher.files());
Chris@584 249
Chris@584 250 foreach (QString path, paths) {
Chris@584 251 path = m_workDirPath + QDir::separator() + path;
Chris@584 252 if (!alreadyWatched.contains(path)) {
Chris@584 253 m_watcher.addPath(path);
Chris@584 254 } else {
Chris@584 255 alreadyWatched.remove(path);
Chris@584 256 }
Chris@584 257 }
Chris@584 258
Chris@584 259 // Remove the remaining paths, those that were being watched
Chris@584 260 // before but that are not in the list we were given
Chris@584 261 foreach (QString path, alreadyWatched) {
Chris@584 262 m_watcher.removePath(path);
Chris@584 263 }
Chris@584 264
Chris@584 265 debugPrint();
Chris@586 266
Chris@584 267 #endif
Chris@538 268 }
Chris@538 269
Chris@538 270 void
Chris@538 271 FsWatcher::setIgnoredFilePrefixes(QStringList prefixes)
Chris@538 272 {
Chris@538 273 QMutexLocker locker(&m_mutex);
Chris@538 274 m_ignoredPrefixes = prefixes;
Chris@538 275 }
Chris@538 276
Chris@538 277 void
Chris@538 278 FsWatcher::setIgnoredFileSuffixes(QStringList suffixes)
Chris@538 279 {
Chris@538 280 QMutexLocker locker(&m_mutex);
Chris@538 281 m_ignoredSuffixes = suffixes;
Chris@538 282 }
Chris@538 283
Chris@538 284 int
Chris@538 285 FsWatcher::getNewToken()
Chris@538 286 {
Chris@538 287 QMutexLocker locker(&m_mutex);
Chris@538 288 int token = ++m_lastToken;
Chris@538 289 m_tokenMap[token] = m_lastCounter;
Chris@538 290 return token;
Chris@538 291 }
Chris@538 292
Chris@538 293 QSet<QString>
Chris@538 294 FsWatcher::getChangedPaths(int token)
Chris@538 295 {
Chris@538 296 QMutexLocker locker(&m_mutex);
Chris@538 297 size_t lastUpdatedAt = m_tokenMap[token];
Chris@538 298 QSet<QString> changed;
Chris@538 299 for (QHash<QString, size_t>::const_iterator i = m_changes.begin();
Chris@593 300 i != m_changes.end(); ++i) {
Chris@593 301 if (i.value() > lastUpdatedAt) {
Chris@593 302 changed.insert(i.key());
Chris@593 303 }
Chris@538 304 }
Chris@538 305 m_tokenMap[token] = m_lastCounter;
Chris@538 306 return changed;
Chris@538 307 }
Chris@538 308
Chris@538 309 void
Chris@538 310 FsWatcher::fsDirectoryChanged(QString path)
Chris@538 311 {
Chris@586 312 bool haveChanges = false;
Chris@586 313
Chris@538 314 {
Chris@538 315 QMutexLocker locker(&m_mutex);
Chris@540 316
Chris@538 317 if (shouldIgnore(path)) return;
Chris@540 318
Chris@540 319 QSet<QString> files = scanDirectory(path);
Chris@586 320
Chris@540 321 if (files == m_dirContents[path]) {
Chris@586 322
Chris@540 323 #ifdef DEBUG_FSWATCHER
Chris@593 324 std::cerr << "FsWatcher: Directory " << path << " has changed, but not in a way that we are monitoring -- doing manual check" << std::endl;
Chris@540 325 #endif
Chris@586 326
Chris@586 327 #ifdef Q_OS_MAC
Chris@586 328 haveChanges = manuallyCheckTrackedFiles();
Chris@586 329 #endif
Chris@586 330
Chris@541 331 } else {
Chris@586 332
Chris@541 333 #ifdef DEBUG_FSWATCHER
Chris@541 334 std::cerr << "FsWatcher: Directory " << path << " has changed" << std::endl;
Chris@541 335 #endif
Chris@541 336 m_dirContents[path] = files;
Chris@586 337 size_t counter = ++m_lastCounter;
Chris@586 338 m_changes[path] = counter;
Chris@586 339 haveChanges = true;
Chris@540 340 }
Chris@538 341 }
Chris@540 342
Chris@586 343 if (haveChanges) {
Chris@586 344 emit changed();
Chris@586 345 }
Chris@538 346 }
Chris@538 347
Chris@538 348 void
Chris@538 349 FsWatcher::fsFileChanged(QString path)
Chris@538 350 {
Chris@540 351 {
Chris@540 352 QMutexLocker locker(&m_mutex);
Chris@540 353
Chris@540 354 // We don't check whether the file matches an ignore pattern,
Chris@540 355 // because we are only notified for file changes if we are
Chris@540 356 // watching the file explicitly, i.e. the file is in the
Chris@586 357 // tracked file paths list. So we never want to ignore these
Chris@540 358
Chris@563 359 #ifdef DEBUG_FSWATCHER
Chris@541 360 std::cerr << "FsWatcher: Tracked file " << path << " has changed" << std::endl;
Chris@563 361 #endif
Chris@541 362
Chris@540 363 size_t counter = ++m_lastCounter;
Chris@540 364 m_changes[path] = counter;
Chris@540 365 }
Chris@540 366
Chris@540 367 emit changed();
Chris@538 368 }
Chris@538 369
Chris@586 370 #ifdef Q_OS_MAC
Chris@586 371 bool
Chris@586 372 FsWatcher::manuallyCheckTrackedFiles()
Chris@586 373 {
Chris@593 374 std::cerr << "FsWatcher::manuallyCheckTrackedFiles" << std::endl;
Chris@586 375 bool foundChanges = false;
Chris@586 376
Chris@586 377 for (PathTimeMap::iterator i = m_trackedFileUpdates.begin();
Chris@586 378 i != m_trackedFileUpdates.end(); ++i) {
Chris@586 379
Chris@586 380 QString path = i.key();
Chris@586 381 QDateTime prevUpdate = i.value();
Chris@586 382
Chris@593 383 QFileInfo fi(m_workDirPath + QDir::separator() + path);
Chris@586 384 QDateTime currUpdate = fi.lastModified();
Chris@586 385
Chris@593 386 // std::cerr << "FsWatcher: Tracked file " << path << " previously changed at "
Chris@593 387 // << prevUpdate.toString().toStdString()
Chris@593 388 // << ", currently at " << currUpdate.toString().toStdString() << std::endl;
Chris@593 389
Chris@586 390 if (currUpdate > prevUpdate) {
Chris@586 391
Chris@586 392 #ifdef DEBUG_FSWATCHER
Chris@586 393 std::cerr << "FsWatcher: Tracked file " << path << " has been changed since last check" << std::endl;
Chris@586 394 #endif
Chris@586 395 i.value() = currUpdate;
Chris@586 396
Chris@586 397 size_t counter = ++m_lastCounter;
Chris@586 398 m_changes[path] = counter;
Chris@586 399 foundChanges = true;
Chris@586 400 }
Chris@586 401 }
Chris@586 402
Chris@586 403 return foundChanges;
Chris@586 404 }
Chris@586 405 #endif
Chris@586 406
Chris@538 407 bool
Chris@538 408 FsWatcher::shouldIgnore(QString path)
Chris@538 409 {
Chris@540 410 QFileInfo fi(path);
Chris@540 411 QString fn(fi.fileName());
Chris@540 412 foreach (QString pfx, m_ignoredPrefixes) {
Chris@541 413 if (fn.startsWith(pfx)) {
Chris@563 414 #ifdef DEBUG_FSWATCHER
Chris@541 415 std::cerr << "(ignoring: " << path << ")" << std::endl;
Chris@563 416 #endif
Chris@541 417 return true;
Chris@541 418 }
Chris@540 419 }
Chris@540 420 foreach (QString sfx, m_ignoredSuffixes) {
Chris@541 421 if (fn.endsWith(sfx)) {
Chris@563 422 #ifdef DEBUG_FSWATCHER
Chris@541 423 std::cerr << "(ignoring: " << path << ")" << std::endl;
Chris@563 424 #endif
Chris@541 425 return true;
Chris@541 426 }
Chris@540 427 }
Chris@540 428 return false;
Chris@538 429 }
Chris@538 430
Chris@540 431 QSet<QString>
Chris@540 432 FsWatcher::scanDirectory(QString path)
Chris@540 433 {
Chris@540 434 QSet<QString> files;
Chris@540 435 QDir d(path);
Chris@540 436 if (d.exists()) {
Chris@540 437 d.setFilter(QDir::Files | QDir::NoDotAndDotDot |
Chris@540 438 QDir::Readable | QDir::NoSymLinks);
Chris@540 439 foreach (QString entry, d.entryList()) {
Chris@540 440 if (entry.startsWith('.')) continue;
Chris@540 441 if (shouldIgnore(entry)) continue;
Chris@540 442 files.insert(entry);
Chris@540 443 }
Chris@540 444 }
Chris@540 445 return files;
Chris@540 446 }
Chris@540 447
Chris@540 448 void
Chris@540 449 FsWatcher::debugPrint()
Chris@540 450 {
Chris@540 451 #ifdef DEBUG_FSWATCHER
Chris@585 452 #ifndef Q_OS_MAC
Chris@540 453 std::cerr << "FsWatcher: Now watching " << m_watcher.directories().size()
Chris@540 454 << " directories and " << m_watcher.files().size()
Chris@540 455 << " files" << std::endl;
Chris@540 456 #endif
Chris@585 457 #endif
Chris@540 458 }