1/**************************************************************************** 2** 3** Copyright (C) 2016 The Qt Company Ltd. 4** Contact: https://www.qt.io/licensing/ 5** 6** This file is part of the QtCore module of the Qt Toolkit. 7** 8** $QT_BEGIN_LICENSE:LGPL$ 9** Commercial License Usage 10** Licensees holding valid commercial Qt licenses may use this file in 11** accordance with the commercial license agreement provided with the 12** Software or, alternatively, in accordance with the terms contained in 13** a written agreement between you and The Qt Company. For licensing terms 14** and conditions see https://www.qt.io/terms-conditions. For further 15** information use the contact form at https://www.qt.io/contact-us. 16** 17** GNU Lesser General Public License Usage 18** Alternatively, this file may be used under the terms of the GNU Lesser 19** General Public License version 3 as published by the Free Software 20** Foundation and appearing in the file LICENSE.LGPL3 included in the 21** packaging of this file. Please review the following information to 22** ensure the GNU Lesser General Public License version 3 requirements 23** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. 24** 25** GNU General Public License Usage 26** Alternatively, this file may be used under the terms of the GNU 27** General Public License version 2.0 or (at your option) the GNU General 28** Public license version 3 or any later version approved by the KDE Free 29** Qt Foundation. The licenses are as published by the Free Software 30** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 31** included in the packaging of this file. Please review the following 32** information to ensure the GNU General Public License requirements will 33** be met: https://www.gnu.org/licenses/gpl-2.0.html and 34** https://www.gnu.org/licenses/gpl-3.0.html. 35** 36** $QT_END_LICENSE$ 37** 38****************************************************************************/ 39 40#include <qplatformdefs.h> 41 42#include "qdiriterator.h" 43#include "qfilesystemwatcher.h" 44#include "qfilesystemwatcher_fsevents_p.h" 45#include "private/qcore_unix_p.h" 46#include "kernel/qcore_mac_p.h" 47 48#include <qdebug.h> 49#include <qdir.h> 50#include <qfile.h> 51#include <qfileinfo.h> 52#include <qvarlengtharray.h> 53#include <qscopeguard.h> 54 55#undef FSEVENT_DEBUG 56#ifdef FSEVENT_DEBUG 57# define DEBUG if (true) qDebug 58#else 59# define DEBUG if (false) qDebug 60#endif 61 62QT_BEGIN_NAMESPACE 63 64static void callBackFunction(ConstFSEventStreamRef streamRef, 65 void *clientCallBackInfo, 66 size_t numEvents, 67 void *eventPaths, 68 const FSEventStreamEventFlags eventFlags[], 69 const FSEventStreamEventId eventIds[]) 70{ 71 QMacAutoReleasePool pool; 72 73 char **paths = static_cast<char **>(eventPaths); 74 QFseventsFileSystemWatcherEngine *engine = static_cast<QFseventsFileSystemWatcherEngine *>(clientCallBackInfo); 75 engine->processEvent(streamRef, numEvents, paths, eventFlags, eventIds); 76} 77 78bool QFseventsFileSystemWatcherEngine::checkDir(DirsByName::iterator &it) 79{ 80 bool needsRestart = false; 81 82 QT_STATBUF st; 83 const QString &name = it.key(); 84 Info &info = it->dirInfo; 85 const int res = QT_STAT(QFile::encodeName(name), &st); 86 if (res == -1) { 87 needsRestart |= derefPath(info.watchedPath); 88 emit emitDirectoryChanged(info.origPath, true); 89 it = watchingState.watchedDirectories.erase(it); 90 } else if (st.st_ctimespec != info.ctime || st.st_mode != info.mode) { 91 info.ctime = st.st_ctimespec; 92 info.mode = st.st_mode; 93 emit emitDirectoryChanged(info.origPath, false); 94 ++it; 95 } else { 96 bool dirChanged = false; 97 InfoByName &entries = it->entries; 98 // check known entries: 99 for (InfoByName::iterator i = entries.begin(); i != entries.end(); ) { 100 if (QT_STAT(QFile::encodeName(i.key()), &st) == -1) { 101 // entry disappeared 102 dirChanged = true; 103 i = entries.erase(i); 104 } else { 105 if (i->ctime != st.st_ctimespec || i->mode != st.st_mode) { 106 // entry changed 107 dirChanged = true; 108 i->ctime = st.st_ctimespec; 109 i->mode = st.st_mode; 110 } 111 ++i; 112 } 113 } 114 // check for new entries: 115 QDirIterator dirIt(name); 116 while (dirIt.hasNext()) { 117 dirIt.next(); 118 QString entryName = dirIt.filePath(); 119 if (!entries.contains(entryName)) { 120 dirChanged = true; 121 QT_STATBUF st; 122 if (QT_STAT(QFile::encodeName(entryName), &st) == -1) 123 continue; 124 entries.insert(entryName, Info(QString(), st.st_ctimespec, st.st_mode, QString())); 125 126 } 127 } 128 if (dirChanged) 129 emit emitDirectoryChanged(info.origPath, false); 130 ++it; 131 } 132 133 return needsRestart; 134} 135 136bool QFseventsFileSystemWatcherEngine::rescanDirs(const QString &path) 137{ 138 bool needsRestart = false; 139 140 for (DirsByName::iterator it = watchingState.watchedDirectories.begin(); 141 it != watchingState.watchedDirectories.end(); ) { 142 if (it.key().startsWith(path)) 143 needsRestart |= checkDir(it); 144 else 145 ++it; 146 } 147 148 return needsRestart; 149} 150 151bool QFseventsFileSystemWatcherEngine::rescanFiles(InfoByName &filesInPath) 152{ 153 bool needsRestart = false; 154 155 for (InfoByName::iterator it = filesInPath.begin(); it != filesInPath.end(); ) { 156 QT_STATBUF st; 157 QString name = it.key(); 158 const int res = QT_STAT(QFile::encodeName(name), &st); 159 if (res == -1) { 160 needsRestart |= derefPath(it->watchedPath); 161 emit emitFileChanged(it.value().origPath, true); 162 it = filesInPath.erase(it); 163 continue; 164 } else if (st.st_ctimespec != it->ctime || st.st_mode != it->mode) { 165 it->ctime = st.st_ctimespec; 166 it->mode = st.st_mode; 167 emit emitFileChanged(it.value().origPath, false); 168 } 169 170 ++it; 171 } 172 173 return needsRestart; 174} 175 176bool QFseventsFileSystemWatcherEngine::rescanFiles(const QString &path) 177{ 178 bool needsRestart = false; 179 180 for (FilesByPath::iterator i = watchingState.watchedFiles.begin(); 181 i != watchingState.watchedFiles.end(); ) { 182 if (i.key().startsWith(path)) { 183 needsRestart |= rescanFiles(i.value()); 184 if (i.value().isEmpty()) { 185 i = watchingState.watchedFiles.erase(i); 186 continue; 187 } 188 } 189 190 ++i; 191 } 192 193 return needsRestart; 194} 195 196void QFseventsFileSystemWatcherEngine::processEvent(ConstFSEventStreamRef streamRef, 197 size_t numEvents, 198 char **eventPaths, 199 const FSEventStreamEventFlags eventFlags[], 200 const FSEventStreamEventId eventIds[]) 201{ 202#if defined(Q_OS_MACOS) 203 Q_UNUSED(streamRef); 204 205 bool needsRestart = false; 206 207 QMutexLocker locker(&lock); 208 209 for (size_t i = 0; i < numEvents; ++i) { 210 FSEventStreamEventFlags eFlags = eventFlags[i]; 211 DEBUG("Change %llu in %s, flags %x", eventIds[i], eventPaths[i], (unsigned int)eFlags); 212 213 if (eFlags & kFSEventStreamEventFlagEventIdsWrapped) { 214 DEBUG("\tthe event ids wrapped"); 215 lastReceivedEvent = 0; 216 } 217 lastReceivedEvent = qMax(lastReceivedEvent, eventIds[i]); 218 219 QString path = QFile::decodeName(eventPaths[i]); 220 if (path.endsWith(QDir::separator())) 221 path = path.mid(0, path.size() - 1); 222 223 if (eFlags & kFSEventStreamEventFlagMustScanSubDirs) { 224 DEBUG("\tmust rescan directory because of coalesced events"); 225 if (eFlags & kFSEventStreamEventFlagUserDropped) 226 DEBUG("\t\t... user dropped."); 227 if (eFlags & kFSEventStreamEventFlagKernelDropped) 228 DEBUG("\t\t... kernel dropped."); 229 needsRestart |= rescanDirs(path); 230 needsRestart |= rescanFiles(path); 231 continue; 232 } 233 234 if (eFlags & kFSEventStreamEventFlagRootChanged) { 235 // re-check everything: 236 DirsByName::iterator dirIt = watchingState.watchedDirectories.find(path); 237 if (dirIt != watchingState.watchedDirectories.end()) 238 needsRestart |= checkDir(dirIt); 239 needsRestart |= rescanFiles(path); 240 continue; 241 } 242 243 if ((eFlags & kFSEventStreamEventFlagItemIsDir) && (eFlags & kFSEventStreamEventFlagItemRemoved)) 244 needsRestart |= rescanDirs(path); 245 246 // check watched directories: 247 DirsByName::iterator dirIt = watchingState.watchedDirectories.find(path); 248 if (dirIt != watchingState.watchedDirectories.end()) 249 needsRestart |= checkDir(dirIt); 250 251 // check watched files: 252 FilesByPath::iterator pIt = watchingState.watchedFiles.find(path); 253 if (pIt != watchingState.watchedFiles.end()) 254 needsRestart |= rescanFiles(pIt.value()); 255 } 256 257 if (needsRestart) 258 emit scheduleStreamRestart(); 259#else 260 Q_UNUSED(streamRef); 261 Q_UNUSED(numEvents); 262 Q_UNUSED(eventPaths); 263 Q_UNUSED(eventFlags); 264 Q_UNUSED(eventIds); 265#endif 266} 267 268void QFseventsFileSystemWatcherEngine::doEmitFileChanged(const QString &path, bool removed) 269{ 270 DEBUG() << "emitting fileChanged for" << path << "with removed =" << removed; 271 emit fileChanged(path, removed); 272} 273 274void QFseventsFileSystemWatcherEngine::doEmitDirectoryChanged(const QString &path, bool removed) 275{ 276 DEBUG() << "emitting directoryChanged for" << path << "with removed =" << removed; 277 emit directoryChanged(path, removed); 278} 279 280bool QFseventsFileSystemWatcherEngine::restartStream() 281{ 282 QMutexLocker locker(&lock); 283 stopStream(); 284 return startStream(); 285} 286 287QFseventsFileSystemWatcherEngine *QFseventsFileSystemWatcherEngine::create(QObject *parent) 288{ 289 return new QFseventsFileSystemWatcherEngine(parent); 290} 291 292QFseventsFileSystemWatcherEngine::QFseventsFileSystemWatcherEngine(QObject *parent) 293 : QFileSystemWatcherEngine(parent) 294 , stream(0) 295 , lastReceivedEvent(kFSEventStreamEventIdSinceNow) 296{ 297 298 // We cannot use signal-to-signal queued connections, because the 299 // QSignalSpy cannot spot signals fired from other/alien threads. 300 connect(this, SIGNAL(emitDirectoryChanged(QString,bool)), 301 this, SLOT(doEmitDirectoryChanged(QString,bool)), Qt::QueuedConnection); 302 connect(this, SIGNAL(emitFileChanged(QString,bool)), 303 this, SLOT(doEmitFileChanged(QString,bool)), Qt::QueuedConnection); 304 connect(this, SIGNAL(scheduleStreamRestart()), 305 this, SLOT(restartStream()), Qt::QueuedConnection); 306 307 queue = dispatch_queue_create("org.qt-project.QFseventsFileSystemWatcherEngine", NULL); 308} 309 310QFseventsFileSystemWatcherEngine::~QFseventsFileSystemWatcherEngine() 311{ 312 QMacAutoReleasePool pool; 313 314 dispatch_sync(queue, ^{ 315 // Stop the stream in case we have to wait for the lock below to be acquired. 316 if (stream) 317 FSEventStreamStop(stream); 318 319 // The assumption with the locking strategy is that this class cannot and will not be subclassed! 320 QMutexLocker locker(&lock); 321 322 stopStream(true); 323 }); 324 dispatch_release(queue); 325} 326 327QStringList QFseventsFileSystemWatcherEngine::addPaths(const QStringList &paths, 328 QStringList *files, 329 QStringList *directories) 330{ 331 QMacAutoReleasePool pool; 332 333 if (stream) { 334 DEBUG("Flushing, last id is %llu", FSEventStreamGetLatestEventId(stream)); 335 FSEventStreamFlushSync(stream); 336 } 337 338 QMutexLocker locker(&lock); 339 340 bool wasRunning = stream != nullptr; 341 bool needsRestart = false; 342 343 WatchingState oldState = watchingState; 344 QStringList unhandled; 345 for (const QString &path : paths) { 346 auto sg = qScopeGuard([&]{ unhandled.push_back(path); }); 347 QString origPath = path.normalized(QString::NormalizationForm_C); 348 QString realPath = origPath; 349 if (realPath.endsWith(QDir::separator())) 350 realPath = realPath.mid(0, realPath.size() - 1); 351 QString watchedPath, parentPath; 352 353 realPath = QFileInfo(realPath).canonicalFilePath(); 354 QFileInfo fi(realPath); 355 if (realPath.isEmpty()) 356 continue; 357 358 QT_STATBUF st; 359 if (QT_STAT(QFile::encodeName(realPath), &st) == -1) 360 continue; 361 362 const bool isDir = S_ISDIR(st.st_mode); 363 if (isDir) { 364 if (watchingState.watchedDirectories.contains(realPath)) 365 continue; 366 directories->append(origPath); 367 watchedPath = realPath; 368 } else { 369 if (files->contains(origPath)) 370 continue; 371 files->append(origPath); 372 373 watchedPath = fi.path(); 374 parentPath = watchedPath; 375 } 376 377 sg.dismiss(); 378 379 for (PathRefCounts::const_iterator i = watchingState.watchedPaths.begin(), 380 ei = watchingState.watchedPaths.end(); i != ei; ++i) { 381 if (watchedPath.startsWith(i.key() % QDir::separator())) { 382 watchedPath = i.key(); 383 break; 384 } 385 } 386 387 PathRefCounts::iterator it = watchingState.watchedPaths.find(watchedPath); 388 if (it == watchingState.watchedPaths.end()) { 389 needsRestart = true; 390 watchingState.watchedPaths.insert(watchedPath, 1); 391 DEBUG("Adding '%s' to watchedPaths", qPrintable(watchedPath)); 392 } else { 393 ++it.value(); 394 } 395 396 Info info(origPath, st.st_ctimespec, st.st_mode, watchedPath); 397 if (isDir) { 398 DirInfo dirInfo; 399 dirInfo.dirInfo = info; 400 dirInfo.entries = scanForDirEntries(realPath); 401 watchingState.watchedDirectories.insert(realPath, dirInfo); 402 DEBUG("-- Also adding '%s' to watchedDirectories", qPrintable(realPath)); 403 } else { 404 watchingState.watchedFiles[parentPath].insert(realPath, info); 405 DEBUG("-- Also adding '%s' to watchedFiles", qPrintable(realPath)); 406 } 407 } 408 409 if (needsRestart) { 410 stopStream(); 411 if (!startStream()) { 412 // ok, something went wrong, let's try to restore the previous state 413 watchingState = std::move(oldState); 414 // and because we don't know which path caused the issue (if any), fail on all of them 415 unhandled = paths; 416 417 if (wasRunning) 418 startStream(); 419 } 420 } 421 422 return unhandled; 423} 424 425QStringList QFseventsFileSystemWatcherEngine::removePaths(const QStringList &paths, 426 QStringList *files, 427 QStringList *directories) 428{ 429 QMacAutoReleasePool pool; 430 431 QMutexLocker locker(&lock); 432 433 bool needsRestart = false; 434 435 WatchingState oldState = watchingState; 436 QStringList unhandled; 437 for (const QString &origPath : paths) { 438 auto sg = qScopeGuard([&]{ unhandled.push_back(origPath); }); 439 QString realPath = origPath; 440 if (realPath.endsWith(QDir::separator())) 441 realPath = realPath.mid(0, realPath.size() - 1); 442 443 QFileInfo fi(realPath); 444 realPath = fi.canonicalFilePath(); 445 446 if (fi.isDir()) { 447 DirsByName::iterator dirIt = watchingState.watchedDirectories.find(realPath); 448 if (dirIt != watchingState.watchedDirectories.end()) { 449 needsRestart |= derefPath(dirIt->dirInfo.watchedPath); 450 watchingState.watchedDirectories.erase(dirIt); 451 directories->removeAll(origPath); 452 sg.dismiss(); 453 DEBUG("Removed directory '%s'", qPrintable(realPath)); 454 } 455 } else { 456 QFileInfo fi(realPath); 457 QString parentPath = fi.path(); 458 FilesByPath::iterator pIt = watchingState.watchedFiles.find(parentPath); 459 if (pIt != watchingState.watchedFiles.end()) { 460 InfoByName &filesInDir = pIt.value(); 461 InfoByName::iterator fIt = filesInDir.find(realPath); 462 if (fIt != filesInDir.end()) { 463 needsRestart |= derefPath(fIt->watchedPath); 464 filesInDir.erase(fIt); 465 if (filesInDir.isEmpty()) 466 watchingState.watchedFiles.erase(pIt); 467 files->removeAll(origPath); 468 sg.dismiss(); 469 DEBUG("Removed file '%s'", qPrintable(realPath)); 470 } 471 } 472 } 473 } 474 475 locker.unlock(); 476 477 if (needsRestart) { 478 if (!restartStream()) { 479 watchingState = std::move(oldState); 480 startStream(); 481 } 482 } 483 484 return unhandled; 485} 486 487// Returns false if FSEventStream* calls failed for some mysterious reason, true if things got a 488// thumbs-up. 489bool QFseventsFileSystemWatcherEngine::startStream() 490{ 491 Q_ASSERT(stream == 0); 492 if (stream) // Ok, this really shouldn't happen, esp. not after the assert. But let's be nice in release mode and still handle it. 493 stopStream(); 494 495 QMacAutoReleasePool pool; 496 497 if (watchingState.watchedPaths.isEmpty()) 498 return true; // we succeeded in doing nothing 499 500 DEBUG() << "Starting stream with paths" << watchingState.watchedPaths.keys(); 501 502 NSMutableArray<NSString *> *pathsToWatch = [NSMutableArray<NSString *> arrayWithCapacity:watchingState.watchedPaths.size()]; 503 for (PathRefCounts::const_iterator i = watchingState.watchedPaths.begin(), ei = watchingState.watchedPaths.end(); i != ei; ++i) 504 [pathsToWatch addObject:i.key().toNSString()]; 505 506 struct FSEventStreamContext callBackInfo = { 507 0, 508 this, 509 NULL, 510 NULL, 511 NULL 512 }; 513 const CFAbsoluteTime latency = .5; // in seconds 514 515 // Never start with kFSEventStreamEventIdSinceNow, because this will generate an invalid 516 // soft-assert in FSEventStreamFlushSync in CarbonCore when no event occurred. 517 if (lastReceivedEvent == kFSEventStreamEventIdSinceNow) 518 lastReceivedEvent = FSEventsGetCurrentEventId(); 519 stream = FSEventStreamCreate(NULL, 520 &callBackFunction, 521 &callBackInfo, 522 reinterpret_cast<CFArrayRef>(pathsToWatch), 523 lastReceivedEvent, 524 latency, 525 FSEventStreamCreateFlags(0)); 526 527 if (!stream) { // nope, no way to know what went wrong, so just fail 528 DEBUG() << "Failed to create stream!"; 529 return false; 530 } 531 532 FSEventStreamSetDispatchQueue(stream, queue); 533 534 if (FSEventStreamStart(stream)) { 535 DEBUG() << "Stream started successfully with sinceWhen =" << lastReceivedEvent; 536 return true; 537 } else { // again, no way to know what went wrong, so just clean up and fail 538 DEBUG() << "Stream failed to start!"; 539 FSEventStreamInvalidate(stream); 540 FSEventStreamRelease(stream); 541 stream = 0; 542 return false; 543 } 544} 545 546void QFseventsFileSystemWatcherEngine::stopStream(bool isStopped) 547{ 548 QMacAutoReleasePool pool; 549 if (stream) { 550 if (!isStopped) 551 FSEventStreamStop(stream); 552 FSEventStreamInvalidate(stream); 553 FSEventStreamRelease(stream); 554 stream = 0; 555 DEBUG() << "Stream stopped. Last event ID:" << lastReceivedEvent; 556 } 557} 558 559QFseventsFileSystemWatcherEngine::InfoByName QFseventsFileSystemWatcherEngine::scanForDirEntries(const QString &path) 560{ 561 InfoByName entries; 562 563 QDirIterator it(path); 564 while (it.hasNext()) { 565 it.next(); 566 QString entryName = it.filePath(); 567 QT_STATBUF st; 568 if (QT_STAT(QFile::encodeName(entryName), &st) == -1) 569 continue; 570 entries.insert(entryName, Info(QString(), st.st_ctimespec, st.st_mode, QString())); 571 } 572 573 return entries; 574} 575 576bool QFseventsFileSystemWatcherEngine::derefPath(const QString &watchedPath) 577{ 578 PathRefCounts::iterator it = watchingState.watchedPaths.find(watchedPath); 579 if (it != watchingState.watchedPaths.end() && --it.value() < 1) { 580 watchingState.watchedPaths.erase(it); 581 DEBUG("Removing '%s' from watchedPaths.", qPrintable(watchedPath)); 582 return true; 583 } 584 585 return false; 586} 587 588QT_END_NAMESPACE 589