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>
83 lines
2.7 KiB
C++
83 lines
2.7 KiB
C++
#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
|