* Licence details can be found in the file COPYING.
*
* \author Angus Leeming
+ * \author Guillaume Munch
*
* Full author contact details are available in file CREDITS.
*/
#include <config.h>
#include "support/FileMonitor.h"
-#include "support/FileName.h"
-#include "support/lyxlib.h"
-
-// FIXME Interface violation
-#include "frontends/Timeout.h"
-#include <boost/bind.hpp>
-#include <boost/filesystem/operations.hpp>
-#include <boost/signals/trackable.hpp>
+#include "support/debug.h"
+#include "support/FileName.h"
+#include "support/qstring_helpers.h"
+#include "support/unique_ptr.h"
+#include <QFile>
+#include <QStringList>
+#include <QTimer>
-using std::string;
+#include <algorithm>
-namespace fs = boost::filesystem;
+using namespace std;
namespace lyx {
namespace support {
-class FileMonitor::Impl : public boost::signals::trackable {
-public:
- ///
- Impl(FileName const & file_with_path, int interval);
+FileSystemWatcher & FileSystemWatcher::instance()
+{
+ // This is thread-safe because QFileSystemWatcher is thread-safe.
+ static FileSystemWatcher f;
+ return f;
+}
- ///
- void monitorFile();
- ///
- FileName filename_;
+FileSystemWatcher::FileSystemWatcher()
+ : qwatcher_(make_unique<QFileSystemWatcher>())
+{}
- ///
- Timeout timer_;
- /// This signal is emitted if the file is modified (has a new checksum).
- FileMonitor::FileChangedSig fileChanged_;
+shared_ptr<FileMonitorGuard>
+FileSystemWatcher::getGuard(FileName const & filename)
+{
+ string const absfilename = filename.absFileName();
+ weak_ptr<FileMonitorGuard> & wptr = store_[absfilename];
+ if (shared_ptr<FileMonitorGuard> mon = wptr.lock())
+ return mon;
+ auto mon = make_shared<FileMonitorGuard>(absfilename, qwatcher_.get());
+ wptr = mon;
+ return mon;
+}
- /** We use these to ascertain whether a file (once loaded successfully)
- * has changed.
- */
- time_t timestamp_;
- ///
- unsigned long checksum_;
-};
+//static
+FileMonitorPtr FileSystemWatcher::monitor(FileName const & filename)
+{
+ return make_unique<FileMonitor>(instance().getGuard(filename));
+}
-FileMonitor::FileMonitor(FileName const & file_with_path, int interval)
- : pimpl_(new Impl(file_with_path, interval))
-{}
+//static
+ActiveFileMonitorPtr FileSystemWatcher::activeMonitor(FileName const & filename,
+ int interval)
+{
+ return make_unique<ActiveFileMonitor>(instance().getGuard(filename),
+ filename, interval);
+}
-FileMonitor::~FileMonitor()
-{}
+
+//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);
+ }
+}
-void FileMonitor::reset(FileName const & file_with_path) const
+FileMonitorGuard::FileMonitorGuard(string const & filename,
+ QFileSystemWatcher * qwatcher)
+ : filename_(filename), qwatcher_(qwatcher), exists_(true)
{
- if (pimpl_->filename_ == file_with_path)
- return;
+ 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();
+}
+
- bool const monitor = pimpl_->timer_.running();
- if (monitor)
- stop();
+FileMonitorGuard::~FileMonitorGuard()
+{
+ qwatcher_->removePath(toqstr(filename_));
+}
- pimpl_->filename_ = file_with_path;
- if (monitor)
- start();
+void FileMonitorGuard::refresh()
+{
+ QString const qfilename = toqstr(filename_);
+ if(!qwatcher_->files().contains(qfilename)) {
+ bool exists = QFile(qfilename).exists();
+#if (QT_VERSION >= 0x050000)
+ if (!exists || !qwatcher_->addPath(qfilename))
+#else
+ auto add_path = [&]() {
+ qwatcher_->addPath(qfilename);
+ return qwatcher_->files().contains(qfilename);
+ };
+ if (!exists || !add_path())
+#endif
+ {
+ if (exists)
+ LYXERR(Debug::FILES,
+ "Could not add path to QFileSystemWatcher: "
+ << filename_);
+ QTimer::singleShot(2000, this, SLOT(refresh()));
+ } else if (exists && !exists_)
+ Q_EMIT fileChanged();
+ setExists(exists);
+ }
}
-FileName const & FileMonitor::filename() const
+void FileMonitorGuard::notifyChange(QString const & path)
{
- return pimpl_->filename_;
+ 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();
+ }
}
-void FileMonitor::start() const
+FileMonitor::FileMonitor(std::shared_ptr<FileMonitorGuard> monitor)
+ : monitor_(monitor)
{
- if (monitoring())
- return;
+ QObject::connect(monitor_.get(), SIGNAL(fileChanged()),
+ this, SLOT(changed()));
+ refresh();
+}
- if (!fs::exists(pimpl_->filename_.toFilesystemEncoding()))
- return;
- pimpl_->timestamp_ = fs::last_write_time(pimpl_->filename_.toFilesystemEncoding());
- pimpl_->checksum_ = sum(pimpl_->filename_);
+void FileMonitor::reconnectToFileMonitorGuard()
+{
+ monitor_->setExists(true);
+ QObject::connect(monitor_.get(), SIGNAL(fileChanged()),
+ this, SLOT(changed()));
+}
- if (pimpl_->timestamp_ && pimpl_->checksum_) {
- pimpl_->timer_.start();
- } else {
- pimpl_->timestamp_ = 0;
- pimpl_->checksum_ = 0;
- }
+
+boost::signals2::connection
+FileMonitor::connect(sig::slot_type const & slot)
+{
+ return fileChanged_.connect(slot);
}
-void FileMonitor::stop() const
+void FileMonitor::disconnect()
{
- pimpl_->timestamp_ = 0;
- pimpl_->checksum_ = 0;
- pimpl_->timer_.stop();
+ fileChanged_.disconnect_all_slots();
+ QObject::disconnect(this, SIGNAL(fileChanged()));
}
-bool FileMonitor::monitoring() const
+void FileMonitor::changed()
{
- return pimpl_->timer_.running();
+ // emit boost signal
+ fileChanged_();
+ Q_EMIT fileChanged();
}
-unsigned long FileMonitor::checksum() const
+FileMonitorBlocker FileMonitor::block(int delay)
{
- // If we aren't actively monitoring the file, then recompute the
- // checksum explicitly.
- if (!pimpl_->timer_.running() && !pimpl_->filename_.empty())
- return sum(pimpl_->filename_);
+ FileMonitorBlocker blocker = blocker_.lock();
+ if (!blocker)
+ blocker_ = blocker = make_shared<FileMonitorBlockerGuard>(this);
+ blocker->setDelay(delay);
+ return blocker;
+}
+
- return pimpl_->checksum_;
+FileMonitorBlockerGuard::FileMonitorBlockerGuard(FileMonitor * monitor)
+ : monitor_(monitor), delay_(0)
+{
+ QObject::disconnect(monitor->monitor_.get(), SIGNAL(fileChanged()),
+ monitor, SLOT(changed()));
}
-boost::signals::connection FileMonitor::connect(slot_type const & slot) const
+void FileMonitorBlockerGuard::setDelay(int delay)
{
- return pimpl_->fileChanged_.connect(slot);
+ delay_ = max(delay_, delay);
}
-//------------------------------
-// Implementation details follow
-//------------------------------
+FileMonitorBlockerGuard::~FileMonitorBlockerGuard()
+{
+ if (!monitor_)
+ return;
+ // 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_, monitor_, SLOT(reconnectToFileMonitorGuard()));
+}
-FileMonitor::Impl::Impl(FileName const & file_with_path, int interval)
- : filename_(file_with_path),
- timer_(interval, Timeout::ONETIME),
- timestamp_(0),
- checksum_(0)
+ActiveFileMonitor::ActiveFileMonitor(std::shared_ptr<FileMonitorGuard> monitor,
+ FileName const & filename, int interval)
+ : FileMonitor(monitor), filename_(filename), interval_(interval),
+ timestamp_(0), checksum_(0), cooldown_(true)
{
- timer_.timeout.connect(boost::bind(&Impl::monitorFile, this));
+ QObject::connect(this, SIGNAL(fileChanged()), this, SLOT(setCooldown()));
+ QTimer::singleShot(interval_, this, SLOT(clearCooldown()));
+ if (!filename_.exists())
+ return;
+ timestamp_ = filename_.lastModified();
+ checksum_ = filename_.checksum();
}
-void FileMonitor::Impl::monitorFile()
+void ActiveFileMonitor::checkModified()
{
- bool changed = false;
+ if (cooldown_)
+ return;
- if (!fs::exists(filename_.toFilesystemEncoding())) {
+ cooldown_ = true;
+ bool changed = false;
+ if (!filename_.exists()) {
changed = timestamp_ || checksum_;
timestamp_ = 0;
checksum_ = 0;
-
} else {
- time_t const new_timestamp = fs::last_write_time(filename_.toFilesystemEncoding());
+ time_t const new_timestamp = filename_.lastModified();
if (new_timestamp != timestamp_) {
timestamp_ = new_timestamp;
- unsigned long const new_checksum = sum(filename_);
+ unsigned long const new_checksum = filename_.checksum();
if (new_checksum != checksum_) {
checksum_ = new_checksum;
changed = true;
}
}
}
-
- timer_.start();
if (changed)
- fileChanged_();
+ FileMonitor::changed();
+ QTimer::singleShot(interval_, this, SLOT(clearCooldown()));
}
+
+void ActiveFileMonitor::checkModifiedAsync()
+{
+ if (!cooldown_)
+ QTimer::singleShot(0, this, SLOT(checkModified()));
+}
+
+
+
} // namespace support
} // namespace lyx
+
+#include "moc_FileMonitor.cpp"