Phase 1 sub-commit 4: Qt foundation types
All checks were successful
CI / Quality (push) Successful in 5s
All checks were successful
CI / Quality (push) Successful in 5s
BackendConnection (QML singleton via create() factory) reads BRIDGE_URL and BRIDGE_TOKEN from env, periodically probes <url>/healthz with a 2s transfer timeout, and exposes a Connecting/Online/Error state machine plus error/token properties to QML. Bundled-mode startup (spawning the embedded FrankenPHP child) is a Phase 4 deliverable; restart() is a no-op for now. tokenRotated signal is reserved for the per-session secret rotation described in PLAN.md §3. SingleInstance is C++-only — main() must call acquireOrForward() before the QML engine boots, so it's exposed via context property rather than QML_SINGLETON. QLocalServer-based lock with stale-socket detection, launch-arg forwarding via QDataStream, and the deadlock-avoiding race fallback specified in §3 *Edge cases*. CMakeLists.txt declares the PhpQml.Bridge static QML module with both sources and is dual-mode: stands alone for sanity builds, integrates via add_subdirectory from the skeleton's top-level CMake (Phase 1 sub-commit 6). Standalone build verified clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
82
framework/qml/src/SingleInstance.cpp
Normal file
82
framework/qml/src/SingleInstance.cpp
Normal file
@@ -0,0 +1,82 @@
|
||||
#include "SingleInstance.h"
|
||||
|
||||
#include <QDataStream>
|
||||
#include <QLocalSocket>
|
||||
#include <QThread>
|
||||
|
||||
namespace PhpQml::Bridge {
|
||||
|
||||
namespace {
|
||||
constexpr int kForwardConnectTimeoutMs = 200;
|
||||
constexpr int kForwardWriteTimeoutMs = 1500;
|
||||
constexpr int kBindRetries = 3;
|
||||
constexpr int kBindRetryDelayMs = 50;
|
||||
} // namespace
|
||||
|
||||
SingleInstance::SingleInstance(const QString& applicationId, QObject* parent)
|
||||
: QObject(parent)
|
||||
, m_appId(applicationId)
|
||||
{
|
||||
connect(&m_server, &QLocalServer::newConnection, this, &SingleInstance::onNewConnection);
|
||||
}
|
||||
|
||||
QString SingleInstance::endpointName() const
|
||||
{
|
||||
// QLocalServer maps the name to a per-user path on Unix and a named
|
||||
// pipe on Windows. The appId keeps multiple bridge-using apps apart.
|
||||
return QStringLiteral("php-qml-bridge.%1").arg(m_appId);
|
||||
}
|
||||
|
||||
bool SingleInstance::acquireOrForward(const QStringList& launchArgs)
|
||||
{
|
||||
const QString name = endpointName();
|
||||
|
||||
for (int attempt = 0; attempt < kBindRetries; ++attempt) {
|
||||
if (m_server.listen(name)) {
|
||||
return true; // we are the live instance
|
||||
}
|
||||
|
||||
// Bind failed. Either a real instance is running (forward args), or
|
||||
// a stale socket/pipe is left over from a crashed process (clean up
|
||||
// and retry). Distinguish by trying to connect.
|
||||
QLocalSocket probe;
|
||||
probe.connectToServer(name);
|
||||
if (probe.waitForConnected(kForwardConnectTimeoutMs)) {
|
||||
QDataStream out(&probe);
|
||||
out.setVersion(QDataStream::Qt_6_5);
|
||||
out << launchArgs;
|
||||
probe.flush();
|
||||
probe.waitForBytesWritten(kForwardWriteTimeoutMs);
|
||||
probe.disconnectFromServer();
|
||||
return false;
|
||||
}
|
||||
|
||||
// No live peer responding — likely stale socket. Remove and retry
|
||||
// with a brief backoff (PLAN.md §3 *Edge cases — Single-instance
|
||||
// launch race*).
|
||||
QLocalServer::removeServer(name);
|
||||
QThread::msleep(kBindRetryDelayMs);
|
||||
}
|
||||
|
||||
// Exhausted retries without binding and without a live peer. Better to
|
||||
// act as the live instance than to deadlock both processes into exiting.
|
||||
return true;
|
||||
}
|
||||
|
||||
void SingleInstance::onNewConnection()
|
||||
{
|
||||
while (auto* socket = m_server.nextPendingConnection()) {
|
||||
connect(socket, &QLocalSocket::readyRead, this, [this, socket]() {
|
||||
QDataStream in(socket);
|
||||
in.setVersion(QDataStream::Qt_6_5);
|
||||
QStringList args;
|
||||
in >> args;
|
||||
if (in.status() == QDataStream::Ok) {
|
||||
emit launchArgsReceived(args);
|
||||
}
|
||||
});
|
||||
connect(socket, &QLocalSocket::disconnected, socket, &QLocalSocket::deleteLater);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace PhpQml::Bridge
|
||||
Reference in New Issue
Block a user