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.
|
||||
step "tear down + relaunch from fresh staging (regression: cache-baked-mount-path)"
|
||||
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
|
||||
sleep 0.2
|
||||
done
|
||||
|
||||
@@ -12,6 +12,28 @@ services:
|
||||
arguments:
|
||||
$expectedToken: '%env(default::BRIDGE_TOKEN)%'
|
||||
|
||||
# Maker classes extend symfony/maker-bundle's AbstractMaker, which is a
|
||||
# require-dev dependency. In `composer install --no-dev` builds (the
|
||||
# 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\BridgeResourceMaker:
|
||||
arguments:
|
||||
$qmlPath: '%bridge.qml_path%'
|
||||
|
||||
@@ -11,12 +11,15 @@
|
||||
#include <QProcessEnvironment>
|
||||
#include <QQmlEngine>
|
||||
#include <QRandomGenerator>
|
||||
#include <QSocketNotifier>
|
||||
#include <QStandardPaths>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
|
||||
#include <csignal>
|
||||
#include <fcntl.h>
|
||||
#include <sys/prctl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace PhpQml::Bridge {
|
||||
|
||||
@@ -28,6 +31,53 @@ constexpr int kProbeIntervalMs = 5000;
|
||||
constexpr int kProbeTimeoutMs = 2000;
|
||||
constexpr int kMigrateTimeoutMs = 60000;
|
||||
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
|
||||
|
||||
BackendConnection::BackendConnection(QObject* parent)
|
||||
@@ -46,9 +96,16 @@ BackendConnection::BackendConnection(QObject* parent)
|
||||
// starts unwinding the stack. Without this, teardownChild only runs from
|
||||
// ~BackendConnection — by then the QQmlEngine is already mid-destruction
|
||||
// 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()) {
|
||||
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit,
|
||||
this, &BackendConnection::teardownChild);
|
||||
installShutdownSignalRelay();
|
||||
}
|
||||
|
||||
const QString explicitUrl = QString::fromUtf8(qgetenv("BRIDGE_URL"));
|
||||
@@ -323,6 +380,14 @@ bool BackendConnection::spawnChild(QString* errorOut)
|
||||
void BackendConnection::teardownChild()
|
||||
{
|
||||
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) {
|
||||
m_child->terminate();
|
||||
if (!m_child->waitForFinished(2000)) {
|
||||
@@ -330,7 +395,6 @@ void BackendConnection::teardownChild()
|
||||
m_child->waitForFinished(1000);
|
||||
}
|
||||
}
|
||||
disconnect(m_child, nullptr, this, nullptr);
|
||||
m_child->deleteLater();
|
||||
m_child = nullptr;
|
||||
m_childLogBuffer.clear();
|
||||
|
||||
Reference in new issue
Block a user