mirror of
https://github.com/logos-co/logos-view-module-runtime.git
synced 2026-08-31 04:51:07 +00:00
262 lines
10 KiB
C++
262 lines
10 KiB
C++
// ui-host: Out-of-process host for view module plugins.
|
|
// Loads a Qt plugin and exposes it directly via a private QRemoteObjectHost
|
|
// socket. The plugin's Q_INVOKABLE methods, signals, Q_PROPERTYs and enums
|
|
// become available to the parent process via QRemoteObjectDynamicReplica.
|
|
// Any Q_PROPERTY of type QAbstractItemModel* is additionally remoted as a
|
|
// child source so QML can use it as a model directly (see Gap 2 design).
|
|
//
|
|
// Only the parent process (via LogosQmlBridge) knows the socket name, so the
|
|
// plugin is NOT discoverable by other modules.
|
|
|
|
#include <QCoreApplication>
|
|
#include <QCommandLineParser>
|
|
#include <QLocalSocket>
|
|
#include <QPluginLoader>
|
|
#include <QRemoteObjectHost>
|
|
#include <QAbstractItemModel>
|
|
#include <QMetaProperty>
|
|
#include <QSocketNotifier>
|
|
#include <QTextStream>
|
|
#include <QDebug>
|
|
|
|
#include <cerrno>
|
|
#include <csignal>
|
|
#include <cstring>
|
|
#include <thread>
|
|
#ifndef _WIN32
|
|
#include <fcntl.h>
|
|
#include <unistd.h>
|
|
#endif
|
|
#ifdef __linux__
|
|
#include <sys/prctl.h>
|
|
#endif
|
|
|
|
#include "logos_api.h"
|
|
#include "token_manager.h"
|
|
#include "LogosViewPlugin.h"
|
|
|
|
int main(int argc, char* argv[])
|
|
{
|
|
#ifndef _WIN32
|
|
// Out-of-process view-module hosts are spawned by a parent (Basecamp, or any
|
|
// app embedding the view runtime). Mirror logos_host: (1) detach into our own
|
|
// session/process group so tearing the module tree down can't leak a signal
|
|
// into the parent's process group; (2) tie our lifetime to the parent so we
|
|
// never linger as an orphan if the parent *crashes* (it normally kills us
|
|
// explicitly). setsid() removes the controlling-terminal SIGHUP that used to
|
|
// reap orphans, so we replace it with PR_SET_PDEATHSIG (Linux) plus a
|
|
// portable getppid() watchdog. Compare against the parent's actual pid (not
|
|
// pid 1) so a parent that is itself PID 1 (a container) is handled correctly.
|
|
if (::setsid() == -1 && errno != EPERM) {
|
|
::setpgid(0, 0);
|
|
}
|
|
{
|
|
const pid_t parent_pid = ::getppid();
|
|
#ifdef __linux__
|
|
::prctl(PR_SET_PDEATHSIG, SIGKILL);
|
|
#endif
|
|
if (::getppid() != parent_pid) {
|
|
_exit(0);
|
|
}
|
|
std::thread([parent_pid] {
|
|
while (::getppid() == parent_pid) {
|
|
::sleep(1);
|
|
}
|
|
_exit(0);
|
|
}).detach();
|
|
}
|
|
#endif
|
|
|
|
QCoreApplication app(argc, argv);
|
|
app.setOrganizationName("Logos");
|
|
app.setOrganizationDomain("logos.co");
|
|
app.setApplicationName("ui-host");
|
|
|
|
// Catch SIGTERM/SIGINT so the parent's QProcess::terminate() (and a
|
|
// user-issued Ctrl-C) unwinds app.exec() cleanly. Without this the
|
|
// default handler kills the process outright, skipping plugin
|
|
// destructors. SIGKILL stays uncatchable and remains the force-kill
|
|
// fallback in ViewModuleHost::stop().
|
|
//
|
|
// Self-pipe trick: the POSIX handler may only call async-signal-safe
|
|
// functions, and QCoreApplication::quit() is not (it grabs Qt-internal
|
|
// mutexes). Write a byte to a pipe from the handler — write(2) IS
|
|
// async-signal-safe — and let a QSocketNotifier on the main thread
|
|
// pick it up and call quit() from normal Qt context.
|
|
#ifndef _WIN32
|
|
static int sigPipe[2] = {-1, -1};
|
|
if (::pipe(sigPipe) != 0) {
|
|
qWarning() << "ui-host: pipe() for signal handler failed:"
|
|
<< ::strerror(errno);
|
|
} else {
|
|
// Non-blocking read end so the drain loop in the notifier callback
|
|
// returns instead of blocking once the pipe is empty. The write end
|
|
// stays blocking (write of 1 byte never blocks in practice since
|
|
// the pipe buffer is 4 KiB+) so the handler stays trivial.
|
|
::fcntl(sigPipe[0], F_SETFL, ::fcntl(sigPipe[0], F_GETFL, 0) | O_NONBLOCK);
|
|
|
|
auto* notifier = new QSocketNotifier(sigPipe[0], QSocketNotifier::Read, &app);
|
|
QObject::connect(notifier, &QSocketNotifier::activated, &app, [&app]() {
|
|
char buf[8];
|
|
while (::read(sigPipe[0], buf, sizeof(buf)) > 0) {}
|
|
qDebug() << "ui-host: received termination signal, quitting";
|
|
app.quit();
|
|
});
|
|
|
|
struct sigaction sa{};
|
|
sa.sa_handler = [](int) {
|
|
const char c = 1;
|
|
// Drop EINTR/EAGAIN silently — a missed wake-up just delays
|
|
// shutdown until the next signal or socket activity.
|
|
(void)::write(sigPipe[1], &c, 1);
|
|
};
|
|
sigemptyset(&sa.sa_mask);
|
|
sa.sa_flags = SA_RESTART;
|
|
::sigaction(SIGTERM, &sa, nullptr);
|
|
::sigaction(SIGINT, &sa, nullptr);
|
|
}
|
|
#endif
|
|
|
|
QCommandLineParser parser;
|
|
parser.setApplicationDescription("Logos UI module host process");
|
|
parser.addHelpOption();
|
|
|
|
QCommandLineOption nameOpt(QStringList() << "name",
|
|
"Module name", "module_name");
|
|
QCommandLineOption pathOpt(QStringList() << "path",
|
|
"Path to the plugin .so/.dylib", "plugin_path");
|
|
QCommandLineOption socketOpt(QStringList() << "socket",
|
|
"Local socket name for QRemoteObjectHost", "socket_name");
|
|
|
|
parser.addOption(nameOpt);
|
|
parser.addOption(pathOpt);
|
|
parser.addOption(socketOpt);
|
|
parser.process(app);
|
|
|
|
if (!parser.isSet(nameOpt) || !parser.isSet(pathOpt) || !parser.isSet(socketOpt)) {
|
|
qCritical() << "Usage: ui-host --name <module_name> --path <plugin.so> --socket <socket_name>";
|
|
return 1;
|
|
}
|
|
|
|
const QString moduleName = parser.value(nameOpt);
|
|
const QString pluginPath = parser.value(pathOpt);
|
|
const QString socketName = parser.value(socketOpt);
|
|
|
|
// Receive the per-spawn auth token from the parent (ViewModuleHost)
|
|
QString authToken;
|
|
{
|
|
QLocalSocket client;
|
|
client.connectToServer(socketName + QStringLiteral("_token"));
|
|
if (!client.waitForConnected(10000)) {
|
|
qCritical() << "ui-host: failed to connect to parent token socket for"
|
|
<< moduleName << ":" << client.errorString();
|
|
return 1;
|
|
}
|
|
if (!client.waitForReadyRead(5000)) {
|
|
qCritical() << "ui-host: timeout waiting for auth token from parent for"
|
|
<< moduleName;
|
|
return 1;
|
|
}
|
|
authToken = QString::fromUtf8(client.readAll());
|
|
client.disconnectFromServer();
|
|
}
|
|
if (authToken.isEmpty()) {
|
|
qCritical() << "ui-host: parent sent empty auth token for" << moduleName;
|
|
return 1;
|
|
}
|
|
|
|
QPluginLoader loader(pluginPath);
|
|
if (!loader.load()) {
|
|
qCritical() << "Failed to load plugin:" << loader.errorString();
|
|
return 1;
|
|
}
|
|
|
|
QObject* pluginObject = loader.instance();
|
|
if (!pluginObject) {
|
|
qCritical() << "Failed to get plugin instance:" << loader.errorString();
|
|
return 1;
|
|
}
|
|
|
|
qDebug() << "ui-host: loaded plugin" << moduleName << "from" << pluginPath;
|
|
|
|
LogosAPI* logosAPI = new LogosAPI(moduleName);
|
|
logosAPI->setParent(&app);
|
|
|
|
logosAPI->getTokenManager()->saveToken(QStringLiteral("core"), authToken);
|
|
logosAPI->getTokenManager()->saveToken(QStringLiteral("capability_module"), authToken);
|
|
|
|
int methodIndex = pluginObject->metaObject()->indexOfMethod("initLogos(LogosAPI*)");
|
|
if (methodIndex != -1) {
|
|
QMetaObject::invokeMethod(pluginObject, "initLogos",
|
|
Qt::DirectConnection,
|
|
Q_ARG(LogosAPI*, logosAPI));
|
|
qDebug() << "ui-host: called initLogos on plugin" << moduleName;
|
|
}
|
|
|
|
QRemoteObjectHost host(QUrl(QStringLiteral("local:") + socketName));
|
|
|
|
// Prefer typed remoting via the LogosViewPlugin interface. The generated
|
|
// <Foo>ViewPluginBase (from logos_module(REP_FILE ...)) performs
|
|
// host->enableRemoting<FooSourceAPI>(backend), which publishes the typed
|
|
// source signature so typed replicas on the client side reach the Valid
|
|
// state. Without this, dynamic (name-based) remoting would use a
|
|
// different signature hash and typed replicas would stall in Default.
|
|
auto* viewPlugin = qobject_cast<LogosViewPlugin*>(pluginObject);
|
|
if (!viewPlugin) {
|
|
viewPlugin = dynamic_cast<LogosViewPlugin*>(pluginObject);
|
|
}
|
|
QObject* remoteTarget = viewPlugin ? viewPlugin->viewObject() : pluginObject;
|
|
if (!remoteTarget) remoteTarget = pluginObject;
|
|
|
|
bool remotingEnabled = false;
|
|
if (viewPlugin) {
|
|
remotingEnabled = viewPlugin->enableRemoting(&host);
|
|
if (!remotingEnabled) {
|
|
qWarning() << "ui-host: LogosViewPlugin::enableRemoting() returned "
|
|
"false, falling back to dynamic remoting";
|
|
}
|
|
}
|
|
|
|
// Fallback: dynamic remoting. All Q_INVOKABLEs, slots, signals, and
|
|
// Q_PROPERTYs (with NOTIFY) on the remote target propagate to a
|
|
// QRemoteObjectDynamicReplica on the client side. Used for plugins
|
|
// without a .rep / LogosViewPlugin implementation.
|
|
if (!remotingEnabled) {
|
|
if (!host.enableRemoting(remoteTarget, moduleName)) {
|
|
qCritical() << "Failed to enable remoting for" << moduleName;
|
|
return 1;
|
|
}
|
|
}
|
|
qDebug() << "ui-host: remoting enabled for" << moduleName;
|
|
|
|
// Gap 2: scan the remote target for Q_PROPERTYs whose value is a
|
|
// QAbstractItemModel* and remote each as a child source named
|
|
// "<moduleName>/<propertyName>". The parent side acquires these via
|
|
// QRemoteObjectNode::acquireModel().
|
|
const QMetaObject* mo = remoteTarget->metaObject();
|
|
for (int i = 0; i < mo->propertyCount(); ++i) {
|
|
QMetaProperty prop = mo->property(i);
|
|
if (!prop.isReadable()) continue;
|
|
QVariant value = prop.read(remoteTarget);
|
|
auto* model = qvariant_cast<QAbstractItemModel*>(value);
|
|
if (!model) continue;
|
|
|
|
QList<int> roles = model->roleNames().keys();
|
|
QString childName = QStringLiteral("%1/%2").arg(moduleName, QString::fromUtf8(prop.name()));
|
|
if (!host.enableRemoting(model, childName, roles)) {
|
|
qWarning() << "ui-host: failed to enable remoting for model" << childName;
|
|
continue;
|
|
}
|
|
qDebug() << "ui-host: remoted model property" << prop.name()
|
|
<< "as" << childName << "with roles" << roles;
|
|
}
|
|
|
|
QTextStream out(stdout);
|
|
out << "READY" << Qt::endl;
|
|
out.flush();
|
|
|
|
const int rc = app.exec();
|
|
delete pluginObject;
|
|
return rc;
|
|
}
|