#include "DevDriver.h" #include "controllers/PanePool.h" #include "AppModel.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Localhost eval server; see DevDriver.h for the protocol. namespace { constexpr quint16 kDefaultPort = 45773; // Writes one newline-delimited JSON response line. void writeResponse(QTcpSocket* socket, const QJsonValue& id, bool ok, const QJsonValue& value, const QString& error) { if (socket == nullptr) { return; } QJsonObject out; out.insert(QStringLiteral("id"), id); out.insert(QStringLiteral("ok"), ok); if (ok) { out.insert(QStringLiteral("value"), value); } else { out.insert(QStringLiteral("error"), error); } socket->write(QJsonDocument(out).toJson(QJsonDocument::Compact)); socket->write("\n"); } } // namespace DevDriver::DevDriver(QQmlApplicationEngine* engine, QObject* parent) : QObject(parent), m_engine(engine) { // Resolve the live scene objects and expose them to evaluated code. const QList roots = engine->rootObjects(); auto* rootWindow = qobject_cast(roots.isEmpty() ? nullptr : roots.first()); QObject* shellObject = nullptr; QObject* devHelpersObject = nullptr; if (rootWindow != nullptr) { shellObject = rootWindow->findChild("xeno-shell"); devHelpersObject = rootWindow->findChild("xeno-dev-helpers"); } engine->rootContext()->setContextProperty(QStringLiteral("ui"), rootWindow); engine->rootContext()->setContextProperty( QStringLiteral("shell"), shellObject); // Helper surface for driving panes / reading screen text from eval scripts. engine->rootContext()->setContextProperty(QStringLiteral("dev"), devHelpersObject); // The model + pool singletons: DevHelpers and eval scripts address the // app state directly (model.workspaces, pool.item(paneId), ...). engine->rootContext()->setContextProperty(QStringLiteral("model"), AppModel::instance()); engine->rootContext()->setContextProperty(QStringLiteral("pool"), PanePool::instance()); // Inert unless explicitly enabled: shipped builds must expose nothing. if (!qEnvironmentVariableIsSet("XENO_DEV_SERVER")) { return; } bool portOk = false; const int parsedPort = qEnvironmentVariableIntValue("XENO_DEV_PORT", &portOk); const quint16 port = (portOk && parsedPort > 0 && parsedPort <= 65535) ? static_cast(parsedPort) : kDefaultPort; connect(&m_server, &QTcpServer::newConnection, this, &DevDriver::acceptConnections); if (!m_server.listen(QHostAddress::LocalHost, port)) { qWarning("dev-driver: listen failed on 127.0.0.1:%u: %s", static_cast(port), m_server.errorString().toUtf8().constData()); return; } qInfo("dev-driver: listening on 127.0.0.1:%d", static_cast(m_server.serverPort())); } DevDriver::~DevDriver() = default; void DevDriver::acceptConnections() { while (QTcpSocket* socket = m_server.nextPendingConnection()) { socket->setParent(this); connect(socket, &QTcpSocket::disconnected, socket, &QTcpSocket::deleteLater); connect(socket, &QTcpSocket::disconnected, this, [this, socket] { m_readBuffers.remove(socket); }); connect(socket, &QTcpSocket::readyRead, this, [this, socket] { readRequest(socket); }); } } void DevDriver::readRequest(QTcpSocket* socket) { QByteArray& buffer = m_readBuffers[socket]; buffer.append(socket->readAll()); while (true) { const qsizetype newline = buffer.indexOf('\n'); if (newline < 0) { break; } const QByteArray line = buffer.left(newline); buffer.remove(0, newline + 1); dispatch(socket, line); } } void DevDriver::dispatch(QTcpSocket* socket, const QByteArray& line) { if (line.trimmed().isEmpty()) { return; } QJsonParseError parseError{}; const QJsonDocument doc = QJsonDocument::fromJson(line, &parseError); if (!doc.isObject()) { writeResponse(socket, QJsonValue(), false, QJsonValue(), QStringLiteral("bad JSON: %1").arg(parseError.errorString())); return; } const QJsonObject request = doc.object(); const QJsonValue id = request.value(QStringLiteral("id")); if (request.contains(QLatin1String("ping"))) { writeResponse(socket, id, true, QStringLiteral("pong"), {}); return; } if (request.contains(QLatin1String("screenshot"))) { auto* window = qobject_cast( m_engine->rootObjects().isEmpty() ? nullptr : m_engine->rootObjects().first()); if (window == nullptr) { writeResponse(socket, id, false, QJsonValue(), QStringLiteral("no root window to grab")); return; } const QImage image = window->grabWindow(); if (image.isNull()) { writeResponse(socket, id, false, QJsonValue(), QStringLiteral("grabWindow returned an empty image")); return; } const QString path = QStringLiteral("/tmp/xenomorphic-%1.png") .arg(QDateTime::currentMSecsSinceEpoch()); if (!image.save(path, "PNG")) { writeResponse(socket, id, false, QJsonValue(), QStringLiteral("failed to save %1").arg(path)); return; } writeResponse(socket, id, true, path, {}); return; } const QString filePath = request.value(QStringLiteral("file")).toString(); QString code; if (!filePath.isEmpty()) { QFile file(filePath); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { writeResponse(socket, id, false, QJsonValue(), QStringLiteral("cannot open %1").arg(filePath)); return; } code = QString::fromUtf8(file.readAll()); } else { code = request.value(QStringLiteral("expr")).toString(); } if (code.isEmpty()) { writeResponse(socket, id, false, QJsonValue(), QStringLiteral("need non-empty \"expr\" or \"file\"")); return; } // Evaluate inside the root object's own context so expressions see the // document's imports (SplitView etc.) and ids; the engine root context // has neither. auto* rootObject = m_engine->rootObjects().isEmpty() ? nullptr : m_engine->rootObjects().first(); QQmlExpression expression( rootObject != nullptr ? qmlContext(rootObject) : m_engine->rootContext(), rootObject, code); const QVariant result = expression.evaluate(); if (expression.hasError()) { const QQmlError err = expression.error(); writeResponse(socket, id, false, QJsonValue(), QStringLiteral("%1 (at %2:%3)") .arg(err.description()) .arg(err.line()) .arg(err.column())); return; } // QQmlExpression::evaluate() returns a QVariant that usually wraps a // QJSValue (the engine's value box). QJsonValue::fromVariant cannot // descend into a QJSValue, so object/array results would degrade to // JSON null. Unwrap with toVariant() so expressions can return real // structured JSON (nested objects and arrays) to the client. QVariant unwrapped = result; if (result.metaType() == QMetaType::fromType()) { unwrapped = result.value().toVariant(); } writeResponse(socket, id, true, QJsonValue::fromVariant(unwrapped), {}); }