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.
This commit is contained in:
Guillaume Munch 2017-02-27 23:46:10 +01:00
parent 22edb3df96
commit caa54e80ee
4 changed files with 411 additions and 1 deletions

View 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);

View File

@ -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"

192
src/support/FileMonitor2.h Normal file
View File

@ -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

View 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 \