bundled: relay SIGTERM/SIGINT into Qt's quit() via self-pipe
The aboutToQuit-based teardown wired in v0.1.2 only fires when something calls QCoreApplication::quit() — typically a window close. `kill -TERM` to the host process bypasses Qt entirely (no default SIGTERM handler), so teardownChild never ran on signal-driven shutdown. Local tests passed on lucky timing because PR_SET_PDEATHSIG made the kernel SIGTERM frankenphp once the host died, but the timing was racy and surfaced on CI as "frankenphp child PID outlived the host (supervisor didn't clean up)". Fix: install a SIGTERM/SIGINT handler in BackendConnection that uses the self-pipe pattern — the C signal handler writes one byte (the only truly async-signal-safe primitive), a QSocketNotifier on the read end calls QCoreApplication::quit() in the main thread, and aboutToQuit runs the existing teardownChild before app.exec() returns. The host now exits cleanly under `kill -TERM` from service managers, launchers, and the test harness. Also bumps the bundled-supervisor.sh first-relaunch grace from 2s to 3s — teardownChild itself waits up to 2s for frankenphp to finish after SIGTERM, so the host needs ~2.x seconds to exit. The graceful-shutdown step further down was already at 3s. No public-API change; production-correctness fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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"));
|
||||
|
||||
Reference in New Issue
Block a user