Files
logos-app/app/CoreModuleManager.h
Dario Gabriel Lipicar 9b8cf6e47e feat(shell): ship main_ui as a plugin that links no logos runtime
Measured on the built artefact: main_ui.dylib DEFINES zero and IMPORTS zero of
TokenManager, StoreRegistry, LogosAPI, LogosAPIClient and logos_core_*, out of
3410 symbols read. It links Qt and nothing else from this workspace, which
nix/symbol-gate.nix enforces across the in-process image set.

Getting there needed the last non-Qt types off the boundary: the model
properties cross as QAbstractItemModel*, catalogInstallStageChanged carries an
int rather than InstallStage::Value, and the two prebuilt AppsFilterProxy
instances are declared in QML instead of owned by MainUIBackend. That last one
removes a real inversion -- PackageCoordinator called setRequiredPackages() on
a proxy the host held a pointer to; it now emits requiredPackagesResolved() and
QML binds to the republished property.

Window resolves the plugin, qobject_casts it to IShellView, checks
hostAbiVersion() against IShellHost_abi, and calls createShell(IShellHost*).
No error-label fallback widget: that degraded to something that looked like a
working app with an empty window.

Filter proxies read role constants off host-side models and InstallEnums is
used by nine host files, so app/interfaces/ gains the contract headers both
sides compile against -- the models inherit the role structs, leaving every
AppsModel::NameRole call site unchanged. The plugin's include path is
app/interfaces only, so including a host header does not compile.

Four things only running it finds:
  * Logos::DesignSystem may be linked by exactly ONE image -- both linked it
    and the app aborted with "Cannot add multiple registrations for
    Logos.Icons"; QML module registration is process-global
  * qmltyperegistrar emits no #include for a SOURCES header given as an
    absolute path outside the project
  * each qt_add_qml_module is its own target and inherits no include dirs
  * AUTOMOC pairs header<->cpp by same-basename-same-DIRECTORY, which the
    split breaks

tst_AppManagerView.qml grows five tests for the QML binding, checked with a
negative control: breaking one assertion fails qml-tests, so they run.
2026-08-22 16:42:13 -03:00

90 lines
3.7 KiB
C++

#pragma once
#include <QObject>
#include <QMap>
#include <QString>
#include <QStringList>
#include <QVariantMap>
#include "logos_api.h"
class QTimer;
// Forward-declared, not included: logos_qt_host_core.h pulls in nlohmann/json,
// and MainUIBackend, UIPluginManager, PackageCoordinator and PluginLoader all
// include this header without touching the facade.
namespace logos { namespace qt { class QtLogosCore; } }
// CoreModuleManager — the app's module-management surface over the SDK facade.
//
// Every call into liblogos (known/loaded module lists, load/unload, cascade
// unload, stats) funnels through this class; UIPluginManager, PackageManager
// and MainUIBackend never touch the C API directly.
//
// The char*/char** marshalling and the `delete[]`-not-`free()` ownership rule
// live once, in logos-qt-sdk's `logos::qt::QtLogosCore`
// (`logos_qt_host_core.h`) over logos-cpp-sdk's `logos::host::LogosCore`.
// Declaring that ABI a second time in the same image is an ODR hazard with no
// diagnostic.
//
// What stays here is what the facade has no basis to decide: the poll interval,
// which thread the timer lives on, when "the module set changed" is announced,
// and the QML-facing key names. The poller is a single 2s QTimer that reads
// per-module CPU/memory and emits coreModulesChanged() so the Modules tab
// re-reads via Q_PROPERTY.
class CoreModuleManager : public QObject {
Q_OBJECT
public:
// `core` is the process-wide facade, owned by main() and outliving this
// object. Must not be null.
explicit CoreModuleManager(LogosAPI* logosAPI,
logos::qt::QtLogosCore* core,
QObject* parent = nullptr);
~CoreModuleManager() override;
// Thin wrappers over QtLogosCore. Callers never see a raw C string.
QStringList knownModules() const;
QStringList loadedModules() const;
// Loads with forward dependencies resolved. Returns true on success.
bool loadModule(const QString& name);
// Returns true on success. Does NOT cascade — see
// unloadModuleWithDependents.
bool unloadModule(const QString& name);
// Tears down `name` and every currently-loaded module that depends on it,
// leaves-first. False if any step failed — the cascade may still have made
// progress, so callers should refresh their UI state.
bool unloadModuleWithDependents(const QString& name);
// Cached as of the last timer tick, so up to ~2s stale; empty for modules
// the poller hasn't seen yet.
QVariantMap moduleStats(const QString& name) const;
// Re-scans every plugin directory, then emits coreModulesChanged(). Called
// by the Modules tab's Reload button and after install/uninstall reshapes
// the known set.
Q_INVOKABLE void refresh();
// Introspection, serialised to JSON for QML. On failure getMethods/
// getEvents return "[]" and callMethod returns error JSON; a disconnected
// module is a normal transient state, not an error.
Q_INVOKABLE QString getMethods(const QString& moduleName);
Q_INVOKABLE QString getEvents(const QString& moduleName);
Q_INVOKABLE QString callMethod(const QString& moduleName,
const QString& methodName,
const QString& argsJson);
signals:
// Emitted by refresh() and after every stats tick. MainUIBackend forwards
// it into its own same-named signal, which is what QML binds to.
void coreModulesChanged();
private slots:
void updateModuleStats();
private:
LogosAPI* m_logosAPI; // not owned
logos::qt::QtLogosCore* m_core; // not owned; owned by main()
QTimer* m_statsTimer; // owned (parent=this)
QMap<QString, QVariantMap> m_moduleStats;
};