annotate src/fswatcher.cpp @ 672:88fa1544b407

Merge from branch qt5. There's much more to be done before we can make another release, but clearly it's going to be done using qt5
author Chris Cannam
date Wed, 05 Dec 2018 09:44:10 +0000
parents a03264984ef8
children 646e48a0d3a5
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@644 8 Copyright (c) 2013 Chris Cannam
Chris@644 9 Copyright (c) 2013 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@647 157 #ifdef DEBUG_FSWATCHER
Chris@586 158 std::cerr << "path " << i << " = " << cpaths[i] << std::endl;
Chris@647 159 #endif
Chris@585 160 watcher->fsDirectoryChanged(QString::fromLocal8Bit(cpaths[i]));
Chris@585 161 }
Chris@539 162 }
Chris@584 163 #endif
Chris@539 164
Chris@539 165 void
Chris@538 166 FsWatcher::addWorkDirectory(QString path)
Chris@538 167 {
Chris@584 168 #ifdef Q_OS_MAC
Chris@585 169
Chris@585 170 CFStringRef cfPath = CFStringCreateWithCharacters
Chris@585 171 (0, reinterpret_cast<const UniChar *>(path.unicode()),
Chris@585 172 path.length());
Chris@585 173
Chris@585 174 CFArrayRef cfPaths = CFArrayCreate(0, (const void **)&cfPath, 1, 0);
Chris@585 175
Chris@585 176 FSEventStreamContext ctx = { 0, 0, 0, 0, 0 };
Chris@585 177 ctx.info = this;
Chris@585 178
Chris@584 179 FSEventStreamRef stream =
Chris@584 180 FSEventStreamCreate(kCFAllocatorDefault,
Chris@585 181 &fsEventsCallback,
Chris@585 182 &ctx,
Chris@584 183 cfPaths,
Chris@584 184 kFSEventStreamEventIdSinceNow,
Chris@584 185 1.0, // latency, seconds
Chris@584 186 kFSEventStreamCreateFlagNone);
Chris@584 187
Chris@584 188 m_stream = stream;
Chris@584 189
Chris@584 190 FSEventStreamScheduleWithRunLoop(stream,
Chris@584 191 CFRunLoopGetCurrent(),
Chris@584 192 kCFRunLoopDefaultMode);
Chris@584 193
Chris@584 194 if (!FSEventStreamStart(stream)) {
Chris@584 195 std::cerr << "ERROR: FsWatcher::addWorkDirectory: Failed to start FSEvent stream" << std::endl;
Chris@584 196 }
Chris@584 197 #else
Chris@538 198 // QFileSystemWatcher will refuse to add a file or directory to
Chris@538 199 // its watch list that it is already watching -- fine -- but it
Chris@538 200 // prints a warning when this happens, which we wouldn't want. So
Chris@538 201 // we'll check for duplicates ourselves.
Chris@538 202 QSet<QString> alreadyWatched =
Chris@538 203 QSet<QString>::fromList(m_watcher.directories());
Chris@538 204
Chris@538 205 std::deque<QString> pending;
Chris@538 206 pending.push_back(path);
Chris@538 207
Chris@538 208 while (!pending.empty()) {
Chris@538 209
Chris@538 210 QString path = pending.front();
Chris@538 211 pending.pop_front();
Chris@538 212 if (!alreadyWatched.contains(path)) {
Chris@538 213 m_watcher.addPath(path);
Chris@540 214 m_dirContents[path] = scanDirectory(path);
Chris@538 215 }
Chris@538 216
Chris@538 217 QDir d(path);
Chris@538 218 if (d.exists()) {
Chris@538 219 d.setFilter(QDir::Dirs | QDir::NoDotAndDotDot |
Chris@538 220 QDir::Readable | QDir::NoSymLinks);
Chris@538 221 foreach (QString entry, d.entryList()) {
Chris@538 222 if (entry.startsWith('.')) continue;
Chris@538 223 QString entryPath = d.absoluteFilePath(entry);
Chris@538 224 pending.push_back(entryPath);
Chris@538 225 }
Chris@538 226 }
Chris@538 227 }
Chris@584 228 #endif
Chris@584 229 }
Chris@584 230
Chris@584 231 void
Chris@584 232 FsWatcher::setTrackedFilePaths(QStringList paths)
Chris@584 233 {
Chris@584 234 #ifdef Q_OS_MAC
Chris@586 235
Chris@586 236 // FSEvents will notify when any file in the directory changes,
Chris@586 237 // but we need to be able to check whether the file change was
Chris@586 238 // meaningful to us if it didn't result in any files being added
Chris@586 239 // or removed -- and we have to do that by examining timestamps on
Chris@586 240 // the files we care about
Chris@586 241 foreach (QString p, paths) {
Chris@586 242 m_trackedFileUpdates[p] = QDateTime::currentDateTime();
Chris@586 243 }
Chris@586 244
Chris@584 245 #else
Chris@586 246
Chris@584 247 QMutexLocker locker(&m_mutex);
Chris@584 248
Chris@584 249 QSet<QString> alreadyWatched =
Chris@584 250 QSet<QString>::fromList(m_watcher.files());
Chris@584 251
Chris@584 252 foreach (QString path, paths) {
Chris@584 253 path = m_workDirPath + QDir::separator() + path;
Chris@584 254 if (!alreadyWatched.contains(path)) {
Chris@584 255 m_watcher.addPath(path);
Chris@584 256 } else {
Chris@584 257 alreadyWatched.remove(path);
Chris@584 258 }
Chris@584 259 }
Chris@584 260
Chris@584 261 // Remove the remaining paths, those that were being watched
Chris@584 262 // before but that are not in the list we were given
Chris@584 263 foreach (QString path, alreadyWatched) {
Chris@584 264 m_watcher.removePath(path);
Chris@584 265 }
Chris@584 266
Chris@584 267 debugPrint();
Chris@586 268
Chris@584 269 #endif
Chris@538 270 }
Chris@538 271
Chris@538 272 void
Chris@538 273 FsWatcher::setIgnoredFilePrefixes(QStringList prefixes)
Chris@538 274 {
Chris@538 275 QMutexLocker locker(&m_mutex);
Chris@538 276 m_ignoredPrefixes = prefixes;
Chris@538 277 }
Chris@538 278
Chris@538 279 void
Chris@538 280 FsWatcher::setIgnoredFileSuffixes(QStringList suffixes)
Chris@538 281 {
Chris@538 282 QMutexLocker locker(&m_mutex);
Chris@538 283 m_ignoredSuffixes = suffixes;
Chris@538 284 }
Chris@538 285
Chris@538 286 int
Chris@538 287 FsWatcher::getNewToken()
Chris@538 288 {
Chris@538 289 QMutexLocker locker(&m_mutex);
Chris@538 290 int token = ++m_lastToken;
Chris@538 291 m_tokenMap[token] = m_lastCounter;
Chris@538 292 return token;
Chris@538 293 }
Chris@538 294
Chris@538 295 QSet<QString>
Chris@538 296 FsWatcher::getChangedPaths(int token)
Chris@538 297 {
Chris@538 298 QMutexLocker locker(&m_mutex);
Chris@538 299 size_t lastUpdatedAt = m_tokenMap[token];
Chris@538 300 QSet<QString> changed;
Chris@538 301 for (QHash<QString, size_t>::const_iterator i = m_changes.begin();
Chris@593 302 i != m_changes.end(); ++i) {
Chris@593 303 if (i.value() > lastUpdatedAt) {
Chris@593 304 changed.insert(i.key());
Chris@593 305 }
Chris@538 306 }
Chris@538 307 m_tokenMap[token] = m_lastCounter;
Chris@538 308 return changed;
Chris@538 309 }
Chris@538 310
Chris@538 311 void
Chris@538 312 FsWatcher::fsDirectoryChanged(QString path)
Chris@538 313 {
Chris@586 314 bool haveChanges = false;
Chris@586 315
Chris@538 316 {
Chris@538 317 QMutexLocker locker(&m_mutex);
Chris@540 318
Chris@538 319 if (shouldIgnore(path)) return;
Chris@540 320
Chris@540 321 QSet<QString> files = scanDirectory(path);
Chris@586 322
Chris@540 323 if (files == m_dirContents[path]) {
Chris@586 324
Chris@540 325 #ifdef DEBUG_FSWATCHER
Chris@593 326 std::cerr << "FsWatcher: Directory " << path << " has changed, but not in a way that we are monitoring -- doing manual check" << std::endl;
Chris@540 327 #endif
Chris@586 328
Chris@586 329 #ifdef Q_OS_MAC
Chris@586 330 haveChanges = manuallyCheckTrackedFiles();
Chris@586 331 #endif
Chris@586 332
Chris@541 333 } else {
Chris@586 334
Chris@541 335 #ifdef DEBUG_FSWATCHER
Chris@541 336 std::cerr << "FsWatcher: Directory " << path << " has changed" << std::endl;
Chris@541 337 #endif
Chris@541 338 m_dirContents[path] = files;
Chris@586 339 size_t counter = ++m_lastCounter;
Chris@586 340 m_changes[path] = counter;
Chris@586 341 haveChanges = true;
Chris@540 342 }
Chris@538 343 }
Chris@540 344
Chris@586 345 if (haveChanges) {
Chris@586 346 emit changed();
Chris@586 347 }
Chris@538 348 }
Chris@538 349
Chris@538 350 void
Chris@538 351 FsWatcher::fsFileChanged(QString path)
Chris@538 352 {
Chris@540 353 {
Chris@540 354 QMutexLocker locker(&m_mutex);
Chris@540 355
Chris@540 356 // We don't check whether the file matches an ignore pattern,
Chris@540 357 // because we are only notified for file changes if we are
Chris@540 358 // watching the file explicitly, i.e. the file is in the
Chris@586 359 // tracked file paths list. So we never want to ignore these
Chris@540 360
Chris@563 361 #ifdef DEBUG_FSWATCHER
Chris@541 362 std::cerr << "FsWatcher: Tracked file " << path << " has changed" << std::endl;
Chris@563 363 #endif
Chris@541 364
Chris@540 365 size_t counter = ++m_lastCounter;
Chris@540 366 m_changes[path] = counter;
Chris@540 367 }
Chris@540 368
Chris@540 369 emit changed();
Chris@538 370 }
Chris@538 371
Chris@586 372 #ifdef Q_OS_MAC
Chris@586 373 bool
Chris@586 374 FsWatcher::manuallyCheckTrackedFiles()
Chris@586 375 {
Chris@647 376 #ifdef DEBUG_FSWATCHER
Chris@593 377 std::cerr << "FsWatcher::manuallyCheckTrackedFiles" << std::endl;
Chris@647 378 #endif
Chris@586 379 bool foundChanges = false;
Chris@586 380
Chris@586 381 for (PathTimeMap::iterator i = m_trackedFileUpdates.begin();
Chris@586 382 i != m_trackedFileUpdates.end(); ++i) {
Chris@586 383
Chris@586 384 QString path = i.key();
Chris@586 385 QDateTime prevUpdate = i.value();
Chris@586 386
Chris@593 387 QFileInfo fi(m_workDirPath + QDir::separator() + path);
Chris@586 388 QDateTime currUpdate = fi.lastModified();
Chris@586 389
Chris@593 390 // std::cerr << "FsWatcher: Tracked file " << path << " previously changed at "
Chris@593 391 // << prevUpdate.toString().toStdString()
Chris@593 392 // << ", currently at " << currUpdate.toString().toStdString() << std::endl;
Chris@593 393
Chris@586 394 if (currUpdate > prevUpdate) {
Chris@586 395
Chris@586 396 #ifdef DEBUG_FSWATCHER
Chris@586 397 std::cerr << "FsWatcher: Tracked file " << path << " has been changed since last check" << std::endl;
Chris@586 398 #endif
Chris@586 399 i.value() = currUpdate;
Chris@586 400
Chris@586 401 size_t counter = ++m_lastCounter;
Chris@586 402 m_changes[path] = counter;
Chris@586 403 foundChanges = true;
Chris@586 404 }
Chris@586 405 }
Chris@586 406
Chris@586 407 return foundChanges;
Chris@586 408 }
Chris@586 409 #endif
Chris@586 410
Chris@538 411 bool
Chris@538 412 FsWatcher::shouldIgnore(QString path)
Chris@538 413 {
Chris@540 414 QFileInfo fi(path);
Chris@540 415 QString fn(fi.fileName());
Chris@540 416 foreach (QString pfx, m_ignoredPrefixes) {
Chris@541 417 if (fn.startsWith(pfx)) {
Chris@563 418 #ifdef DEBUG_FSWATCHER
Chris@541 419 std::cerr << "(ignoring: " << path << ")" << std::endl;
Chris@563 420 #endif
Chris@541 421 return true;
Chris@541 422 }
Chris@540 423 }
Chris@540 424 foreach (QString sfx, m_ignoredSuffixes) {
Chris@541 425 if (fn.endsWith(sfx)) {
Chris@563 426 #ifdef DEBUG_FSWATCHER
Chris@541 427 std::cerr << "(ignoring: " << path << ")" << std::endl;
Chris@563 428 #endif
Chris@541 429 return true;
Chris@541 430 }
Chris@540 431 }
Chris@540 432 return false;
Chris@538 433 }
Chris@538 434
Chris@540 435 QSet<QString>
Chris@540 436 FsWatcher::scanDirectory(QString path)
Chris@540 437 {
Chris@540 438 QSet<QString> files;
Chris@540 439 QDir d(path);
Chris@540 440 if (d.exists()) {
Chris@540 441 d.setFilter(QDir::Files | QDir::NoDotAndDotDot |
Chris@540 442 QDir::Readable | QDir::NoSymLinks);
Chris@540 443 foreach (QString entry, d.entryList()) {
Chris@540 444 if (entry.startsWith('.')) continue;
Chris@540 445 if (shouldIgnore(entry)) continue;
Chris@540 446 files.insert(entry);
Chris@540 447 }
Chris@540 448 }
Chris@540 449 return files;
Chris@540 450 }
Chris@540 451
Chris@540 452 void
Chris@540 453 FsWatcher::debugPrint()
Chris@540 454 {
Chris@540 455 #ifdef DEBUG_FSWATCHER
Chris@585 456 #ifndef Q_OS_MAC
Chris@540 457 std::cerr << "FsWatcher: Now watching " << m_watcher.directories().size()
Chris@540 458 << " directories and " << m_watcher.files().size()
Chris@540 459 << " files" << std::endl;
Chris@540 460 #endif
Chris@585 461 #endif
Chris@540 462 }