annotate src/fswatcher.cpp @ 737:4f3a8aa8d384 tip

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