Phase 5 sub-commit 1: DevConsole + child-output capture + Ctrl+` toggle

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>
This commit is contained in:
2026-05-02 20:58:53 +02:00
parent 31bdac80e6
commit 4c15ac281c
6 changed files with 221 additions and 1 deletions

View File

@@ -261,7 +261,11 @@ bool BackendConnection::spawnChild(QString* errorOut)
env.insert(QStringLiteral("MERCURE_PUBLISHER_JWT_KEY"), m_jwtSecret);
env.insert(QStringLiteral("MERCURE_SUBSCRIBER_JWT_KEY"), m_jwtSecret);
m_child->setProcessEnvironment(env);
m_child->setProcessChannelMode(QProcess::ForwardedChannels);
// Capture both streams into a single chronological buffer so the
// DevConsole sees stderr (FrankenPHP / Symfony logs) interleaved with
// stdout the same way a TTY would. The console keeps still emitting
// each line via qCInfo(lcBundled) so terminal users aren't blinded.
m_child->setProcessChannelMode(QProcess::MergedChannels);
// Linux: kernel kills the child if the parent dies for any reason.
m_child->setChildProcessModifier([] {
@@ -270,6 +274,8 @@ bool BackendConnection::spawnChild(QString* errorOut)
connect(m_child, &QProcess::finished,
this, &BackendConnection::onChildFinished);
connect(m_child, &QProcess::readyReadStandardOutput,
this, &BackendConnection::onChildOutputReady);
qCInfo(lcBundled) << "spawning frankenphp on port" << m_port;
m_child->start();
@@ -296,6 +302,38 @@ void BackendConnection::teardownChild()
disconnect(m_child, nullptr, this, nullptr);
m_child->deleteLater();
m_child = nullptr;
m_childLogBuffer.clear();
}
void BackendConnection::onChildOutputReady()
{
if (!m_child) return;
m_childLogBuffer += QString::fromUtf8(m_child->readAllStandardOutput());
int nl;
while ((nl = m_childLogBuffer.indexOf(QLatin1Char('\n'))) >= 0) {
QString line = m_childLogBuffer.left(nl);
m_childLogBuffer.remove(0, nl + 1);
if (line.endsWith(QLatin1Char('\r'))) {
line.chop(1);
}
qCInfo(lcBundled).noquote() << line;
m_childLog.enqueue(line);
while (m_childLog.size() > kChildLogMax) {
m_childLog.dequeue();
}
emit childLogLine(line);
}
}
QStringList BackendConnection::childLogTail() const
{
QStringList out;
out.reserve(m_childLog.size());
for (const QString& l : m_childLog) {
out.append(l);
}
return out;
}
void BackendConnection::onChildFinished(int exitCode, QProcess::ExitStatus status)

View File

@@ -3,7 +3,9 @@
#include <QElapsedTimer>
#include <QObject>
#include <QProcess>
#include <QQueue>
#include <QString>
#include <QStringList>
#include <QtQmlIntegration>
class QNetworkAccessManager;
@@ -78,6 +80,11 @@ public:
/// 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();
@@ -94,10 +101,16 @@ signals:
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);
@@ -138,6 +151,10 @@ private:
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;