Files
logos-basecamp/app/main.cpp
T

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;
}