Replaces the attempts to track incubation controllers directly with a list of all known windows, then pulls the first usable incubation controller when an assignment is requested. This should finally fix incubation controller related use after free crashes.
343 lines
9.6 KiB
C++
343 lines
9.6 KiB
C++
#include "generation.hpp"
|
|
#include <utility>
|
|
|
|
#include <qcontainerfwd.h>
|
|
#include <qcoreapplication.h>
|
|
#include <qdebug.h>
|
|
#include <qdir.h>
|
|
#include <qfileinfo.h>
|
|
#include <qfilesystemwatcher.h>
|
|
#include <qhash.h>
|
|
#include <qlist.h>
|
|
#include <qlogging.h>
|
|
#include <qloggingcategory.h>
|
|
#include <qobject.h>
|
|
#include <qqmlcontext.h>
|
|
#include <qqmlengine.h>
|
|
#include <qqmlerror.h>
|
|
#include <qqmlincubator.h>
|
|
#include <qquickwindow.h>
|
|
#include <qtmetamacros.h>
|
|
|
|
#include "iconimageprovider.hpp"
|
|
#include "imageprovider.hpp"
|
|
#include "incubator.hpp"
|
|
#include "logcat.hpp"
|
|
#include "plugin.hpp"
|
|
#include "qsintercept.hpp"
|
|
#include "reload.hpp"
|
|
#include "scan.hpp"
|
|
|
|
namespace {
|
|
QS_LOGGING_CATEGORY(logScene, "scene");
|
|
}
|
|
|
|
static QHash<const QQmlEngine*, EngineGeneration*> g_generations; // NOLINT
|
|
|
|
EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner)
|
|
: rootPath(rootPath)
|
|
, scanner(std::move(scanner))
|
|
, urlInterceptor(this->rootPath)
|
|
, interceptNetFactory(this->rootPath, this->scanner.fileIntercepts)
|
|
, engine(new QQmlEngine()) {
|
|
g_generations.insert(this->engine, this);
|
|
|
|
this->engine->setOutputWarningsToStandardError(false);
|
|
QObject::connect(this->engine, &QQmlEngine::warnings, this, &EngineGeneration::onEngineWarnings);
|
|
|
|
this->engine->addUrlInterceptor(&this->urlInterceptor);
|
|
this->engine->addImportPath("qs:@/");
|
|
|
|
this->engine->setNetworkAccessManagerFactory(&this->interceptNetFactory);
|
|
this->engine->setIncubationController(&this->delayedIncubationController);
|
|
|
|
this->engine->addImageProvider("icon", new IconImageProvider());
|
|
this->engine->addImageProvider("qsimage", new QsImageProvider());
|
|
this->engine->addImageProvider("qspixmap", new QsPixmapProvider());
|
|
|
|
QsEnginePlugin::runConstructGeneration(*this);
|
|
}
|
|
|
|
EngineGeneration::EngineGeneration(): EngineGeneration(QDir(), QmlScanner()) {}
|
|
|
|
EngineGeneration::~EngineGeneration() {
|
|
if (this->engine != nullptr) {
|
|
qFatal() << this << "destroyed without calling destroy()";
|
|
}
|
|
}
|
|
|
|
void EngineGeneration::destroy() {
|
|
if (this->destroying) return;
|
|
this->destroying = true;
|
|
|
|
if (this->watcher != nullptr) {
|
|
// Multiple generations can detect a reload at the same time.
|
|
QObject::disconnect(this->watcher, nullptr, this, nullptr);
|
|
this->watcher->deleteLater();
|
|
this->watcher = nullptr;
|
|
}
|
|
|
|
for (auto* extension: this->extensions.values()) {
|
|
delete extension;
|
|
}
|
|
|
|
if (this->root != nullptr) {
|
|
QObject::connect(this->root, &QObject::destroyed, this, [this]() {
|
|
// prevent further js execution between garbage collection and engine destruction.
|
|
this->engine->setInterrupted(true);
|
|
|
|
g_generations.remove(this->engine);
|
|
|
|
// Garbage is not collected during engine destruction.
|
|
this->engine->collectGarbage();
|
|
|
|
delete this->engine;
|
|
this->engine = nullptr;
|
|
|
|
auto terminate = this->shouldTerminate;
|
|
auto code = this->exitCode;
|
|
delete this;
|
|
|
|
if (terminate) QCoreApplication::exit(code);
|
|
});
|
|
|
|
this->root->deleteLater();
|
|
this->root = nullptr;
|
|
} else {
|
|
g_generations.remove(this->engine);
|
|
|
|
// the engine has never been used, no need to clean up
|
|
delete this->engine;
|
|
this->engine = nullptr;
|
|
|
|
auto terminate = this->shouldTerminate;
|
|
auto code = this->exitCode;
|
|
delete this;
|
|
|
|
if (terminate) QCoreApplication::exit(code);
|
|
}
|
|
}
|
|
|
|
void EngineGeneration::shutdown() {
|
|
if (this->destroying) return;
|
|
|
|
delete this->root;
|
|
this->root = nullptr;
|
|
delete this->engine;
|
|
this->engine = nullptr;
|
|
delete this;
|
|
}
|
|
|
|
void EngineGeneration::onReload(EngineGeneration* old) {
|
|
if (old != nullptr) {
|
|
// if the old generation holds the window incubation controller as the
|
|
// new generation acquires it then incubators will hang intermittently
|
|
qCDebug(logIncubator) << "Locking incubation controllers of old generation" << old;
|
|
old->incubationControllersLocked = true;
|
|
old->assignIncubationController();
|
|
}
|
|
|
|
QObject::connect(this->engine, &QQmlEngine::quit, this, &EngineGeneration::quit);
|
|
QObject::connect(this->engine, &QQmlEngine::exit, this, &EngineGeneration::exit);
|
|
|
|
if (auto* reloadable = qobject_cast<Reloadable*>(this->root)) {
|
|
reloadable->reload(old ? old->root : nullptr);
|
|
}
|
|
|
|
this->singletonRegistry.onReload(old == nullptr ? nullptr : &old->singletonRegistry);
|
|
this->reloadComplete = true;
|
|
emit this->reloadFinished();
|
|
|
|
if (old != nullptr) {
|
|
QObject::connect(old, &QObject::destroyed, this, [this]() { this->postReload(); });
|
|
old->destroy();
|
|
} else {
|
|
this->postReload();
|
|
}
|
|
}
|
|
|
|
void EngineGeneration::postReload() {
|
|
// This can be called on a generation during its destruction.
|
|
if (this->engine == nullptr || this->root == nullptr) return;
|
|
|
|
QsEnginePlugin::runOnReload();
|
|
|
|
emit this->firePostReload();
|
|
QObject::disconnect(this, &EngineGeneration::firePostReload, nullptr, nullptr);
|
|
}
|
|
|
|
void EngineGeneration::setWatchingFiles(bool watching) {
|
|
if (watching) {
|
|
if (this->watcher == nullptr) {
|
|
this->watcher = new QFileSystemWatcher();
|
|
|
|
for (auto& file: this->scanner.scannedFiles) {
|
|
this->watcher->addPath(file);
|
|
this->watcher->addPath(QFileInfo(file).dir().absolutePath());
|
|
}
|
|
|
|
for (auto& file: this->extraWatchedFiles) {
|
|
this->watcher->addPath(file);
|
|
this->watcher->addPath(QFileInfo(file).dir().absolutePath());
|
|
}
|
|
|
|
QObject::connect(
|
|
this->watcher,
|
|
&QFileSystemWatcher::fileChanged,
|
|
this,
|
|
&EngineGeneration::onFileChanged
|
|
);
|
|
|
|
QObject::connect(
|
|
this->watcher,
|
|
&QFileSystemWatcher::directoryChanged,
|
|
this,
|
|
&EngineGeneration::onDirectoryChanged
|
|
);
|
|
}
|
|
} else {
|
|
if (this->watcher != nullptr) {
|
|
this->watcher->deleteLater();
|
|
this->watcher = nullptr;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool EngineGeneration::setExtraWatchedFiles(const QVector<QString>& files) {
|
|
this->extraWatchedFiles.clear();
|
|
for (const auto& file: files) {
|
|
if (!this->scanner.scannedFiles.contains(file)) {
|
|
this->extraWatchedFiles.append(file);
|
|
}
|
|
}
|
|
|
|
if (this->watcher) {
|
|
this->setWatchingFiles(false);
|
|
this->setWatchingFiles(true);
|
|
}
|
|
|
|
return !this->extraWatchedFiles.isEmpty();
|
|
}
|
|
|
|
void EngineGeneration::onFileChanged(const QString& name) {
|
|
if (!this->watcher->files().contains(name)) {
|
|
this->deletedWatchedFiles.push_back(name);
|
|
} else {
|
|
// some editors (e.g vscode) perform file saving in two steps: truncate + write
|
|
// ignore the first event (truncate) with size 0 to prevent incorrect live reloading
|
|
auto fileInfo = QFileInfo(name);
|
|
if (fileInfo.isFile() && fileInfo.size() == 0) return;
|
|
|
|
emit this->filesChanged();
|
|
}
|
|
}
|
|
|
|
void EngineGeneration::onDirectoryChanged() {
|
|
// try to find any files that were just deleted from a replace operation
|
|
for (auto& file: this->deletedWatchedFiles) {
|
|
if (QFileInfo(file).exists()) {
|
|
emit this->filesChanged();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void EngineGeneration::onEngineWarnings(const QList<QQmlError>& warnings) {
|
|
for (const auto& error: warnings) {
|
|
const auto& url = error.url();
|
|
auto rel = url.scheme() == "qs" && url.path().startsWith("@/qs/") ? "@" % url.path().sliced(5)
|
|
: url.toString();
|
|
|
|
QString objectName;
|
|
auto desc = error.description();
|
|
if (auto i = desc.indexOf(": "); i != -1 && desc.startsWith("QML ")) {
|
|
objectName = desc.first(i) + " at ";
|
|
desc = desc.sliced(i + 2);
|
|
}
|
|
|
|
qCWarning(logScene).noquote().nospace()
|
|
<< objectName << rel << '[' << error.line() << ':' << error.column() << "]: " << desc;
|
|
}
|
|
}
|
|
|
|
void EngineGeneration::registerExtension(const void* key, EngineGenerationExt* extension) {
|
|
if (this->extensions.contains(key)) {
|
|
delete this->extensions.value(key);
|
|
}
|
|
|
|
this->extensions.insert(key, extension);
|
|
}
|
|
|
|
EngineGenerationExt* EngineGeneration::findExtension(const void* key) {
|
|
return this->extensions.value(key);
|
|
}
|
|
|
|
void EngineGeneration::quit() {
|
|
this->shouldTerminate = true;
|
|
this->destroy();
|
|
}
|
|
|
|
void EngineGeneration::exit(int code) {
|
|
this->shouldTerminate = true;
|
|
this->exitCode = code;
|
|
this->destroy();
|
|
}
|
|
|
|
void EngineGeneration::trackWindowIncubationController(QQuickWindow* window) {
|
|
if (this->trackedWindows.contains(window)) return;
|
|
|
|
QObject::connect(window, &QObject::destroyed, this, &EngineGeneration::onTrackedWindowDestroyed);
|
|
this->trackedWindows.append(window);
|
|
this->assignIncubationController();
|
|
}
|
|
|
|
void EngineGeneration::onTrackedWindowDestroyed(QObject* object) {
|
|
this->trackedWindows.removeAll(static_cast<QQuickWindow*>(object)); // NOLINT
|
|
this->assignIncubationController();
|
|
}
|
|
|
|
void EngineGeneration::assignIncubationController() {
|
|
QQmlIncubationController* controller = &this->delayedIncubationController;
|
|
|
|
for (auto* window: this->trackedWindows) {
|
|
if (auto* wctl = window->incubationController()) {
|
|
controller = wctl;
|
|
break;
|
|
}
|
|
}
|
|
|
|
qCDebug(logIncubator) << "Assigning incubation controller" << controller << "to generation"
|
|
<< this
|
|
<< "fallback:" << (controller == &this->delayedIncubationController);
|
|
|
|
this->engine->setIncubationController(controller);
|
|
}
|
|
|
|
EngineGeneration* EngineGeneration::currentGeneration() {
|
|
if (g_generations.size() == 1) {
|
|
return *g_generations.begin();
|
|
} else return nullptr;
|
|
}
|
|
|
|
EngineGeneration* EngineGeneration::findEngineGeneration(const QQmlEngine* engine) {
|
|
return g_generations.value(engine);
|
|
}
|
|
|
|
EngineGeneration* EngineGeneration::findObjectGeneration(const QObject* object) {
|
|
// Objects can still attempt to find their generation after it has been destroyed.
|
|
// if (g_generations.size() == 1) return EngineGeneration::currentGeneration();
|
|
|
|
while (object != nullptr) {
|
|
auto* context = QQmlEngine::contextForObject(object);
|
|
|
|
if (context != nullptr) {
|
|
if (auto* generation = EngineGeneration::findEngineGeneration(context->engine())) {
|
|
return generation;
|
|
}
|
|
}
|
|
|
|
object = object->parent();
|
|
}
|
|
|
|
return nullptr;
|
|
}
|