BackendConnection now captures the bundled FrankenPHP child's merged stdout+stderr into a 500-line ring buffer, mirrors each line through qCInfo(lcBundled) so terminal users still see logs, and exposes childLogTail() / childLogLine for QML. DevConsole.qml is an opt-in monospaced viewer with auto-scroll + clear that the skeleton and the todo example bind to Ctrl+`. Dev mode (when BRIDGE_URL is set, no bundled child) renders an explanatory hint. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
166 lines
5.4 KiB
C++
166 lines
5.4 KiB
C++
#pragma once
|
|
|
|
#include <QElapsedTimer>
|
|
#include <QObject>
|
|
#include <QProcess>
|
|
#include <QQueue>
|
|
#include <QString>
|
|
#include <QStringList>
|
|
#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();
|
|
|
|
/// Bundled mode: ask the bundled `AppImageUpdate.AppImage` sidecar
|
|
/// whether a newer release is available. Dev mode: emits
|
|
/// `updateCheckFailed` with an explanatory string. Result is signalled
|
|
/// via updatesAvailable / noUpdatesAvailable / updateCheckFailed.
|
|
Q_INVOKABLE void checkForUpdates();
|
|
|
|
/// Bundled mode: invoke the sidecar to download and apply the
|
|
/// available update in place. The user has to restart the app
|
|
/// after this completes; emits `updateApplied` / `updateApplyFailed`.
|
|
Q_INVOKABLE void applyUpdate();
|
|
|
|
/// Returns a snapshot of the captured child stdout/stderr ring buffer
|
|
/// (most recent kChildLogMax lines). Bundled mode only — empty in dev
|
|
/// mode. Used by `DevConsole.qml` to seed its view on first show.
|
|
Q_INVOKABLE QStringList childLogTail() const;
|
|
|
|
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);
|
|
|
|
void updatesAvailable();
|
|
void noUpdatesAvailable();
|
|
void updateCheckFailed(const QString& reason);
|
|
void updateApplied();
|
|
void updateApplyFailed(const QString& reason);
|
|
|
|
/// Emitted for each newline-terminated chunk read from the bundled
|
|
/// FrankenPHP child's merged stdout+stderr stream. DevConsole.qml
|
|
/// listens for these to populate its log view live.
|
|
void childLogLine(const QString& line);
|
|
|
|
private slots:
|
|
void probe();
|
|
void onProbeFinished();
|
|
void onChildFinished(int exitCode, QProcess::ExitStatus status);
|
|
void onChildOutputReady();
|
|
void onUpdateCheckFinished(int exitCode, QProcess::ExitStatus status);
|
|
void onUpdateApplyFinished(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;
|
|
|
|
QQueue<QString> m_childLog;
|
|
QString m_childLogBuffer;
|
|
static constexpr int kChildLogMax = 500;
|
|
|
|
QProcess* m_updateCheck = nullptr;
|
|
QProcess* m_updateApply = nullptr;
|
|
|
|
QString resolveSidecarUpdater() const;
|
|
QString currentAppImagePath() const;
|
|
};
|
|
|
|
} // namespace PhpQml::Bridge
|