Auto-detected on construction:
- BRIDGE_URL env set → dev mode (today's behaviour, unchanged).
- BRIDGE_URL unset → bundled mode: BackendConnection now
1. Resolves the user app data dir (QStandardPaths::AppDataLocation,
~/.local/share/<org>/<app> on Linux) and ensures var/, var/log/,
var/cache/ exist there.
2. Generates a per-session 32-byte URL-safe token and a 48-byte
Mercure JWT secret.
3. Runs `frankenphp php-cli bin/console doctrine:migrations:migrate -n`
against the user's DATABASE_URL with a 60s timeout.
4. Spawns FrankenPHP via QProcess with BRIDGE_TOKEN/MERCURE_*/PORT
in the env, prctl(PR_SET_PDEATHSIG, SIGTERM) on the child, and
a supervisor that re-spawns up to 5 times on unexpected exit.
Each restart fires tokenRotated(newToken).
Path resolution defaults to applicationDirPath() + bin/frankenphp,
applicationDirPath() + symfony, applicationDirPath() + Caddyfile, with
both `/../share/<app>/...` and `/../usr/share/<app>/...` fallbacks for
AppImage-style layouts. All three are overridable via
BRIDGE_FRANKENPHP_BIN / BRIDGE_SYMFONY_DIR / BRIDGE_CADDYFILE env vars.
Caddyfiles in skeleton + example now use {$VAR:default} substitution
for PORT and the Mercure JWT keys, so the same Caddyfile works in both
modes. Dev defaults match symfony/.env.
restart() in bundled mode re-spawns the child (resets the supervisor
counter); in dev mode it stays a probe-only no-op.
Smoke-tested locally with `BRIDGE_FRANKENPHP_BIN=… BRIDGE_SYMFONY_DIR=…
BRIDGE_CADDYFILE=… ./build/qml/todo` (no BRIDGE_URL): bundled mode
created ~/.local/share/php-qml/todo/var/data.sqlite, ran the migration,
spawned FrankenPHP, served /healthz, accepted a POST /api/todos with
the per-session bearer. Dev mode (`make dev`) still works unchanged.
Includes a `phpqml.bridge.bundled` Q_LOGGING_CATEGORY so failures
surface to the user; enable with QT_LOGGING_RULES='phpqml.bridge.bundled.*=true'.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
124 lines
3.7 KiB
C++
124 lines
3.7 KiB
C++
#pragma once
|
|
|
|
#include <QElapsedTimer>
|
|
#include <QObject>
|
|
#include <QProcess>
|
|
#include <QString>
|
|
#include <QtQmlIntegration>
|
|
|
|
class QNetworkAccessManager;
|
|
class QNetworkReply;
|
|
class QTimer;
|
|
class QQmlEngine;
|
|
class QJSEngine;
|
|
|
|
namespace PhpQml::Bridge {
|
|
|
|
/// Owns the backend lifecycle and exposes its health to QML.
|
|
///
|
|
/// Mode is auto-detected on construction:
|
|
/// - `BRIDGE_URL` env set → **dev mode**: connect to a developer-managed
|
|
/// backend at that URL.
|
|
/// - `BRIDGE_URL` unset → **bundled mode** (Phase 4a): spawn the embedded
|
|
/// `frankenphp` next to the host binary, generate a per-session bearer
|
|
/// token, run first-launch migrations, and supervise the child.
|
|
///
|
|
/// See PLAN.md §3 (Run modes), §7 (BackendConnection), §13 Phase 4a.
|
|
class BackendConnection : public QObject
|
|
{
|
|
Q_OBJECT
|
|
QML_ELEMENT
|
|
QML_SINGLETON
|
|
|
|
Q_PROPERTY(Mode mode READ mode CONSTANT)
|
|
Q_PROPERTY(QString url READ url NOTIFY urlChanged)
|
|
Q_PROPERTY(QString token READ token NOTIFY tokenChanged)
|
|
Q_PROPERTY(ConnectionState connectionState READ connectionState NOTIFY connectionStateChanged)
|
|
Q_PROPERTY(QString error READ error NOTIFY errorChanged)
|
|
|
|
public:
|
|
enum class Mode {
|
|
Dev,
|
|
Bundled,
|
|
};
|
|
Q_ENUM(Mode)
|
|
|
|
/// Full Update Semantics enum (PLAN.md §5).
|
|
enum class ConnectionState {
|
|
Connecting,
|
|
Online,
|
|
Reconnecting,
|
|
Offline,
|
|
};
|
|
Q_ENUM(ConnectionState)
|
|
|
|
explicit BackendConnection(QObject* parent = nullptr);
|
|
~BackendConnection() override;
|
|
|
|
static BackendConnection* create(QQmlEngine* engine, QJSEngine*);
|
|
|
|
Mode mode() const noexcept { return m_mode; }
|
|
QString url() const { return m_url; }
|
|
QString token() const { return m_token; }
|
|
ConnectionState connectionState() const noexcept { return m_state; }
|
|
QString error() const { return m_error; }
|
|
|
|
/// Bundled mode: re-spawn the FrankenPHP child (e.g. after the user
|
|
/// hits Retry on the Offline overlay). Dev mode: re-probe.
|
|
Q_INVOKABLE void restart();
|
|
|
|
signals:
|
|
void urlChanged();
|
|
void tokenChanged();
|
|
void connectionStateChanged();
|
|
void errorChanged();
|
|
/// Emitted in bundled mode when the supervisor restarts the FrankenPHP
|
|
/// child and a fresh per-session secret is generated. RestClient and
|
|
/// MercureClient pick the new value up on next request (§3 *Edge cases*).
|
|
void tokenRotated(const QString& newToken);
|
|
|
|
private slots:
|
|
void probe();
|
|
void onProbeFinished();
|
|
void onChildFinished(int exitCode, QProcess::ExitStatus status);
|
|
|
|
private:
|
|
void initDevMode();
|
|
void initBundledMode();
|
|
bool runMigrations();
|
|
bool spawnChild(QString* errorOut = nullptr);
|
|
void teardownChild();
|
|
QString resolveFrankenphpBin() const;
|
|
QString resolveSymfonyDir() const;
|
|
QString resolveCaddyfilePath() const;
|
|
QString userDataDir() const;
|
|
QString databaseUrl() const;
|
|
void setState(ConnectionState s);
|
|
void setError(const QString& msg);
|
|
void setUrl(const QString& url);
|
|
void setToken(const QString& token);
|
|
static QString randomSecret(int bytes);
|
|
|
|
Mode m_mode = Mode::Dev;
|
|
QString m_url;
|
|
QString m_token;
|
|
QString m_jwtSecret;
|
|
ConnectionState m_state = ConnectionState::Connecting;
|
|
QString m_error;
|
|
QString m_appName;
|
|
QString m_dataDir;
|
|
quint16 m_port = 8765;
|
|
|
|
QNetworkAccessManager* m_nam = nullptr;
|
|
QNetworkReply* m_pendingReply = nullptr;
|
|
QTimer* m_retryTimer = nullptr;
|
|
QElapsedTimer m_firstFailureSinceOnline;
|
|
int m_offlineThresholdMs = 30000;
|
|
|
|
QProcess* m_child = nullptr;
|
|
int m_supervisorRetries = 0;
|
|
static constexpr int kMaxSupervisorRetries = 5;
|
|
};
|
|
|
|
} // namespace PhpQml::Bridge
|