Compare commits
4
Commits
8b2fc4dd06
..
v0.1.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d6b9fde2c | ||
|
|
ed4db00a62 | ||
|
|
ee68561bae | ||
|
|
c78d471368 |
No files matched your search
@@ -145,7 +145,11 @@ fi
|
|||||||
# fresh-mount situation), assert /healthz comes back up.
|
# fresh-mount situation), assert /healthz comes back up.
|
||||||
step "tear down + relaunch from fresh staging (regression: cache-baked-mount-path)"
|
step "tear down + relaunch from fresh staging (regression: cache-baked-mount-path)"
|
||||||
kill -TERM "$PID" 2>/dev/null || true
|
kill -TERM "$PID" 2>/dev/null || true
|
||||||
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
# 3s grace: teardownChild itself waits up to 2s for frankenphp to finish
|
||||||
|
# after sending it SIGTERM, so the host can take ~2.x seconds to exit
|
||||||
|
# cleanly. A 2s loop here was right at the boundary and triggered the
|
||||||
|
# fallback SIGKILL on slower runners.
|
||||||
|
for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do
|
||||||
kill -0 "$PID" 2>/dev/null || break
|
kill -0 "$PID" 2>/dev/null || break
|
||||||
sleep 0.2
|
sleep 0.2
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -12,10 +12,32 @@ services:
|
|||||||
arguments:
|
arguments:
|
||||||
$expectedToken: '%env(default::BRIDGE_TOKEN)%'
|
$expectedToken: '%env(default::BRIDGE_TOKEN)%'
|
||||||
|
|
||||||
PhpQml\Bridge\Maker\BridgeResourceMaker:
|
# Maker classes extend symfony/maker-bundle's AbstractMaker, which is a
|
||||||
arguments:
|
# require-dev dependency. In `composer install --no-dev` builds (the
|
||||||
$qmlPath: '%bridge.qml_path%'
|
# staging-symfony tree the AppImage is assembled from) AbstractMaker is
|
||||||
|
# absent: PHP fails to autoload BridgeResourceMaker etc., so the glob
|
||||||
|
# above silently drops them — that's fine. But a top-level explicit
|
||||||
|
# `services.PhpQml\Bridge\Maker\BridgeResourceMaker:` block forces
|
||||||
|
# ResolveClassPass to load the class regardless of dev/prod, which then
|
||||||
|
# crashes the prod container compile. Scope the qml_path injection to
|
||||||
|
# `when@dev:` so prod builds never touch these definitions.
|
||||||
|
when@dev:
|
||||||
|
services:
|
||||||
|
# _defaults must be repeated here — `when@<env>` opens a fresh
|
||||||
|
# services block, so the top-level autowire/autoconfigure don't
|
||||||
|
# carry over. Without autoconfigure the explicit definitions
|
||||||
|
# below would lose maker-bundle's `maker.command` tag, and
|
||||||
|
# `make:bridge:resource` would silently disappear from the
|
||||||
|
# console while `make:bridge:command` (registered by the glob,
|
||||||
|
# no override) keeps working.
|
||||||
|
_defaults:
|
||||||
|
autowire: true
|
||||||
|
autoconfigure: true
|
||||||
|
|
||||||
PhpQml\Bridge\Maker\BridgeWindowMaker:
|
PhpQml\Bridge\Maker\BridgeResourceMaker:
|
||||||
arguments:
|
arguments:
|
||||||
$qmlPath: '%bridge.qml_path%'
|
$qmlPath: '%bridge.qml_path%'
|
||||||
|
|
||||||
|
PhpQml\Bridge\Maker\BridgeWindowMaker:
|
||||||
|
arguments:
|
||||||
|
$qmlPath: '%bridge.qml_path%'
|
||||||
@@ -11,12 +11,15 @@
|
|||||||
#include <QProcessEnvironment>
|
#include <QProcessEnvironment>
|
||||||
#include <QQmlEngine>
|
#include <QQmlEngine>
|
||||||
#include <QRandomGenerator>
|
#include <QRandomGenerator>
|
||||||
|
#include <QSocketNotifier>
|
||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
|
|
||||||
#include <csignal>
|
#include <csignal>
|
||||||
|
#include <fcntl.h>
|
||||||
#include <sys/prctl.h>
|
#include <sys/prctl.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
namespace PhpQml::Bridge {
|
namespace PhpQml::Bridge {
|
||||||
|
|
||||||
@@ -28,6 +31,53 @@ constexpr int kProbeIntervalMs = 5000;
|
|||||||
constexpr int kProbeTimeoutMs = 2000;
|
constexpr int kProbeTimeoutMs = 2000;
|
||||||
constexpr int kMigrateTimeoutMs = 60000;
|
constexpr int kMigrateTimeoutMs = 60000;
|
||||||
constexpr int kBootProbeMaxMs = 10000;
|
constexpr int kBootProbeMaxMs = 10000;
|
||||||
|
|
||||||
|
// Self-pipe used to relay SIGTERM/SIGINT into the Qt event loop. The
|
||||||
|
// signal handler can only call async-signal-safe functions, so it just
|
||||||
|
// writes one byte to the pipe; a QSocketNotifier on the read end picks
|
||||||
|
// it up in the main thread and calls QCoreApplication::quit(), which
|
||||||
|
// fires aboutToQuit → teardownChild → frankenphp gets a clean SIGTERM
|
||||||
|
// while the event loop is still running. Without this, `kill -TERM`
|
||||||
|
// to the host bypasses Qt entirely and the supervisor never gets to
|
||||||
|
// reap the child.
|
||||||
|
int g_signalPipe[2] = {-1, -1};
|
||||||
|
|
||||||
|
extern "C" void shutdownSignalHandler(int signum)
|
||||||
|
{
|
||||||
|
const char b = static_cast<char>(signum & 0xff);
|
||||||
|
// write() is async-signal-safe; failure is ignored — best effort.
|
||||||
|
[[maybe_unused]] auto _ = ::write(g_signalPipe[1], &b, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void installShutdownSignalRelay()
|
||||||
|
{
|
||||||
|
if (g_signalPipe[0] != -1) return; // already installed
|
||||||
|
if (::pipe2(g_signalPipe, O_CLOEXEC | O_NONBLOCK) != 0) {
|
||||||
|
qCWarning(lcBundled) << "shutdown signal pipe creation failed; SIGTERM will not run teardownChild cleanly";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// QSocketNotifier needs a parent that outlives any signal delivery.
|
||||||
|
// QCoreApplication is the natural anchor.
|
||||||
|
auto* notifier = new QSocketNotifier(g_signalPipe[0], QSocketNotifier::Read,
|
||||||
|
QCoreApplication::instance());
|
||||||
|
QObject::connect(notifier, &QSocketNotifier::activated, [](QSocketDescriptor) {
|
||||||
|
char buf[16];
|
||||||
|
while (::read(g_signalPipe[0], buf, sizeof(buf)) > 0) {
|
||||||
|
// drain — content is just the signum, we don't care which
|
||||||
|
}
|
||||||
|
if (auto* app = QCoreApplication::instance()) {
|
||||||
|
app->quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
struct sigaction sa{};
|
||||||
|
sa.sa_handler = &shutdownSignalHandler;
|
||||||
|
sigemptyset(&sa.sa_mask);
|
||||||
|
sa.sa_flags = SA_RESTART;
|
||||||
|
::sigaction(SIGTERM, &sa, nullptr);
|
||||||
|
::sigaction(SIGINT, &sa, nullptr);
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
BackendConnection::BackendConnection(QObject* parent)
|
BackendConnection::BackendConnection(QObject* parent)
|
||||||
@@ -46,9 +96,16 @@ BackendConnection::BackendConnection(QObject* parent)
|
|||||||
// starts unwinding the stack. Without this, teardownChild only runs from
|
// starts unwinding the stack. Without this, teardownChild only runs from
|
||||||
// ~BackendConnection — by then the QQmlEngine is already mid-destruction
|
// ~BackendConnection — by then the QQmlEngine is already mid-destruction
|
||||||
// and Qt warns "QProcess: Destroyed while process is still running".
|
// and Qt warns "QProcess: Destroyed while process is still running".
|
||||||
|
//
|
||||||
|
// aboutToQuit only fires when something *calls* quit() — Qt does not
|
||||||
|
// install a default SIGTERM handler. installShutdownSignalRelay() bridges
|
||||||
|
// SIGTERM/SIGINT into a quit() call so `kill -TERM` from a service
|
||||||
|
// manager / launcher / test harness goes through the same teardown path
|
||||||
|
// as a window-close.
|
||||||
if (QCoreApplication::instance()) {
|
if (QCoreApplication::instance()) {
|
||||||
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit,
|
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit,
|
||||||
this, &BackendConnection::teardownChild);
|
this, &BackendConnection::teardownChild);
|
||||||
|
installShutdownSignalRelay();
|
||||||
}
|
}
|
||||||
|
|
||||||
const QString explicitUrl = QString::fromUtf8(qgetenv("BRIDGE_URL"));
|
const QString explicitUrl = QString::fromUtf8(qgetenv("BRIDGE_URL"));
|
||||||
@@ -323,6 +380,14 @@ bool BackendConnection::spawnChild(QString* errorOut)
|
|||||||
void BackendConnection::teardownChild()
|
void BackendConnection::teardownChild()
|
||||||
{
|
{
|
||||||
if (!m_child) return;
|
if (!m_child) return;
|
||||||
|
// Disconnect *before* terminating: waitForFinished() pumps a local event
|
||||||
|
// loop, so QProcess::finished would fire synchronously inside that wait,
|
||||||
|
// run onChildFinished as the crash-supervisor restart path, and spawn a
|
||||||
|
// brand-new frankenphp child during shutdown — the new QProcess then
|
||||||
|
// gets destroyed mid-spawn during stack unwinding and Qt warns
|
||||||
|
// "Destroyed while process is still running". Severing signals first
|
||||||
|
// turns terminate() into the synchronous reap it should always have been.
|
||||||
|
disconnect(m_child, nullptr, this, nullptr);
|
||||||
if (m_child->state() != QProcess::NotRunning) {
|
if (m_child->state() != QProcess::NotRunning) {
|
||||||
m_child->terminate();
|
m_child->terminate();
|
||||||
if (!m_child->waitForFinished(2000)) {
|
if (!m_child->waitForFinished(2000)) {
|
||||||
@@ -330,7 +395,6 @@ void BackendConnection::teardownChild()
|
|||||||
m_child->waitForFinished(1000);
|
m_child->waitForFinished(1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
disconnect(m_child, nullptr, this, nullptr);
|
|
||||||
m_child->deleteLater();
|
m_child->deleteLater();
|
||||||
m_child = nullptr;
|
m_child = nullptr;
|
||||||
m_childLogBuffer.clear();
|
m_childLogBuffer.clear();
|
||||||
|
|||||||
Reference in new issue
Block a user