* 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 "support/FileMonitor.h"
+#include "support/debug.h"
#include "support/FileName.h"
-#include "support/lyxlib.h"
-#include "support/Timeout.h"
+#include "support/qstring_helpers.h"
+#include "support/unique_ptr.h"
-#include <boost/bind.hpp>
-#include <boost/signals/trackable.hpp>
+#include <QFile>
+#include <QStringList>
+#include <QTimer>
+#include <algorithm>
-using std::string;
+using namespace std;
namespace lyx {
namespace support {
-class FileMonitor::Impl : public boost::signals::trackable {
-public:
- ///
- Impl(FileName const & file_with_path, int interval);
-
- ///
- void monitorFile();
-
- ///
- FileName filename_;
-
- ///
- Timeout timer_;
-
- /// This signal is emitted if the file is modified (has a new checksum).
- FileMonitor::FileChangedSig fileChanged_;
-
- /** We use these to ascertain whether a file (once loaded successfully)
- * has changed.
- */
- time_t timestamp_;
- ///
- unsigned long checksum_;
-};
+FileSystemWatcher & FileSystemWatcher::instance()
+{
+ // This is thread-safe because QFileSystemWatcher is thread-safe.
+ static FileSystemWatcher f;
+ return f;
+}
-FileMonitor::FileMonitor(FileName const & file_with_path, int interval)
- : pimpl_(new Impl(file_with_path, interval))
+FileSystemWatcher::FileSystemWatcher()
+ : qwatcher_(make_unique<QFileSystemWatcher>())
{}
-FileMonitor::~FileMonitor()
-{}
+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;
+}
-void FileMonitor::reset(FileName const & file_with_path) const
+//static
+FileMonitorPtr FileSystemWatcher::monitor(FileName const & filename)
{
- if (pimpl_->filename_ == file_with_path)
- return;
-
- bool const monitor = pimpl_->timer_.running();
- if (monitor)
- stop();
+ return lyx::make_unique<FileMonitor>(instance().getGuard(filename));
+}
- pimpl_->filename_ = file_with_path;
- if (monitor)
- start();
+//static
+ActiveFileMonitorPtr FileSystemWatcher::activeMonitor(FileName const & filename,
+ int interval)
+{
+ return lyx::make_unique<ActiveFileMonitor>(instance().getGuard(filename),
+ filename, interval);
}
-FileName const & FileMonitor::filename() const
+//static
+void FileSystemWatcher::debug()
{
- return pimpl_->filename_;
+ 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::start() const
+FileMonitorGuard::FileMonitorGuard(string const & filename,
+ QFileSystemWatcher * qwatcher)
+ : filename_(filename), qwatcher_(qwatcher), exists_(true)
{
- if (monitoring())
+ if (filename.empty())
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();
+}
- if (!pimpl_->filename_.exists())
- return;
- pimpl_->timestamp_ = pimpl_->filename_.lastModified();
- pimpl_->checksum_ = sum(pimpl_->filename_);
+FileMonitorGuard::~FileMonitorGuard()
+{
+ if (!filename_.empty())
+ qwatcher_->removePath(toqstr(filename_));
+}
+
- if (pimpl_->timestamp_ && pimpl_->checksum_) {
- pimpl_->timer_.start();
- } else {
- pimpl_->timestamp_ = 0;
- pimpl_->checksum_ = 0;
+void FileMonitorGuard::refresh(bool const emit)
+{
+ if (filename_.empty())
+ return;
+ QString const qfilename = toqstr(filename_);
+ if (!qwatcher_->files().contains(qfilename)) {
+ bool const existed = exists_;
+ 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
+ {
+ LYXERR(Debug::FILES,
+ "Could not add path to QFileSystemWatcher: " << filename_);
+ QTimer::singleShot(5000, this, SLOT(refresh()));
+ } else {
+ if (!exists_)
+ // The standard way to overwrite a file is to delete it and
+ // create a new file with the same name. Therefore if the file
+ // has just been deleted, it is smart to check not too long
+ // after whether it has been recreated.
+ QTimer::singleShot(existed ? 100 : 2000, this, SLOT(refresh()));
+ if (existed != exists_ && emit)
+ Q_EMIT fileChanged(exists_);
+ }
}
}
-void FileMonitor::stop() const
+void FileMonitorGuard::notifyChange(QString const & path)
{
- pimpl_->timestamp_ = 0;
- pimpl_->checksum_ = 0;
- pimpl_->timer_.stop();
+ if (path == toqstr(filename_)) {
+ // 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).
+ // Therefore we must refresh.
+ refresh(false);
+ Q_EMIT fileChanged(exists_);
+ }
}
-bool FileMonitor::monitoring() const
+FileMonitor::FileMonitor(std::shared_ptr<FileMonitorGuard> monitor)
+ : monitor_(monitor)
{
- return pimpl_->timer_.running();
+ connectToFileMonitorGuard();
+ refresh();
}
-unsigned long FileMonitor::checksum() const
+void FileMonitor::connectToFileMonitorGuard()
{
- // If we aren't actively monitoring the file, then recompute the
- // checksum explicitly.
- if (!pimpl_->timer_.running() && !pimpl_->filename_.empty())
- return sum(pimpl_->filename_);
-
- return pimpl_->checksum_;
+ // Connections need to be asynchronous because the receiver can own this
+ // object and therefore is allowed to delete it.
+ // Qt signal:
+ QObject::connect(monitor_.get(), SIGNAL(fileChanged(bool)),
+ this, SIGNAL(fileChanged(bool)),
+ Qt::QueuedConnection);
+ // Boost signal:
+ QObject::connect(this, SIGNAL(fileChanged(bool)),
+ this, SLOT(changed(bool)));
}
-boost::signals::connection FileMonitor::connect(slot_type const & slot) const
+connection FileMonitor::connect(slot const & slot)
{
- return pimpl_->fileChanged_.connect(slot);
+ return fileChanged_.connect(slot);
}
-//------------------------------
-// Implementation details follow
-//------------------------------
+void FileMonitor::changed(bool const exists)
+{
+ // emit boost signal
+ fileChanged_(exists);
+}
-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(bool)), this, SLOT(setCooldown()));
+ QTimer::singleShot(interval_, this, SLOT(clearCooldown()));
+ filename_.refresh();
+ if (!filename_.exists())
+ return;
+ timestamp_ = filename_.lastModified();
+ checksum_ = filename_.checksum();
}
-void FileMonitor::Impl::monitorFile()
+void ActiveFileMonitor::checkModified()
{
- bool changed = false;
+ if (cooldown_)
+ return;
- if (!filename_.exists()) {
+ cooldown_ = true;
+ bool changed = false;
+ filename_.refresh();
+ bool exists = filename_.exists();
+ if (!exists) {
changed = timestamp_ || checksum_;
timestamp_ = 0;
checksum_ = 0;
-
} else {
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_();
+ Q_EMIT FileMonitor::fileChanged(exists);
+ 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"