view 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
line wrap: on
line source
/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */

/*
    EasyMercurial

    Based on hgExplorer by Jari Korhonen
    Copyright (c) 2010 Jari Korhonen
    Copyright (c) 2012 Chris Cannam
    Copyright (c) 2012 Queen Mary, University of London
    
    This program is free software; you can redistribute it and/or
    modify it under the terms of the GNU General Public License as
    published by the Free Software Foundation; either version 2 of the
    License, or (at your option) any later version.  See the file
    COPYING included with this distribution for more information.
*/

#include <QMutexLocker>
#include <QDir>

#ifdef Q_OS_MAC
// Must include this before debug.h
#include <CoreServices/CoreServices.h>
#endif

#include "fswatcher.h"
#include "debug.h"

#include <deque>

//#define DEBUG_FSWATCHER 1

/*
 * Watching the filesystem is trickier than it seems at first glance.
 *
 * We ideally should watch every directory, and every file that is
 * tracked by Hg. If a new file is created in a directory, then we
 * need to respond in order to show it as a potential candidate to be
 * added.
 *
 * Complicating matters though is that Hg itself might modify the
 * filesystem. This can happen even in "read-only" operations: for
 * example, hg stat creates files called hg-checklink and hg-checkexec
 * to test properties of the filesystem. So we need to know to ignore
 * those files; unfortunately, when watching a directory (which is how
 * we find out about the creation of new files) we are notified only
 * that the directory has changed -- we aren't told what changed.
 *
 * This means that, when a directory changes, we need to rescan the
 * directory to learn whether the set of files in it _excluding_ files
 * matching our ignore patterns differs from the previous scan, and
 * ignore the change if it doesn't.
 *
 */

/*
 * 20120312 -- Another complication. The documentation for
 * QFileSystemWatcher says:
 *
 *     On Mac OS X 10.4 [...] an open file descriptor is required for
 *     each monitored file. [...] This means that addPath() and
 *     addPaths() will fail if your process tries to add more than 256
 *     files or directories to the file system monitor [...] Mac OS X
 *     10.5 and up use a different backend and do not suffer from this
 *     issue.
 * 
 * Unfortunately, the last sentence above is not true:
 * http://qt.gitorious.org/qt/qt/commit/6d1baf9979346d6f15da81a535becb4046278962
 * ("Removing the usage of FSEvents-based backend for now as it has a
 * few bugs...").  It can't be restored without hacking the Qt source,
 * which we don't want to do in this context. The commit log doesn't
 * make clear how serious the bugs were -- an example is given but it
 * doesn't indicate whether it's an edge case or a common case and
 * whether the result was a crash or failure to notify.
 *
 * This means the Qt class uses kqueue instead on OS/X, but that
 * doesn't really work for us -- it can only monitor 256 files (or
 * whatever the fd ulimit is set to, but that's the default) and it
 * doesn't notify if a file within a directory is modified unless the
 * metadata changes. The main limitation of FSEvents is that it only
 * notifies with directory granularity, but that might be OK for us so
 * long as notifications are actually provoked by file changes as
 * well. (In OS/X 10.7 there appear to be file-level notifications
 * too, but that doesn't help us.)
 *
 * One other problem with FSEvents is that the API only exists on OS/X
 * 10.5 or newer -- on older versions we would have no option but to
 * use kqueue via QFileSystemWatcher. But we can't ship a binary
 * linked with the FSEvents API to run on 10.4 without some fiddling,
 * and I'm not really keen to do that either.  That may be our cue to
 * drop 10.4 support for EasyMercurial.
 */

FsWatcher::FsWatcher() :
    m_lastToken(0),
    m_lastCounter(0)
{
#ifdef Q_OS_MAC
    m_stream = 0; // create when we have a path
#else
    connect(&m_watcher, SIGNAL(directoryChanged(QString)),
	    this, SLOT(fsDirectoryChanged(QString)));
    connect(&m_watcher, SIGNAL(fileChanged(QString)),
	    this, SLOT(fsFileChanged(QString)));
#endif
}

FsWatcher::~FsWatcher()
{
}

void
FsWatcher::setWorkDirPath(QString path)
{
    QMutexLocker locker(&m_mutex);
    if (m_workDirPath == path) return;
    clearWatchedPaths();
    m_workDirPath = path;
    addWorkDirectory(path);
    debugPrint();
}

void
FsWatcher::clearWatchedPaths()
{
#ifdef Q_OS_MAC
    FSEventStreamRef stream = (FSEventStreamRef)m_stream;
    if (stream) {
        FSEventStreamStop(stream);
        FSEventStreamInvalidate(stream);
        FSEventStreamRelease(stream);
    }
    m_stream = 0;
#else
    // annoyingly, removePaths prints a warning if given an empty list
    if (!m_watcher.directories().empty()) {
        m_watcher.removePaths(m_watcher.directories());
    }
    if (!m_watcher.files().empty()) {
        m_watcher.removePaths(m_watcher.files());
    }
#endif
}

#ifdef Q_OS_MAC
static void
fsEventsCallback(ConstFSEventStreamRef streamRef,
                 void *clientCallBackInfo,
                 size_t numEvents,
                 void *paths,
                 const FSEventStreamEventFlags eventFlags[],
                 const FSEventStreamEventId eventIDs[])
{
    FsWatcher *watcher = reinterpret_cast<FsWatcher *>(clientCallBackInfo);
    const char *const *cpaths = reinterpret_cast<const char *const *>(paths);
    for (size_t i = 0; i < numEvents; ++i) {
        std::cerr << "path " << i << " = " << cpaths[i] << std::endl;
        watcher->fsDirectoryChanged(QString::fromLocal8Bit(cpaths[i]));
    }
}
#endif

void
FsWatcher::addWorkDirectory(QString path)
{
#ifdef Q_OS_MAC

    CFStringRef cfPath = CFStringCreateWithCharacters
        (0, reinterpret_cast<const UniChar *>(path.unicode()),
         path.length());

    CFArrayRef cfPaths = CFArrayCreate(0, (const void **)&cfPath, 1, 0);

    FSEventStreamContext ctx = { 0, 0, 0, 0, 0 };
    ctx.info = this;

    FSEventStreamRef stream =
        FSEventStreamCreate(kCFAllocatorDefault,
                            &fsEventsCallback,
                            &ctx,
                            cfPaths,
                            kFSEventStreamEventIdSinceNow,
                            1.0, // latency, seconds
                            kFSEventStreamCreateFlagNone);

    m_stream = stream;
    
    FSEventStreamScheduleWithRunLoop(stream,
                                     CFRunLoopGetCurrent(),
                                     kCFRunLoopDefaultMode);

    if (!FSEventStreamStart(stream)) {
        std::cerr << "ERROR: FsWatcher::addWorkDirectory: Failed to start FSEvent stream" << std::endl;
    }
#else
    // QFileSystemWatcher will refuse to add a file or directory to
    // its watch list that it is already watching -- fine -- but it
    // prints a warning when this happens, which we wouldn't want.  So
    // we'll check for duplicates ourselves.
    QSet<QString> alreadyWatched = 
	QSet<QString>::fromList(m_watcher.directories());
    
    std::deque<QString> pending;
    pending.push_back(path);

    while (!pending.empty()) {

        QString path = pending.front();
        pending.pop_front();
        if (!alreadyWatched.contains(path)) {
            m_watcher.addPath(path);
            m_dirContents[path] = scanDirectory(path);
        }

        QDir d(path);
        if (d.exists()) {
            d.setFilter(QDir::Dirs | QDir::NoDotAndDotDot |
                        QDir::Readable | QDir::NoSymLinks);
            foreach (QString entry, d.entryList()) {
                if (entry.startsWith('.')) continue;
                QString entryPath = d.absoluteFilePath(entry);
                pending.push_back(entryPath);
            }
        }
    }
#endif
}

void
FsWatcher::setTrackedFilePaths(QStringList paths)
{
#ifdef Q_OS_MAC

    // FSEvents will notify when any file in the directory changes,
    // but we need to be able to check whether the file change was
    // meaningful to us if it didn't result in any files being added
    // or removed -- and we have to do that by examining timestamps on
    // the files we care about
    foreach (QString p, paths) {
        m_trackedFileUpdates[p] = QDateTime::currentDateTime();
    }

#else

    QMutexLocker locker(&m_mutex);

    QSet<QString> alreadyWatched = 
	QSet<QString>::fromList(m_watcher.files());

    foreach (QString path, paths) {
        path = m_workDirPath + QDir::separator() + path;
        if (!alreadyWatched.contains(path)) {
            m_watcher.addPath(path);
        } else {
            alreadyWatched.remove(path);
        }
    }

    // Remove the remaining paths, those that were being watched
    // before but that are not in the list we were given
    foreach (QString path, alreadyWatched) {
        m_watcher.removePath(path);
    }

    debugPrint();

#endif
}

void
FsWatcher::setIgnoredFilePrefixes(QStringList prefixes)
{
    QMutexLocker locker(&m_mutex);
    m_ignoredPrefixes = prefixes;
}

void
FsWatcher::setIgnoredFileSuffixes(QStringList suffixes)
{
    QMutexLocker locker(&m_mutex);
    m_ignoredSuffixes = suffixes;
}

int
FsWatcher::getNewToken()
{
    QMutexLocker locker(&m_mutex);
    int token = ++m_lastToken;
    m_tokenMap[token] = m_lastCounter;
    return token;
}

QSet<QString>
FsWatcher::getChangedPaths(int token)
{
    QMutexLocker locker(&m_mutex);
    size_t lastUpdatedAt = m_tokenMap[token];
    QSet<QString> changed;
    for (QHash<QString, size_t>::const_iterator i = m_changes.begin();
         i != m_changes.end(); ++i) {
        if (i.value() > lastUpdatedAt) {
             changed.insert(i.key());
        }
    }
    m_tokenMap[token] = m_lastCounter;
    return changed;
}

void
FsWatcher::fsDirectoryChanged(QString path)
{
    bool haveChanges = false;

    {
	QMutexLocker locker(&m_mutex);

	if (shouldIgnore(path)) return;

        QSet<QString> files = scanDirectory(path);

        if (files == m_dirContents[path]) {

#ifdef DEBUG_FSWATCHER
            std::cerr << "FsWatcher: Directory " << path << " has changed, but not in a way that we are monitoring -- doing manual check" << std::endl;
#endif

#ifdef Q_OS_MAC
            haveChanges = manuallyCheckTrackedFiles();
#endif

        } else {

#ifdef DEBUG_FSWATCHER
            std::cerr << "FsWatcher: Directory " << path << " has changed" << std::endl;
#endif
            m_dirContents[path] = files;
            size_t counter = ++m_lastCounter;
            m_changes[path] = counter;
            haveChanges = true;
        }
    }

    if (haveChanges) {
        emit changed();
    }
}

void
FsWatcher::fsFileChanged(QString path)
{
    {
        QMutexLocker locker(&m_mutex);

        // We don't check whether the file matches an ignore pattern,
        // because we are only notified for file changes if we are
        // watching the file explicitly, i.e. the file is in the
        // tracked file paths list. So we never want to ignore these

#ifdef DEBUG_FSWATCHER
        std::cerr << "FsWatcher: Tracked file " << path << " has changed" << std::endl;
#endif

        size_t counter = ++m_lastCounter;
        m_changes[path] = counter;
    }

    emit changed();
}

#ifdef Q_OS_MAC
bool
FsWatcher::manuallyCheckTrackedFiles()
{
    std::cerr << "FsWatcher::manuallyCheckTrackedFiles" << std::endl;
    bool foundChanges = false;

    for (PathTimeMap::iterator i = m_trackedFileUpdates.begin();
         i != m_trackedFileUpdates.end(); ++i) {

        QString path = i.key();
        QDateTime prevUpdate = i.value();

        QFileInfo fi(m_workDirPath + QDir::separator() + path);
        QDateTime currUpdate = fi.lastModified();

//        std::cerr << "FsWatcher: Tracked file " << path << " previously changed at "
//                  << prevUpdate.toString().toStdString()
//                  << ", currently at " << currUpdate.toString().toStdString() << std::endl;

        if (currUpdate > prevUpdate) {

#ifdef DEBUG_FSWATCHER
            std::cerr << "FsWatcher: Tracked file " << path << " has been changed since last check" << std::endl;
#endif
            i.value() = currUpdate;
            
            size_t counter = ++m_lastCounter;
            m_changes[path] = counter;
            foundChanges = true;
        }
    }
    
    return foundChanges;
}
#endif

bool
FsWatcher::shouldIgnore(QString path)
{
    QFileInfo fi(path);
    QString fn(fi.fileName());
    foreach (QString pfx, m_ignoredPrefixes) {
        if (fn.startsWith(pfx)) {
#ifdef DEBUG_FSWATCHER
            std::cerr << "(ignoring: " << path << ")" << std::endl;
#endif
            return true;
        }
    }
    foreach (QString sfx, m_ignoredSuffixes) {
        if (fn.endsWith(sfx)) {
#ifdef DEBUG_FSWATCHER
            std::cerr << "(ignoring: " << path << ")" << std::endl;
#endif
            return true;
        }
    }
    return false;
}

QSet<QString>
FsWatcher::scanDirectory(QString path)
{
    QSet<QString> files;
    QDir d(path);
    if (d.exists()) {
        d.setFilter(QDir::Files | QDir::NoDotAndDotDot |
                    QDir::Readable | QDir::NoSymLinks);
        foreach (QString entry, d.entryList()) {
            if (entry.startsWith('.')) continue;
            if (shouldIgnore(entry)) continue;
            files.insert(entry);
        }
    }
    return files;
}

void
FsWatcher::debugPrint()
{
#ifdef DEBUG_FSWATCHER
#ifndef Q_OS_MAC
    std::cerr << "FsWatcher: Now watching " << m_watcher.directories().size()
              << " directories and " << m_watcher.files().size()
              << " files" << std::endl;
#endif
#endif
}