]> git.lyx.org Git - features.git/commitdiff
Implement FileMonitor as a wrapper for QFileSystemWatcher
authorGuillaume Munch <gm@lyx.org>
Mon, 27 Feb 2017 22:46:10 +0000 (23:46 +0100)
committerGuillaume Munch <gm@lyx.org>
Fri, 10 Mar 2017 23:50:57 +0000 (00:50 +0100)
The new file monitor supports both boost and qt signals. It is implemented in a
ressource-safe way.

src/support/FileMonitor.h
src/support/FileMonitor2.cpp [new file with mode: 0644]
src/support/FileMonitor2.h [new file with mode: 0644]
src/support/Makefile.am

index d0d3741d4a7262a025f29dab1217ad847f1550c1..efc8102ed578716c8d7f0a66114aa989e6ec8b7b 100644 (file)
@@ -15,6 +15,9 @@
 #ifndef FILEMONITOR_H
 #define FILEMONITOR_H
 
+// TODO: Remove FileMonitor
+#include "support/FileMonitor2.h"
+
 #include <boost/signals2.hpp>
 
 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 (file)
index 0000000..2cf8cb9
--- /dev/null
@@ -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 <config.h>
+
+#include "support/FileMonitor2.h"
+
+#include "support/debug.h"
+#include "support/FileName.h"
+#include "support/qstring_helpers.h"
+#include "support/unique_ptr.h"
+
+#include <QFile>
+#include <QSignalBlocker>
+#include <QTimer>
+
+#include <algorithm>
+
+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<QFileSystemWatcher>())
+{}
+
+
+//static
+FileMonitorPtr FileSystemWatcher::monitor(FileName const & file_with_path)
+{
+       FileSystemWatcher & f = instance();
+       string const filename = file_with_path.absFileName();
+       weak_ptr<FileMonitorGuard> & wptr = f.store_[filename];
+       if (shared_ptr<FileMonitorGuard> mon = wptr.lock())
+               return make_unique<FileMonitor2>(mon);
+       auto mon = make_shared<FileMonitorGuard>(filename, f.qwatcher_.get());
+       wptr = mon;
+       return make_unique<FileMonitor2>(mon);
+}
+
+
+//static
+void FileSystemWatcher::debug()
+{
+       FileSystemWatcher & f = instance();
+       QStringList q_files = f.qwatcher_->files();
+       for (pair<string, weak_ptr<FileMonitorGuard>> 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<FileMonitorGuard> & 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
+               // <https://bugreports.qt.io/browse/QTBUG-46483> (not a bug).
+               refresh();
+       }
+}
+
+
+FileMonitor2::FileMonitor2(std::shared_ptr<FileMonitorGuard> 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<FileMonitorBlockerGuard>(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 (file)
index 0000000..eb59fa5
--- /dev/null
@@ -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 <memory>
+
+#include <QFileSystemWatcher>
+#include <QObject>
+
+#include <boost/signals2.hpp>
+
+
+namespace lyx {
+namespace support {
+
+class FileName;
+
+///
+///  FileMonitor2, a file monitor based on QFileSystemWatcher
+///
+
+class FileMonitor2;
+class FileMonitorGuard;
+using FileMonitorPtr = std::unique_ptr<FileMonitor2>;
+
+///
+/// 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<std::string, std::weak_ptr<FileMonitorGuard>> store_;
+       // This class is a wrapper for QFileSystemWatcher
+       std::unique_ptr<QFileSystemWatcher> 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
+       /// <https://bugreports.qt.io/browse/QTBUG-46483>. 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<FileMonitorBlockerGuard>;
+
+
+/// Main class
+class FileMonitor2 : public QObject
+{
+       Q_OBJECT
+       friend class FileMonitorBlockerGuard;
+
+public:
+       FileMonitor2(std::shared_ptr<FileMonitorGuard> monitor);
+
+       using sig = boost::signals2::signal<void()>;
+       /// 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 <https://bugreports.qt.io/browse/QTBUG-46483>. 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<FileMonitorGuard> const monitor_;
+       //
+       std::weak_ptr<FileMonitorBlockerGuard> blocker_;
+};
+
+
+
+} // namespace support
+} // namespace lyx
+
+#endif // FILEMONITOR2_H
index f3a8823ac6e6956dc13bec3f0f8330938bc7013e..e816c6fda911d541d5c89b110b068b0903fd437a 100644 (file)
@@ -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 \