lyx_mirror/src/support/FileMonitor.cpp
Guillaume MM 131f4b92ba Fix segfault after deleting monitor
The boost signal was sent synchronously, and so made the Qt signal to be posted
in FileMonitor::changed after the boost signal returned, so after the sender was
possibly destroyed.

The solution is to make the boost signal asynchronous using the Qt event loop.

Thanks to Scott Kostyshak for the report and MWE.
2018-12-04 00:05:01 -05:00

263 lines
6.5 KiB
C++

/**
* \file FileMonitor.cpp
* This file is part of LyX, the document processor.
* 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/debug.h"
#include "support/FileName.h"
#include "support/qstring_helpers.h"
#include "support/unique_ptr.h"
#include <QFile>
#include <QStringList>
#include <QTimer>
#include <algorithm>
using namespace std;
namespace lyx {
namespace support {
FileSystemWatcher & FileSystemWatcher::instance()
{
// This is thread-safe because QFileSystemWatcher is thread-safe.
static FileSystemWatcher f;
return f;
}
FileSystemWatcher::FileSystemWatcher()
: qwatcher_(make_unique<QFileSystemWatcher>())
{}
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;
}
//static
FileMonitorPtr FileSystemWatcher::monitor(FileName const & filename)
{
return make_unique<FileMonitor>(instance().getGuard(filename));
}
//static
ActiveFileMonitorPtr FileSystemWatcher::activeMonitor(FileName const & filename,
int interval)
{
return make_unique<ActiveFileMonitor>(instance().getGuard(filename),
filename, interval);
}
//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), exists_(true)
{
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();
}
FileMonitorGuard::~FileMonitorGuard()
{
if (!filename_.empty())
qwatcher_->removePath(toqstr(filename_));
}
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 FileMonitorGuard::notifyChange(QString const & path)
{
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_);
}
}
FileMonitor::FileMonitor(std::shared_ptr<FileMonitorGuard> monitor)
: monitor_(monitor)
{
connectToFileMonitorGuard();
refresh();
}
void FileMonitor::connectToFileMonitorGuard()
{
// 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)));
}
signals2::connection FileMonitor::connect(slot const & slot)
{
return fileChanged_.connect(slot);
}
void FileMonitor::changed(bool const exists)
{
// emit boost signal
fileChanged_(exists);
}
ActiveFileMonitor::ActiveFileMonitor(std::shared_ptr<FileMonitorGuard> monitor,
FileName const & filename, int interval)
: FileMonitor(monitor), filename_(filename), interval_(interval),
timestamp_(0), checksum_(0), cooldown_(true)
{
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 ActiveFileMonitor::checkModified()
{
if (cooldown_)
return;
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 = filename_.checksum();
if (new_checksum != checksum_) {
checksum_ = new_checksum;
changed = true;
}
}
}
if (changed)
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"