From: Guillaume Munch Date: Mon, 27 Feb 2017 22:46:10 +0000 (+0100) Subject: Implement FileMonitor as a wrapper for QFileSystemWatcher X-Git-Tag: 2.3.0alpha1~243 X-Git-Url: https://git.lyx.org/gitweb/?a=commitdiff_plain;h=caa54e80ee410c0fa3cc9a1eea6588d68a92386b;p=features.git Implement FileMonitor as a wrapper for QFileSystemWatcher The new file monitor supports both boost and qt signals. It is implemented in a ressource-safe way. --- diff --git a/src/support/FileMonitor.h b/src/support/FileMonitor.h index d0d3741d4a..efc8102ed5 100644 --- a/src/support/FileMonitor.h +++ b/src/support/FileMonitor.h @@ -15,6 +15,9 @@ #ifndef FILEMONITOR_H #define FILEMONITOR_H +// TODO: Remove FileMonitor +#include "support/FileMonitor2.h" + #include namespace lyx { @@ -28,7 +31,8 @@ public: /** Once monitoring begins, the file will be monitored every * interval ms. * - * FIXME: rewrite and simplify using an encapsulation of QFileSystemWatcher. + * This is now obsoleted by FileMonitor2 based on QFileSystemWatcher. + * FIXME: Remove FileMonitor */ FileMonitor(FileName const & file_with_path, int interval); diff --git a/src/support/FileMonitor2.cpp b/src/support/FileMonitor2.cpp new file mode 100644 index 0000000000..2cf8cb917a --- /dev/null +++ b/src/support/FileMonitor2.cpp @@ -0,0 +1,211 @@ +/** + * \file FileMonitor.cpp + * This file is part of LyX, the document processor. + * Licence details can be found in the file COPYING. + * + * \author Guillaume Munch + * + * Full author contact details are available in file CREDITS. + */ + +#include + +#include "support/FileMonitor2.h" + +#include "support/debug.h" +#include "support/FileName.h" +#include "support/qstring_helpers.h" +#include "support/unique_ptr.h" + +#include +#include +#include + +#include + +using namespace std; + +namespace lyx { +namespace support { + + +FileSystemWatcher & FileSystemWatcher::instance() +{ + // This thread-safe because QFileSystemWatcher is thread-safe. + static FileSystemWatcher f; + return f; +} + + +FileSystemWatcher::FileSystemWatcher() + : qwatcher_(make_unique()) +{} + + +//static +FileMonitorPtr FileSystemWatcher::monitor(FileName const & file_with_path) +{ + FileSystemWatcher & f = instance(); + string const filename = file_with_path.absFileName(); + weak_ptr & wptr = f.store_[filename]; + if (shared_ptr mon = wptr.lock()) + return make_unique(mon); + auto mon = make_shared(filename, f.qwatcher_.get()); + wptr = mon; + return make_unique(mon); +} + + +//static +void FileSystemWatcher::debug() +{ + FileSystemWatcher & f = instance(); + QStringList q_files = f.qwatcher_->files(); + for (pair> pair : f.store_) { + string const & name = pair.first; + if (!pair.second.expired()) { + if (!q_files.contains(toqstr(name))) + LYXERR0("Monitored but not QFileSystemWatched (bad): " << name); + else { + //LYXERR0("Monitored and QFileSystemWatched (good): " << name); + } + } + } + for (QString const & qname : q_files) { + string const name = fromqstr(qname); + weak_ptr & wptr = f.store_[name]; + if (wptr.expired()) + LYXERR0("QFileSystemWatched but not monitored (bad): " << name); + } +} + + +FileMonitorGuard::FileMonitorGuard(string const & filename, + QFileSystemWatcher * qwatcher) + : filename_(filename), qwatcher_(qwatcher) +{ + QObject::connect(qwatcher, SIGNAL(fileChanged(QString const &)), + this, SLOT(notifyChange(QString const &))); + if (qwatcher_->files().contains(toqstr(filename))) + LYXERR0("This file is already being QFileSystemWatched: " << filename + << ". This should not happen."); + refresh(); +} + + +FileMonitorGuard::~FileMonitorGuard() +{ + qwatcher_->removePath(toqstr(filename_)); +} + + +void FileMonitorGuard::refresh(bool new_file) +{ + QString const qfilename = toqstr(filename_); + if(!qwatcher_->files().contains(qfilename)) { + bool exists = QFile(qfilename).exists(); + if (!exists || !qwatcher_->addPath(qfilename)) { + if (exists) + LYXERR(Debug::FILES, + "Could not add path to QFileSystemWatcher: " + << filename_); + QTimer::singleShot(1000, this, [=](){ + refresh(new_file || !exists); + }); + } else if (exists && new_file) + Q_EMIT fileChanged(); + } +} + + +void FileMonitorGuard::notifyChange(QString const & path) +{ + if (path == toqstr(filename_)) { + Q_EMIT fileChanged(); + // If the file has been modified by delete-move, we are notified of the + // deletion but we no longer track the file. See + // (not a bug). + refresh(); + } +} + + +FileMonitor2::FileMonitor2(std::shared_ptr monitor) + : monitor_(monitor) +{ + connectToFileMonitorGuard(); + refresh(); +} + + +void FileMonitor2::connectToFileMonitorGuard() +{ + QObject::connect(monitor_.get(), SIGNAL(fileChanged()), + this, SLOT(changed())); +} + + +boost::signals2::connection +FileMonitor2::connect(sig::slot_type const & slot) +{ + return fileChanged_.connect(slot); +} + + +void FileMonitor2::disconnect() +{ + fileChanged_.disconnect_all_slots(); + QObject::disconnect(this, SIGNAL(fileChanged())); +} + + +void FileMonitor2::changed() +{ + // emit boost signal + fileChanged_(); + Q_EMIT fileChanged(); +} + + +FileMonitorBlocker FileMonitor2::block(int delay) +{ + FileMonitorBlocker blocker = blocker_.lock(); + if (!blocker) + blocker_ = blocker = make_shared(this); + blocker->setDelay(delay); + return blocker; +} + + +FileMonitorBlockerGuard::FileMonitorBlockerGuard(FileMonitor2 * parent) + : QObject(parent), parent_(parent), delay_(0) +{ + QObject::disconnect(parent_->monitor_.get(), SIGNAL(fileChanged()), + parent_, SLOT(changed())); +} + + +void FileMonitorBlockerGuard::setDelay(int delay) +{ + delay_ = max(delay_, delay); +} + + +FileMonitorBlockerGuard::~FileMonitorBlockerGuard() +{ + // closures can only copy local copies + FileMonitor2 * parent = parent_; + // parent is also our QObject::parent() so we are deleted before parent. + // Even if delay_ is 0, the QTimer is necessary. Indeed, the notifications + // from QFileSystemWatcher that we meant to ignore are not going to be + // treated immediately, so we must yield to give us the opportunity to + // ignore them. + QTimer::singleShot(delay_, parent, [parent]() { + parent->connectToFileMonitorGuard(); + }); +} + +} // namespace support +} // namespace lyx + +#include "moc_FileMonitor2.cpp" diff --git a/src/support/FileMonitor2.h b/src/support/FileMonitor2.h new file mode 100644 index 0000000000..eb59fa512f --- /dev/null +++ b/src/support/FileMonitor2.h @@ -0,0 +1,192 @@ +// -*- C++ -*- +/** + * \file FileMonitor2.h + * This file is part of LyX, the document processor. + * Licence details can be found in the file COPYING. + * + * \author Guillaume Munch + * + * Full author contact details are available in file CREDITS. + * + * FileMonitor monitors a file and informs a listener when that file has + * changed. + */ + +#ifndef FILEMONITOR2_H +#define FILEMONITOR2_H + +#include + +#include +#include + +#include + + +namespace lyx { +namespace support { + +class FileName; + +/// +/// FileMonitor2, a file monitor based on QFileSystemWatcher +/// + +class FileMonitor2; +class FileMonitorGuard; +using FileMonitorPtr = std::unique_ptr; + +/// +/// Watch a file: +/// FileMonitorPtr monitor = FileSystemWatcher::monitor(file_with_path); +/// monitor.connect(...); //(using boost::signals2), or: +/// connect(monitor, SIGNAL(fileChanged()),...); // (using Qt) +/// +/// Remember that a unique_ptr is automatically deleted at the end of a scope if +/// it has not been moved, or when assigned. When that happens, the signal +/// object is deleted and therefore all the connections are closed. The file +/// ceases being tracked when all the monitors for a file have been deleted. +/// +/// Stop watching: +/// * as determined statically by the scope, or +/// * dynamically, using: +/// monitor = nullptr; +/// +/// Watch a different file: +/// monitor = FileSystemWatcher::monitor(file_with_path2); +/// monitor.connect(...); +/// (stops watching the first) +/// +/// Block notifications for the duration of a scope: +/// { +/// FileMonitorBlocker block = monitor.block(); +/// ... +/// } +/// +/// Reset connections: +/// monitor.disconnect(); +/// or the disconnect method of the connection object for the boost signal. +/// +class FileSystemWatcher +{ +public: + // as described above + static FileMonitorPtr monitor(FileName const & file_with_path); + // Output whether the paths tracked by qwatcher_ and the active + // FileMonitorGuards are in correspondence. + static void debug(); +private: + FileSystemWatcher(); + // A global instance is created automatically on first call to monitor + static FileSystemWatcher & instance(); + // Caches the monitor guards but allow them to be destroyed + std::map> store_; + // This class is a wrapper for QFileSystemWatcher + std::unique_ptr const qwatcher_; +}; + + +// Must be unique per path +// Ends the watch when deleted +class FileMonitorGuard : public QObject +{ + Q_OBJECT + +public: + /// Start the watch + FileMonitorGuard(std::string const & filename, + QFileSystemWatcher * qwatcher); + /// End the watch + ~FileMonitorGuard(); + /// absolute path being tracked + std::string const & filename() { return filename_; } + /// Make sure it is being monitored, after e.g. a deletion. See + /// . This is called + /// automatically. + /// \param new_file If true, emit fileChanged if the file exists and was + /// successfully added. + void refresh(bool new_file = false); + +Q_SIGNALS: + /// Connect to this to be notified when the file changes + void fileChanged() const; + +private Q_SLOTS: + /// Receive notifications from the QFileSystemWatcher + void notifyChange(QString const & path); + +private: + std::string const filename_; + QFileSystemWatcher * qwatcher_; +}; + + +class FileMonitorBlockerGuard : public QObject +{ + Q_OBJECT + FileMonitor2 * parent_; + int delay_; + +public: + FileMonitorBlockerGuard(FileMonitor2 * parent); + ~FileMonitorBlockerGuard(); + void setDelay(int delay); +}; + + +using FileMonitorBlocker = std::shared_ptr; + + +/// Main class +class FileMonitor2 : public QObject +{ + Q_OBJECT + friend class FileMonitorBlockerGuard; + +public: + FileMonitor2(std::shared_ptr monitor); + + using sig = boost::signals2::signal; + /// Connect and you'll be informed when the file has changed. + boost::signals2::connection connect(sig::slot_type const &); + /// disconnect all slots connected to the boost signal fileChanged_ or to + /// the qt signal fileChanged() + void disconnect(); + /// absolute path being tracked + std::string const & filename() { return monitor_->filename(); } + /// Creates a guard that blocks notifications. Copyable. Notifications from + /// this monitor are blocked as long as there are copies around. + /// \param delay is the amount waited in ms after expiration of the guard + /// before reconnecting. This delay thing is to deal with asynchronous + /// notifications in a not so elegant fashion. But it can also be used to + /// slow down incoming events. + FileMonitorBlocker block(int delay = 0); + /// Make sure the good file is being monitored, after e.g. a move or a + /// deletion. See . This is + /// called automatically. + void refresh() { return monitor_->refresh(); } + +Q_SIGNALS: + /// Connect to this to be notified when the file changes + void fileChanged() const; + +private Q_SLOTS: + /// Receive notifications from the FileMonitorGuard + void changed(); + +private: + void connectToFileMonitorGuard(); + // boost signal + sig fileChanged_; + // the unique watch for our file + std::shared_ptr const monitor_; + // + std::weak_ptr blocker_; +}; + + + +} // namespace support +} // namespace lyx + +#endif // FILEMONITOR2_H diff --git a/src/support/Makefile.am b/src/support/Makefile.am index f3a8823ac6..e816c6fda9 100644 --- a/src/support/Makefile.am +++ b/src/support/Makefile.am @@ -11,6 +11,7 @@ noinst_LIBRARIES = liblyxsupport.a MOCHEADER = \ ConsoleApplicationPrivate.h \ + FileMonitor2.h \ SystemcallPrivate.h MOCEDFILES = $(MOCHEADER:%.h=moc_%.cpp) @@ -33,6 +34,8 @@ AM_CPPFLAGS += -I$(srcdir)/.. \ liblyxsupport_a_SOURCES = \ FileMonitor.h \ FileMonitor.cpp \ + FileMonitor2.h \ + FileMonitor2.cpp \ RandomAccessList.h \ bind.h \ Cache.h \