mirror of
https://github.com/logos-co/logos-app-poc.git
synced 2026-08-27 09:51:17 +00:00
* refactor(plugins): admit consumers through the shared verb Replaces the hand-rolled isolate/mint/register sequence with logos::admitConsumer. Net -16 lines. This is where the duplication bug lived: the registration used to sit inside the has-a-backend branch, below an early return, so a pure-QML plugin registered nothing and called out on the host's ambient ring. That worked only because the ring already held every token and the handshake was never reached — and logos-protocol #71 removes the ring, so it would now be a hard failure rather than a silent elevation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(build): link the view-module-runtime archive before the qt host The Windows cross-build of this branch failed to link, and it is the only platform that could have told us: liblogos_view_module_runtime.a(LogosQmlBridge.cpp.obj): undefined reference to `LogosAPI::forIdentity(QString const&, QObject*)' The symbol is not missing. It is present and correctly mangled in both halves of the host package — nm shows T _ZN8LogosAPI11forIdentityERK7QStringP7QObject and the matching __imp_ thunk in liblogos_qt_host.dll.a — and the archive asks for exactly that name. What was wrong is the ORDER. liblogos_qt_host.dll.a is an IMPORT library, so GNU ld treats it like any other archive: it takes only the members that satisfy references already undefined when it reaches them. logos_qt_host_shared sat AHEAD of LOGOS_VIEW_MODULE_RUNTIME_LIB, so when ld walked the host nothing had asked for forIdentity yet, the member was skipped, and the archive's demand a moment later had nowhere left to resolve. This is the same rule the comment on logos_core below already spells out for four other symbols; the host simply had not needed it yet. It had not needed it because something else was holding the old order up. While app/PluginLoader.cpp called LogosAPI::forIdentity itself, basecamp's own objects demanded the symbol before ld ever reached the host, the member was pulled in early, and the archive's later reference resolved against it for free. Admitting consumers through logos::admitConsumer — the commit right before this one — removed the last first-party call, and with it the accident. So the defect is latent-made-live, not new: the link line has been wrong for as long as the archive has referenced a symbol it does not define. Linux and macOS cannot see any of this. There the host is a real shared object, and shared libraries satisfy undefined references regardless of position, which is why the full x86_64-linux matrix — 19 outputs, every check — was green with the broken order. A green Linux build is not evidence about this line. Measured on a 24-core x86_64-linux box, cross-building x86_64-windows: before packages.x86_64-windows.symbol-gate FAILS to link symbol-gate-negative / bin-bundle-dir / default all FAIL, same cause after packages.x86_64-windows.symbol-gate OK and basecamp master cross-builds the same output green on the same box, which is what established this as a regression of this branch rather than a broken toolchain. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(deps): move onto logos-protocol 0.8 and logos-plugin-qt master logos-plugin-qt#26 made protocol 0.8 a HARD FLOOR for every consumer of logos-qt-host — cpp/logos_provider_object.cpp and cpp/qt_provider_object.cpp call TokenManager::saveInboundToken unguarded — so plugin-qt and protocol move together or not at all. logos-protocol 2e3344ac -> 42460e5b (0.7 -> 0.8, protocol#73) logos-plugin-qt 1aa3e31c -> 048152f2 (plugin-qt#26) logos-liblogos eeb5cd32 -> 5c095129 (liblogos#186, NOT MERGED YET) liblogos is a branch pin because #186 is the liblogos half of this same wave and is still open; retire it for a plain master URL once that lands. This PR's CI was last seen failing at logos-co/setup-nix-cache-action with HTTP 502, every build step `skipped`, which made it look like pure infrastructure. It was not only that. Building the pre-relock tree of THIS branch on a real box reproduces a second, independent failure — the same one logos-standalone-app#42 had: app/PluginLoader.h:12:10: fatal error: logos_consumer.h: No such file or directory logos_consumer.h arrives with #26, so admitting consumers through the shared verb could never have compiled against plugin-qt 1aa3e31c. The 502 hid it. TWO follows added on logos-liblogos, and the honest status of both is that they are REDUNDANT TODAY. That was measured, not assumed: against the pin above, liblogos#186's lock already names plugin-qt 048152f2 — this root's rev — so adding or removing them leaves 6 logos-qt-host derivations and exactly ONE in the built runtime closure, unchanged. Today the two sides agree by coincidence of two locks rather than by constraint. They are added because the next step breaks that coincidence, which was measured too. nix/app.nix:729 copies ${logosLiblogos}/lib/*.{so,dylib,dll} over this app's own lib/ while the binary links logos-qt-host directly. Point this input back at plain master — exactly what retiring the pin does — and liblogos resolves logos-plugin-qt through its own lock again: liblogos master bfbb1998 pins 1aa3e31c and packages a lib/liblogos_qt_host.so at kdz79ljg… against this root's ka5vgzb8…, differing byte-wise. The derivation count goes 6 -> 7. The app would LINK one host runtime and SHIP another in the same directory. Closure audit on packages.x86_64-linux.default (384 paths): logos-qt-host 1 ka5vgzb8…-logos-qt-host-0.1.0 logos-protocol 1 derivation at 0.8, 3 outputs (lib, headers, join) and lib/liblogos_qt_host.so compares BYTE-IDENTICAL to the one in that store path, so the linked host and the shipped host are one image rather than two that merely agree. liblogos_protocol.so is the sole definer of TokenManager:: — LogosBasecamp and ui-host define none of it. logos-view-module-runtime is deliberately NOT given the same follows. It is still 7cbc5a6e, built against plugin-qt ef11c210 and protocol 79894727, so this build does link a 0.7-era static archive against a 0.8 host. That seam was checked rather than waved through: * LAYOUT is safe. LogosAPI's private members are identical between ef11c210 and 048152f2 — same five members, same order, no new virtuals — and 0.8 freezes TokenManager's layout by design. * BEHAVIOUR is safe HERE, for a specific reason. 048152f2 changes LogosAPI::forIdentity to create the private store EMPTY where it used to be bootstrap-seeded, and vmr's include/LogosQmlBridge.h:64 still documents the old contract. But nothing reaches it: basecamp constructs the bridge directly at app/PluginLoader.cpp:292, `new LogosQmlBridge(consumer.api, this)`, on the API logos::admitConsumer already credentialed. vmr's LogosQmlBridge::forIdentity factory is dead code in this consumer; it is only in the link line because it shares an object file with the constructor that is used. Give vmr the follows when vmr#27 moves it to 0.8 — not before, since that PR owns the migration. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> VERIFIED BY BUILDING, each output individually with --print-out-paths on a 24-core x86_64-linux box. 22 of 23 non-empty: x86_64-linux packages default app main-ui-plugin package-manager-ui-plugin bin-bundle-dir bin-bundle-dir-inspector bin-appimage mcp-server logos-qt-mcp coverage x86_64-linux checks smoke-test symbol-gate symbol-gate-negative unit-tests qml-tests sandbox-test host-services-test integration-test x86_64-windows symbol-gate symbol-gate-negative bin-bundle-dir default symbol-gate passes on BOTH targets, and Windows is the one that counts: PE has no symbol interposition, so a duplicate runtime that Linux collapses to one is fatal there. symbol-gate-negative — the planted-duplicate control — passes on both too, so the gate is still capable of failing. The single red is checks.x86_64-linux.shutdown-test, and it is PRE-EXISTING, not a regression of this branch. basecamp master builds the same check on the same box and fails it identically — "Linux: Window.close() quits (Alt+F4 / X button convention) ... FAIL — did not exit within 10000ms", 3 passed / 1 skipped / 1 failed, byte for byte the same shape. It needs a window manager this environment does not have. Note also that no CI job builds it: build.yml runs unit-tests, qml-tests, sandbox-test, integration-test, host-services-test and the symbol gates, and never shutdown-test. NOT COVERED HERE, and this box cannot cover it — stated rather than implied: * aarch64-linux — both build-appimage and test-linux matrix legs * macOS — build-macos-app, test-macos, and the *-bundle outputs only they build (integration-test-bundle, host-services-test-bundle, smoke-test-bundle) * the build-windows job's zip packaging step and its staged-file count assertion; the three nix builds it runs are covered, the packaging is not * doctests.yml on both ubuntu-latest and macos-latest * the three Jenkins packaging jobs (jenkins/prs/package/linux/{x86_64,aarch64} and macos/aarch64), which reported nothing at all on the last run * bump --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
438 lines
17 KiB
C++
438 lines
17 KiB
C++
#include "PluginLoader.h"
|
|
|
|
#include <QCoreApplication>
|
|
#include <QDebug>
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QFileInfo>
|
|
#include <QIcon>
|
|
#include <QMutexLocker>
|
|
#include <QPluginLoader>
|
|
#include "win_dll_search.h"
|
|
#include <QQmlComponent>
|
|
#include <QQmlContext>
|
|
#include <QQmlEngine>
|
|
#include <QQmlError>
|
|
#include <QQuickWidget>
|
|
#include <QThread>
|
|
#include <QTimer>
|
|
#include <QUrl>
|
|
|
|
#include <memory>
|
|
|
|
#include "CoreModuleManager.h"
|
|
#include "IComponent.h"
|
|
#include "LogosQmlBridge.h"
|
|
#include "logos_api.h"
|
|
#include "logos_consumer.h"
|
|
#include "restricted/QmlSandbox.h"
|
|
#include <ViewModuleHost.h>
|
|
|
|
PluginLoader::PluginLoader(LogosAPI* logosAPI,
|
|
CoreModuleManager* coreModuleManager,
|
|
QObject* parent)
|
|
: QObject(parent)
|
|
, m_logosAPI(logosAPI)
|
|
, m_coreModuleManager(coreModuleManager)
|
|
{
|
|
}
|
|
|
|
void PluginLoader::load(const PluginLoadRequest& request)
|
|
{
|
|
if (isLoading(request.name)) {
|
|
qDebug() << "Plugin" << request.name << "is already loading";
|
|
return;
|
|
}
|
|
|
|
setLoading(request.name, true);
|
|
|
|
// Yield to the event loop so the UI can paint the loading state
|
|
QTimer::singleShot(0, this, [this, request]() {
|
|
startLoad(request);
|
|
});
|
|
}
|
|
|
|
bool PluginLoader::isLoading(const QString& name) const
|
|
{
|
|
QMutexLocker lock(&m_mutex);
|
|
return m_loading.contains(name);
|
|
}
|
|
|
|
QStringList PluginLoader::loadingPlugins() const
|
|
{
|
|
QMutexLocker lock(&m_mutex);
|
|
return m_loading.values();
|
|
}
|
|
|
|
void PluginLoader::setLoading(const QString& name, bool loading)
|
|
{
|
|
{
|
|
QMutexLocker lock(&m_mutex);
|
|
if (loading)
|
|
m_loading.insert(name);
|
|
else
|
|
m_loading.remove(name);
|
|
}
|
|
emit loadingChanged();
|
|
}
|
|
|
|
logos::ConsumerIdentity PluginLoader::consumerFor(const QString& name)
|
|
{
|
|
if (name.isEmpty()) {
|
|
qWarning() << "PluginLoader: refusing to build an identity for an unnamed plugin";
|
|
return {};
|
|
}
|
|
|
|
auto it = m_consumers.constFind(name);
|
|
if (it != m_consumers.constEnd())
|
|
return it.value();
|
|
|
|
// logos::admitConsumer isolates the store, constructs the LogosAPI on it,
|
|
// mints a credential, registers it with capability_module over the HOST's
|
|
// trusted channel, and only then installs it in the identity's store. The
|
|
// ordering notes that used to live here — isolate before constructing
|
|
// anything (a LogosAPIClient captures its store as a raw pointer), and
|
|
// register synchronously before the plugin's first QTimer::singleShot(0)
|
|
// call can fire — are now stated once, next to the implementation.
|
|
logos::ConsumerIdentity consumer = logos::admitConsumer(name, m_logosAPI, this);
|
|
if (!consumer) {
|
|
qWarning() << "PluginLoader: could not admit" << name
|
|
<< "as a consumer - refusing to load it with the host's authority";
|
|
return {};
|
|
}
|
|
m_consumers.insert(name, consumer);
|
|
|
|
return consumer;
|
|
}
|
|
|
|
void PluginLoader::startLoad(const PluginLoadRequest& request)
|
|
{
|
|
if (request.coreDependencies.isEmpty()) {
|
|
continueLoad(request);
|
|
return;
|
|
}
|
|
|
|
loadCoreDependencies(request);
|
|
}
|
|
|
|
void PluginLoader::loadCoreDependencies(const PluginLoadRequest& request)
|
|
{
|
|
// liblogos is not thread-safe for plugin loading; call only from the GUI thread.
|
|
// Every core-plugin load goes through CoreModuleManager so the logos_core_*
|
|
// C API is centralised in one place.
|
|
for (const QVariant& dep : request.coreDependencies) {
|
|
QString depName = dep.toString();
|
|
if (depName.isEmpty())
|
|
continue;
|
|
if (!m_coreModuleManager) {
|
|
qWarning() << "Failed to load core dependency" << depName
|
|
<< "for" << request.name;
|
|
setLoading(request.name, false);
|
|
emit pluginLoadFailed(request.name,
|
|
QStringLiteral("Failed to load core dependencies for ") + request.name);
|
|
return;
|
|
}
|
|
qDebug() << "Loading core dependency for" << request.name << ":" << depName;
|
|
if (!m_coreModuleManager->loadModule(depName)) {
|
|
qWarning() << "Failed to load core dependency" << depName
|
|
<< "for" << request.name;
|
|
setLoading(request.name, false);
|
|
emit pluginLoadFailed(request.name,
|
|
QStringLiteral("Failed to load core dependencies for ") + request.name);
|
|
return;
|
|
}
|
|
}
|
|
continueLoad(request);
|
|
}
|
|
|
|
void PluginLoader::continueLoad(const PluginLoadRequest& request)
|
|
{
|
|
switch (request.type) {
|
|
case UIPluginType::UiQml:
|
|
loadUiQmlModule(request);
|
|
break;
|
|
case UIPluginType::Legacy:
|
|
loadCppPluginAsync(request);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// ---------- Legacy ui plugin path ----------
|
|
|
|
void PluginLoader::loadCppPluginAsync(const PluginLoadRequest& request)
|
|
{
|
|
// Pre-load the shared library in a background thread.
|
|
// Qt's QLibraryStore caches loaded libraries globally, so the subsequent
|
|
// QPluginLoader::load() on the main thread will be instant.
|
|
QThread* thread = QThread::create([path = request.pluginPath]() {
|
|
// Add the plugin's OWN directory to the DLL search before the image is
|
|
// mapped, so a UI plugin can resolve libraries vendored beside it.
|
|
// Windows searches the EXECUTABLE's directory, not the importing DLL's.
|
|
// No-op off Windows. The reference is deliberately not released: these
|
|
// plugins stay resident for the process lifetime (QPluginLoader's
|
|
// destructor does not unload), and this is the load that actually maps
|
|
// the image -- the main-thread load below then hits Qt's cache.
|
|
ModuleLib::preloadPluginWithOwnDirSearch(path);
|
|
QPluginLoader loader(path);
|
|
loader.load();
|
|
});
|
|
|
|
connect(thread, &QThread::finished, this,
|
|
[this, thread, request]() {
|
|
thread->deleteLater();
|
|
finishCppPluginLoad(request);
|
|
});
|
|
|
|
thread->start();
|
|
}
|
|
|
|
void PluginLoader::finishCppPluginLoad(const PluginLoadRequest& request)
|
|
{
|
|
// Normally a no-op: loadCppPluginAsync already mapped the image. Kept so
|
|
// this path is correct on its own, since a caller reaching it without the
|
|
// async pre-load would otherwise fail to resolve vendored DLLs.
|
|
ModuleLib::preloadPluginWithOwnDirSearch(request.pluginPath);
|
|
QPluginLoader loader(request.pluginPath);
|
|
if (!loader.load()) {
|
|
qWarning() << "Failed to load plugin:" << request.name << "-" << loader.errorString();
|
|
setLoading(request.name, false);
|
|
emit pluginLoadFailed(request.name, loader.errorString());
|
|
return;
|
|
}
|
|
|
|
QObject* plugin = loader.instance();
|
|
if (!plugin) {
|
|
qWarning() << "Failed to get plugin instance:" << request.name;
|
|
setLoading(request.name, false);
|
|
emit pluginLoadFailed(request.name, QStringLiteral("Failed to get plugin instance"));
|
|
return;
|
|
}
|
|
|
|
IComponent* component = qobject_cast<IComponent*>(plugin);
|
|
if (!component) {
|
|
qWarning() << "Plugin does not implement IComponent:" << request.name;
|
|
loader.unload();
|
|
setLoading(request.name, false);
|
|
emit pluginLoadFailed(request.name, QStringLiteral("Plugin does not implement IComponent"));
|
|
return;
|
|
}
|
|
|
|
// The plugin's own identity, not the host's. A legacy widget plugin calls
|
|
// modules through whatever LogosAPI it is handed, so handing it m_logosAPI
|
|
// handed it the host's ambient token ring.
|
|
//
|
|
// One call where there were two. The old pair minted a UUID, registered it,
|
|
// and then DROPPED it — the identity was registered under a credential
|
|
// nobody held, and it worked only because the isolated store had been born
|
|
// holding a copy of the host's anchor. That copy is gone; the credential is
|
|
// now installed in the store admitConsumer built.
|
|
const logos::ConsumerIdentity consumer = consumerFor(request.name);
|
|
if (!consumer) {
|
|
loader.unload();
|
|
setLoading(request.name, false);
|
|
emit pluginLoadFailed(request.name,
|
|
QStringLiteral("Could not establish an isolated identity for ") + request.name);
|
|
return;
|
|
}
|
|
|
|
QWidget* widget = component->createWidget(consumer.api);
|
|
if (!widget) {
|
|
qWarning() << "Component returned null widget:" << request.name;
|
|
loader.unload();
|
|
setLoading(request.name, false);
|
|
emit pluginLoadFailed(request.name, QStringLiteral("Component returned null widget"));
|
|
return;
|
|
}
|
|
|
|
if (!request.iconPath.isEmpty())
|
|
widget->setWindowIcon(QIcon(request.iconPath));
|
|
|
|
setLoading(request.name, false);
|
|
emit pluginLoaded(request.name, widget, component, UIPluginType::Legacy, nullptr);
|
|
}
|
|
|
|
// ---------- ui_qml module path ----------
|
|
|
|
void PluginLoader::loadUiQmlModule(const PluginLoadRequest& request)
|
|
{
|
|
if (request.qmlViewPath.isEmpty() || !QFile::exists(request.qmlViewPath)) {
|
|
qWarning() << "ui_qml module QML file not found:" << request.qmlViewPath;
|
|
setLoading(request.name, false);
|
|
emit pluginLoadFailed(request.name,
|
|
QStringLiteral("QML view file not found: ") + request.qmlViewPath);
|
|
return;
|
|
}
|
|
|
|
// The bridge the QML gets speaks AS this module, from this module's own
|
|
// token store — not as basecamp.
|
|
//
|
|
// ONE ADMISSION, ONE CREDENTIAL, BOTH PATHS. Two separate things used to be
|
|
// conflated here, and only the second one needs a ui-host:
|
|
//
|
|
// 1. Making `request.name` a KNOWN CALLER at capability_module, which is
|
|
// what lets its very first requestModule get past the known-caller
|
|
// gate. Needed by the pure-QML path too — in fact especially there,
|
|
// since the QML is the only thing calling out.
|
|
// 2. Giving ui-host the credential its backend accepts inbound calls with.
|
|
//
|
|
// The old code did (1) inside the has-a-backend branch, below an early
|
|
// return, so the pure-QML path registered nothing at all. It got away with
|
|
// it because it was calling with the host's ambient ring, where every
|
|
// target's token was already present and no handshake ever happened. Both
|
|
// are now one call that cannot be branched around, and the credential it
|
|
// returns is the SAME value in the store and on the wire to ui-host —
|
|
// where it used to be minted here and never given to the in-process bridge.
|
|
const logos::ConsumerIdentity consumer = consumerFor(request.name);
|
|
if (!consumer) {
|
|
setLoading(request.name, false);
|
|
emit pluginLoadFailed(request.name,
|
|
QStringLiteral("Could not establish an isolated identity for ") + request.name);
|
|
return;
|
|
}
|
|
auto* bridge = new LogosQmlBridge(consumer.api, this);
|
|
|
|
if (request.mainFilePath.isEmpty()) {
|
|
loadQmlView(request, bridge, nullptr);
|
|
return;
|
|
}
|
|
|
|
// Has a backend plugin — spawn a ViewModuleHost process.
|
|
//
|
|
// The credential goes to ui-host, which adopts it into its own image's
|
|
// token store (logos::adoptConsumerCredential). admitConsumer registered it
|
|
// synchronously above, before this process spawns, which is the race the
|
|
// old comment here described: plugin ctors commonly schedule their first
|
|
// IPC via QTimer::singleShot(0, ...) and those fire the instant ui-host
|
|
// enters its event loop, so a registration deferred to onHostReady would
|
|
// lose to them and capability_module would refuse with "auth token not
|
|
// recognized".
|
|
auto* viewHost = new ViewModuleHost(this);
|
|
if (!viewHost->spawn(request.name, request.mainFilePath, consumer.credential)) {
|
|
qWarning() << "Failed to spawn ui-host for ui_qml module" << request.name;
|
|
delete viewHost;
|
|
delete bridge;
|
|
setLoading(request.name, false);
|
|
emit pluginLoadFailed(request.name,
|
|
QStringLiteral("Failed to spawn ui-host for ") + request.name);
|
|
return;
|
|
}
|
|
|
|
auto onHostReady = [this, request, bridge, viewHost]() {
|
|
bridge->setViewModuleSocket(request.name, viewHost->socketName());
|
|
|
|
const QString base = QFileInfo(request.mainFilePath).absolutePath()
|
|
+ QStringLiteral("/") + request.name
|
|
+ QStringLiteral("_replica_factory");
|
|
for (const QString& suffix : { QStringLiteral(".dylib"),
|
|
QStringLiteral(".so"),
|
|
QStringLiteral(".dll") }) {
|
|
const QString factoryPath = base + suffix;
|
|
if (QFile::exists(factoryPath)) {
|
|
bridge->setViewReplicaPlugin(request.name, factoryPath);
|
|
break;
|
|
}
|
|
}
|
|
loadQmlView(request, bridge, viewHost);
|
|
};
|
|
|
|
auto* timeout = new QTimer(this);
|
|
timeout->setSingleShot(true);
|
|
auto readyConn = std::make_shared<QMetaObject::Connection>();
|
|
auto timeoutConn = std::make_shared<QMetaObject::Connection>();
|
|
*readyConn = connect(viewHost, &ViewModuleHost::ready, this,
|
|
[timeout, readyConn, timeoutConn, onHostReady]() {
|
|
QObject::disconnect(*readyConn);
|
|
QObject::disconnect(*timeoutConn);
|
|
timeout->stop();
|
|
timeout->deleteLater();
|
|
onHostReady();
|
|
});
|
|
*timeoutConn = connect(timeout, &QTimer::timeout, this,
|
|
[this, request, viewHost, bridge, timeout, readyConn, timeoutConn]() {
|
|
QObject::disconnect(*readyConn);
|
|
QObject::disconnect(*timeoutConn);
|
|
timeout->deleteLater();
|
|
qWarning() << "Timeout waiting for ui-host ready signal for" << request.name;
|
|
viewHost->stop();
|
|
viewHost->deleteLater();
|
|
delete bridge;
|
|
setLoading(request.name, false);
|
|
emit pluginLoadFailed(request.name,
|
|
QStringLiteral("Timeout waiting for ui-host for ") + request.name);
|
|
});
|
|
timeout->start(30000);
|
|
}
|
|
|
|
void PluginLoader::loadQmlView(const PluginLoadRequest& request,
|
|
LogosQmlBridge* bridge,
|
|
ViewModuleHost* viewHost)
|
|
{
|
|
auto* qmlWidget = new QQuickWidget;
|
|
qmlWidget->setResizeMode(QQuickWidget::SizeRootObjectToView);
|
|
if (QQmlEngine* engine = qmlWidget->engine()) {
|
|
const QString appLibDir =
|
|
QDir(QCoreApplication::applicationDirPath() + "/../lib").canonicalPath();
|
|
QmlSandbox::configure(engine, request.installDir, request.qmlViewPath,
|
|
appLibDir, request.name);
|
|
engine->setBaseUrl(QUrl::fromLocalFile(request.installDir + "/"));
|
|
}
|
|
|
|
// Async pre-compile: the engine caches compiled types so setSource() is fast.
|
|
QUrl sourceUrl = QUrl::fromLocalFile(request.qmlViewPath);
|
|
auto* preloader = new QQmlComponent(qmlWidget->engine(), sourceUrl,
|
|
QQmlComponent::Asynchronous);
|
|
|
|
auto finishOrCleanup = [this, preloader, qmlWidget, request, bridge,
|
|
viewHost](QQmlComponent::Status status) {
|
|
preloader->deleteLater();
|
|
if (status == QQmlComponent::Ready) {
|
|
finishUiQmlLoad(qmlWidget, request, bridge, viewHost);
|
|
} else {
|
|
QString errors;
|
|
for (const auto& e : preloader->errors())
|
|
errors += e.toString() + QStringLiteral("\n");
|
|
qWarning() << "Failed to compile ui_qml view" << request.name << ":" << errors;
|
|
qmlWidget->deleteLater();
|
|
delete bridge;
|
|
if (viewHost) { viewHost->stop(); delete viewHost; }
|
|
setLoading(request.name, false);
|
|
emit pluginLoadFailed(request.name, errors);
|
|
}
|
|
};
|
|
|
|
if (preloader->isReady() || preloader->isError()) {
|
|
finishOrCleanup(preloader->status());
|
|
} else {
|
|
connect(preloader, &QQmlComponent::statusChanged, this, finishOrCleanup);
|
|
}
|
|
}
|
|
|
|
void PluginLoader::finishUiQmlLoad(QQuickWidget* qmlWidget,
|
|
const PluginLoadRequest& request,
|
|
LogosQmlBridge* bridge,
|
|
ViewModuleHost* viewHost)
|
|
{
|
|
bridge->setParent(qmlWidget);
|
|
qmlWidget->rootContext()->setContextProperty("logos", bridge);
|
|
qmlWidget->rootContext()->setContextProperty("isActiveTab", true);
|
|
qmlWidget->setSource(QUrl::fromLocalFile(request.qmlViewPath));
|
|
|
|
if (!request.iconPath.isEmpty())
|
|
qmlWidget->setWindowIcon(QIcon(request.iconPath));
|
|
|
|
if (qmlWidget->status() == QQuickWidget::Error) {
|
|
qWarning() << "Failed to load ui_qml view" << request.name;
|
|
const auto errors = qmlWidget->errors();
|
|
for (const QQmlError& error : errors) qWarning() << error.toString();
|
|
qmlWidget->deleteLater();
|
|
if (viewHost) { viewHost->stop(); delete viewHost; }
|
|
setLoading(request.name, false);
|
|
emit pluginLoadFailed(request.name,
|
|
QStringLiteral("Failed to load QML view for ") + request.name);
|
|
return;
|
|
}
|
|
|
|
setLoading(request.name, false);
|
|
emit pluginLoaded(request.name, qmlWidget, nullptr, UIPluginType::UiQml, viewHost);
|
|
}
|