mirror of
https://github.com/logos-co/logos-basecamp.git
synced 2026-08-27 14:51:07 +00:00
322 lines
13 KiB
C++
322 lines
13 KiB
C++
#include "window.h"
|
|
#include "logos_api.h"
|
|
#include "token_manager.h"
|
|
#include "logos_mode.h"
|
|
#include "LogosBasecampPaths.h"
|
|
#include "LogRedirector.h"
|
|
#ifdef ENABLE_QML_INSPECTOR
|
|
#include "inspectorserver.h"
|
|
#endif
|
|
#include <QAccessible>
|
|
#include <QApplication>
|
|
#include <QCommandLineOption>
|
|
#include <QCommandLineParser>
|
|
#include <QCoreApplication>
|
|
#include <QEvent>
|
|
#include <QFileInfo>
|
|
#include <QIcon>
|
|
#include <QDir>
|
|
#include <QStyleHints>
|
|
#include <QTimer>
|
|
#include <QStandardPaths>
|
|
#include <iostream>
|
|
#include <memory>
|
|
#include <QStringList>
|
|
#include <QDebug>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QFile>
|
|
#include "logos_provider_object.h"
|
|
#include "qt_provider_object.h"
|
|
#include "BuildInfo.h"
|
|
#ifdef Q_OS_UNIX
|
|
#include <QSocketNotifier>
|
|
#include <signal.h>
|
|
#include <sys/socket.h>
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
// Replace CoreManager with direct C API functions
|
|
extern "C" {
|
|
void logos_core_add_modules_dir(const char* modules_dir);
|
|
void logos_core_set_persistence_base_path(const char* path);
|
|
void logos_core_set_access_policy(const char* policy_json);
|
|
void logos_core_start();
|
|
void logos_core_cleanup();
|
|
char** logos_core_get_loaded_modules();
|
|
int logos_core_load_module(const char* module_name, bool with_dependencies);
|
|
char* logos_core_process_module(const char* module_path);
|
|
char* logos_core_get_module_stats();
|
|
}
|
|
|
|
#ifdef Q_OS_UNIX
|
|
// Self-pipe pattern for SIGTERM/SIGINT: the signal handler writes one byte
|
|
// to a socketpair; a QSocketNotifier on the main thread wakes the event loop
|
|
// and calls QApplication::quit(), which lets the orderly teardown below
|
|
// (~Window, logos_core_cleanup, log flush) run. Doing anything Qt-related
|
|
// directly from a signal handler is undefined behaviour.
|
|
static int gSignalFd[2] = {-1, -1};
|
|
|
|
static void unixSignalHandler(int)
|
|
{
|
|
char a = 1;
|
|
::write(gSignalFd[0], &a, sizeof(a));
|
|
}
|
|
|
|
static void installUnixSignalHandlers(QApplication& app)
|
|
{
|
|
if (::socketpair(AF_UNIX, SOCK_STREAM, 0, gSignalFd) != 0) {
|
|
qWarning() << "Failed to create signal socketpair; SIGTERM/SIGINT will not trigger graceful shutdown";
|
|
return;
|
|
}
|
|
auto* notifier = new QSocketNotifier(gSignalFd[1], QSocketNotifier::Read, &app);
|
|
QObject::connect(notifier, &QSocketNotifier::activated, &app, [notifier]() {
|
|
notifier->setEnabled(false);
|
|
char tmp;
|
|
::read(gSignalFd[1], &tmp, sizeof(tmp));
|
|
QApplication::quit();
|
|
});
|
|
|
|
struct sigaction sa {};
|
|
sa.sa_handler = unixSignalHandler;
|
|
sigemptyset(&sa.sa_mask);
|
|
sa.sa_flags = SA_RESTART;
|
|
::sigaction(SIGTERM, &sa, nullptr);
|
|
::sigaction(SIGINT, &sa, nullptr);
|
|
}
|
|
#endif
|
|
|
|
// Drain a NULL-terminated char** from liblogos: copy each entry into the
|
|
// returned QStringList and release the heap memory. liblogos allocates with
|
|
// new char[] / new char*[], so delete[] is the correct deallocator.
|
|
QStringList drainModuleNameArray(char** modules) {
|
|
QStringList result;
|
|
if (!modules) return result;
|
|
for (char** p = modules; *p != nullptr; ++p) {
|
|
result.append(QString::fromUtf8(*p));
|
|
delete[] *p;
|
|
}
|
|
delete[] modules;
|
|
return result;
|
|
}
|
|
|
|
int main(int argc, char *argv[])
|
|
{
|
|
// Set logos mode to Local for testing
|
|
//LogosModeConfig::setMode(LogosMode::Local);
|
|
|
|
// Kill the per-file .qmlc disk cache under QStandardPaths::CacheLocation
|
|
// for every QQmlEngine in this process. It must be set before Qt is up:
|
|
// Qt reads the env var when the first engine is constructed, and no later.
|
|
//
|
|
// Rationale: basecamp's own QML modules and the design system are STATIC-
|
|
// embedded via qt_add_qml_module — nothing on disk to cache, so this flag
|
|
// is a no-op for them. The load-bearing effect is on plugin QML under
|
|
// Contents/plugins/<name>/qml/, which ships with nix-frozen mtimes; Qt's
|
|
// (path, mtime + content-hash) cache key can reuse stale .qmlc across app
|
|
// upgrades when a bundled plugin's Q_PROPERTY / signal signatures change
|
|
// between releases. Disabling disk cache costs ~30-100ms of QML parse on
|
|
// the first activation of each plugin per session and makes cross-version
|
|
// plugin upgrades physically immune to that class of staleness
|
|
qputenv("QML_DISABLE_DISK_CACHE", "1");
|
|
|
|
// Create QApplication first
|
|
QApplication app(argc, argv);
|
|
app.setOrganizationName("Logos");
|
|
app.setApplicationName("LogosBasecamp");
|
|
app.styleHints()->setTabFocusBehavior(Qt::TabFocusAllControls);
|
|
|
|
// Parse --user-dir / -u and set LOGOS_USER_DIR before anything else resolves
|
|
// a path. This lets multiple Basecamp instances run side-by-side against
|
|
// isolated data trees (plugins, modules, module_data, logs). LOGOS_USER_DIR
|
|
// overrides baseDirectory() as-is (no "Dev" suffix), so the user gets the
|
|
// exact path they asked for. parse() rather than process() so unrecognised
|
|
// flags (e.g. Qt's own -platform, -style) don't abort startup.
|
|
{
|
|
QCommandLineParser parser;
|
|
QCommandLineOption userDirOption({"u", "user-dir"},
|
|
QStringLiteral("Override the data directory (isolates plugins, "
|
|
"modules, module_data, logs for this instance)."),
|
|
QStringLiteral("path"));
|
|
parser.addOption(userDirOption);
|
|
if (!parser.parse(app.arguments())) {
|
|
std::cerr << parser.errorText().toStdString() << std::endl;
|
|
return 1;
|
|
}
|
|
if (parser.isSet(userDirOption)) {
|
|
const QString absUserDir =
|
|
QFileInfo(parser.value(userDirOption)).absoluteFilePath();
|
|
QFileInfo userDirInfo(absUserDir);
|
|
if (userDirInfo.exists() && !userDirInfo.isDir()) {
|
|
qCritical() << "The --user-dir path exists but is not a directory:"
|
|
<< absUserDir;
|
|
return 1;
|
|
}
|
|
if (!userDirInfo.exists() && !QDir().mkpath(absUserDir)) {
|
|
qCritical() << "Failed to create --user-dir directory:"
|
|
<< absUserDir;
|
|
return 1;
|
|
}
|
|
qputenv("LOGOS_USER_DIR", absUserDir.toUtf8());
|
|
}
|
|
}
|
|
|
|
// Redirect stdout/stderr to a rotating per-session log file under
|
|
// <baseDirectory>/logs. Must happen after setOrganizationName/setApplicationName
|
|
// and after the --user-dir override is applied so baseDirectory() resolves
|
|
// to the right location. Terminal output is preserved by mirroring to the
|
|
// original stdout.
|
|
const QString logsDir = LogosBasecampPaths::logsDirectory();
|
|
if (!LogosBasecampLog::LogRedirector::instance().start(logsDir)) {
|
|
qWarning() << "Failed to start log redirection; continuing without file logs."
|
|
<< "Logs directory:" << logsDir;
|
|
}
|
|
|
|
// Print build metadata (version, dev/portable, commit hashes) so the
|
|
// per-session log captures exactly which sources produced this binary.
|
|
LogosBasecampBuildInfo::logStartupBanner();
|
|
qInfo().noquote() << "Base data directory:" << LogosBasecampPaths::baseDirectory();
|
|
|
|
// Set up module directories for logos core.
|
|
// 1. Embedded modules directory (pre-installed at build time, read-only)
|
|
QString embeddedModulesDir = QDir::cleanPath(QCoreApplication::applicationDirPath() + "/../modules");
|
|
logos_core_add_modules_dir(embeddedModulesDir.toUtf8().constData());
|
|
|
|
// 2. User-writable modules directory (for runtime installs via the package store)
|
|
QString userModulesDir = LogosBasecampPaths::modulesDirectory();
|
|
logos_core_add_modules_dir(userModulesDir.toUtf8().constData());
|
|
|
|
// Set persistence base path for core modules
|
|
logos_core_set_persistence_base_path(
|
|
LogosBasecampPaths::moduleDataDirectory().toUtf8().constData());
|
|
|
|
// Access policy temporarily disabled (allow all): passing NULL clears any
|
|
// policy so no enforcement runs. The enforce mode's derived deny-by-default
|
|
// gates every ui_qml app's calls to its own backend module, because UI
|
|
// plugins are loaded out-of-process and aren't tracked as dependents in the
|
|
// core ModuleRegistry — so they're never in a module's derived allowed-caller
|
|
// set and get denied (e.g. accounts_ui -> accounts_module). Re-enable once the
|
|
// access policy is redesigned to account for ui_qml callers.
|
|
// (Must be set before logos_core_start().)
|
|
logos_core_set_access_policy(nullptr);
|
|
|
|
// Start the core
|
|
logos_core_start();
|
|
std::cout << "Logos Core started successfully!" << std::endl;
|
|
|
|
bool loaded = logos_core_load_module("package_manager", true);
|
|
|
|
if (loaded) {
|
|
qInfo() << "package_manager module loaded by default.";
|
|
} else {
|
|
qWarning() << "Failed to load package_manager module by default.";
|
|
}
|
|
|
|
bool downloaderLoaded = logos_core_load_module("package_downloader", true);
|
|
if (downloaderLoaded) {
|
|
qInfo() << "package_downloader module loaded by default.";
|
|
} else {
|
|
qWarning() << "Failed to load package_downloader module by default.";
|
|
}
|
|
|
|
// Log the initial loaded-module list.
|
|
const QStringList modules = drainModuleNameArray(logos_core_get_loaded_modules());
|
|
|
|
if (modules.isEmpty()) {
|
|
qInfo() << "No modules loaded.";
|
|
} else {
|
|
qInfo() << "Currently loaded modules:";
|
|
for (const QString& name : modules) {
|
|
qInfo() << " -" << name;
|
|
}
|
|
qInfo() << "Total modules:" << modules.size();
|
|
}
|
|
|
|
LogosAPI logosAPI("core", nullptr);
|
|
|
|
qDebug() << "LogosAPI: printing keys";
|
|
QList<QString> keys = logosAPI.getTokenManager()->getTokenKeys();
|
|
for (const QString& key : keys) {
|
|
qDebug() << "LogosAPI: Token key:" << key << "value:" << logosAPI.getTokenManager()->getToken(key);
|
|
}
|
|
|
|
// Set application icon.
|
|
#ifdef Q_OS_LINUX
|
|
// setDesktopFileName is required for Wayland compositors, which look up the
|
|
// icon via the .desktop file name rather than honouring setWindowIcon().
|
|
app.setDesktopFileName("logos-basecamp");
|
|
#endif
|
|
app.setWindowIcon(QIcon(":/icons/logos.png"));
|
|
|
|
// Don't quit when last window is closed (for system tray support)
|
|
app.setQuitOnLastWindowClosed(false);
|
|
|
|
#ifdef Q_OS_UNIX
|
|
installUnixSignalHandlers(app);
|
|
#endif
|
|
|
|
// Create and show the main window. Heap-allocated so we can control
|
|
// destruction ordering explicitly during shutdown (see below).
|
|
auto mainWindow = std::make_unique<Window>(&logosAPI);
|
|
mainWindow->show();
|
|
|
|
#ifdef ENABLE_QML_INSPECTOR
|
|
// Start QML Inspector server (controlled by QML_INSPECTOR_PORT env var, default 3768)
|
|
InspectorServer::attach(mainWindow.get());
|
|
#endif
|
|
|
|
// Set up timer to poll module stats every 2 seconds
|
|
QTimer* statsTimer = new QTimer(&app);
|
|
statsTimer->start(2000);
|
|
|
|
// Run the application
|
|
int result = app.exec();
|
|
|
|
// Graceful teardown of the UI before QApplication is destroyed.
|
|
//
|
|
// On macOS, tearing down a QQuickWidget hierarchy crashes inside
|
|
// QCocoaAccessibility::notifyAccessibilityUpdate: QQuickItem destructors
|
|
// call setParentItem(nullptr) which triggers setEffectiveVisibleRecur(false),
|
|
// which notifies the accessibility bridge about items whose backing
|
|
// QObjects are already half-destroyed (null d_ptr → SIGSEGV).
|
|
//
|
|
// Hiding the window alone is insufficient — ~QQuickItem() unconditionally
|
|
// calls setParentItem(nullptr), bypassing the widget visibility state.
|
|
// The fix is to install a no-op accessibility update handler before
|
|
// destroying the widget hierarchy, so the platform bridge is never invoked
|
|
// on partially-destroyed objects.
|
|
statsTimer->stop();
|
|
if (mainWindow) {
|
|
mainWindow->hide();
|
|
QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
|
|
QCoreApplication::processEvents();
|
|
|
|
// Suppress accessibility notifications during destruction and the
|
|
// subsequent deferred-delete drain. QQuickItem::~QQuickItem() →
|
|
// setParentItem(nullptr) → setEffectiveVisibleRecur →
|
|
// notifyAccessibilityUpdate will hit this no-op instead of the
|
|
// Cocoa bridge. The handler stays suppressed through processEvents()
|
|
// because deleteLater() work queued during destruction can also
|
|
// trigger the same crash path.
|
|
auto previousHandler = QAccessible::installUpdateHandler(
|
|
[](QAccessibleEvent*) {});
|
|
|
|
mainWindow.reset();
|
|
|
|
// Drain remaining deferred work while the no-op handler is still active.
|
|
QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
|
|
QCoreApplication::processEvents();
|
|
|
|
// Restore the original handler now that all deferred work is done.
|
|
QAccessible::installUpdateHandler(previousHandler);
|
|
}
|
|
|
|
// Cleanup logos core (plugins, modules, etc.)
|
|
logos_core_cleanup();
|
|
|
|
// Flush final output, restore original stdout/stderr, and close the log file.
|
|
LogosBasecampLog::LogRedirector::instance().stop();
|
|
|
|
return result;
|
|
}
|