Files
MobileGL/MobileGL/MG_Remote/FatalFunnel.cpp
T

330 lines
14 KiB
C++

// MobileGL - MobileGL/MG_Remote/FatalFunnel.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include <MG_Remote/FatalFunnel.h>
#include "../Includes.h" // MGLOG_F and the umbrella; a .cpp may pull it, a Transport/ header may not
#include <MG_Pipe/PipeSessionFail.h> // the seam MG_Backend's renderer dies through
#include "Client/ClientSession.h"
#include "Protocol/generated/protocol_generated.h"
#include "Server/ServerSession.h"
#include "Transport/ITransport.h"
#include <atomic>
#include <cstdarg>
#include <cstdio>
#include <mutex>
#include <unordered_map>
#include <flatbuffers/flatbuffers.h>
namespace MobileGL::MG_Remote {
namespace {
// One telemetry point (5.2). Bumped exactly once per death, before the abort, so a test
// that forks a child and reads it back sees the increment even though the child is gone -
// it reads its OWN process's counter for the funnel it drove, not the child's.
std::atomic<std::uint64_t> g_sessionFaultCount{0};
// SetSessionLatchObserver's; read once per latched session.
std::atomic<SessionLatchObserver> g_sessionLatchObserver{nullptr};
} // namespace
std::uint64_t SessionFaultCount() {
return g_sessionFaultCount.load(std::memory_order_relaxed);
}
void SetSessionLatchObserver(SessionLatchObserver observer) {
g_sessionLatchObserver.store(observer, std::memory_order_release);
}
namespace {
// THE PROJECTION FROM MG_Pipe's TWO-WORD BOUNDARY ENUM ONTO THE .def's VOCABULARY, as a
// switch with a named default rather than a cast. MGPipeFatalFamily is deliberately not
// MGFatalFamily's ordinals (PipeSessionFail.h says why), so this arm is the only place
// the two vocabularies meet - and adding a third word there means arguing for a row here.
//
// The line is forwarded through "%s" rather than re-used as a format: it has already been
// vsnprintf'd once, and a detail string that happened to contain a `%` would otherwise be
// read as a conversion the second time round.
MGFatalFamily MapPipeFamily(MG_Pipe::MGPipeFatalFamily family) {
switch (family) {
case MG_Pipe::MGPipeFatalFamily::UnmigratedVerb:
return MGFatalFamily::UnmigratedVerb;
case MG_Pipe::MGPipeFatalFamily::RoleViolation:
return MGFatalFamily::RoleViolation;
#if MOBILEGL_BUILD_DISAGGREGATED
case MG_Pipe::MGPipeFatalFamily::ProtocolCorruption:
return MGFatalFamily::ProtocolCorruption;
case MG_Pipe::MGPipeFatalFamily::DeviceLost:
return MGFatalFamily::BackendDeviceLost;
#endif
}
return MGFatalFamily::ProtocolCorruption;
}
[[noreturn]] void PipeSessionFailAdapter(MG_Pipe::MGPipeFatalFamily family,
const char* line) {
SessionFail(MapPipeFamily(family), "%s", line);
}
#if MOBILEGL_BUILD_DISAGGREGATED
// The latch twin (PipeSessionFail.h): the calling thread's session latches the fault and
// the backend returns from its verb; unarmed, SessionLatch is SessionFail.
bool PipeSessionLatchAdapter(MG_Pipe::MGPipeFatalFamily family, const char* line) {
return SessionLatch(MapPipeFamily(family), "%s", line);
}
#endif
} // namespace
void InstallPipeSessionFailHook() {
MG_Pipe::MGPipeInstallSessionFailHook(&PipeSessionFailAdapter);
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Pipe::MGPipeInstallSessionLatchHook(&PipeSessionLatchAdapter, &SessionLatchArmed);
#endif
}
namespace {
// The line, formatted exactly as the site's old MGLOG_F would have. Same buffer size and
// same fallback as WireLogFatal, so a death that used either path reads identically.
void FormatFaultLine(char (&line)[512], const char* fmt, va_list args) {
const int written = std::vsnprintf(line, sizeof(line), fmt, args);
if (written < 0) {
std::snprintf(line, sizeof(line),
"MG_Remote: unformattable Fatal diagnostic (format=%s)", fmt);
}
}
// The log line and its stderr echo: Defines.h builds the logger with the console sink
// off, so an MGLOG line reaches only the file - and a gtest death matcher reads the
// child's stderr. Flushed because abort() does not flush stdio.
void LogFaultLine(const char* line) {
MGLOG_F("%s", line);
std::fputs(line, stderr);
std::fputc('\n', stderr);
std::fflush(stderr);
}
// The SessionFault frame to whichever peer this process has; defined below SessionFail,
// whose body it was until PH-1 (3) gave the latch a second caller.
void PublishSessionFault(MGFatalFamily family, const char* line);
// PH-1 (3): the latch's state. Armed once, by the session child; latched once, by the
// first fault. The line is written under the mutex BEFORE the release store that
// publishes `g_latched`, so a reader that saw true reads a whole line.
//
// P14 S2: ONE OF THESE PER SESSION (FatalFunnel.h's domain block). A domain's storage is
// heap-allocated by FatalLatchDomainOpen and lives until FatalLatchDomainClose; domain 0
// is this process-wide one, which is what every thread that binds nothing uses.
struct LatchState {
int id = 0;
std::atomic<bool> armed{false};
std::atomic<bool> latched{false};
std::atomic<std::uint64_t> count{0};
std::mutex mutex;
MGFatalFamily latchedFamily = MGFatalFamily::ProtocolCorruption;
char latchedLine[512] = {};
};
LatchState g_defaultLatch;
thread_local LatchState* t_latch = &g_defaultLatch;
std::mutex g_domainsMutex;
std::unordered_map<int, LatchState*> g_domains;
int g_nextDomain = 1;
LatchState& Latch() { return *t_latch; }
} // namespace
int FatalLatchDomainOpen() {
const std::lock_guard<std::mutex> lock(g_domainsMutex);
if (g_nextDomain <= 0 || g_nextDomain > kMaxLatchDomains) return 0;
LatchState* state = new LatchState{};
state->id = g_nextDomain++;
g_domains.emplace(state->id, state);
return state->id;
}
void FatalLatchDomainClose(int domain) {
if (domain == 0) return;
LatchState* state = nullptr;
{
const std::lock_guard<std::mutex> lock(g_domainsMutex);
const auto found = g_domains.find(domain);
if (found == g_domains.end()) return;
state = found->second;
g_domains.erase(found);
}
delete state;
}
int FatalLatchDomainBind(int domain) {
LatchState* const previous = t_latch;
if (domain == 0) {
t_latch = &g_defaultLatch;
return previous->id;
}
{
const std::lock_guard<std::mutex> lock(g_domainsMutex);
const auto found = g_domains.find(domain);
t_latch = found == g_domains.end() ? &g_defaultLatch : found->second;
}
return previous->id;
}
void ArmSessionLatch() { Latch().armed.store(true, std::memory_order_release); }
void ResetSessionLatch() {
LatchState& latch = Latch();
const std::lock_guard<std::mutex> lock(latch.mutex);
latch.armed.store(false, std::memory_order_release);
latch.count.store(0, std::memory_order_relaxed);
latch.latchedFamily = MGFatalFamily::ProtocolCorruption;
latch.latchedLine[0] = '\0';
latch.latched.store(false, std::memory_order_release);
}
bool SessionLatchArmed() { return Latch().armed.load(std::memory_order_acquire); }
bool SessionLatched() { return Latch().latched.load(std::memory_order_acquire); }
MGFatalFamily SessionLatchedFamily() {
LatchState& latch = Latch();
const std::lock_guard<std::mutex> lock(latch.mutex);
return latch.latchedFamily;
}
const char* SessionLatchedLine() { return Latch().latchedLine; }
std::uint64_t SessionLatchCount() { return Latch().count.load(std::memory_order_relaxed); }
bool SessionLatch(MGFatalFamily family, const char* fmt, ...) {
char line[512];
va_list args;
va_start(args, fmt);
FormatFaultLine(line, fmt, args);
va_end(args);
LatchState& latch = Latch();
// UNARMED IS SessionFail, byte for byte: the inproc server, the client and every unit
// death test keep the death they had (FatalFunnel.h's block says why inproc must).
if (!latch.armed.load(std::memory_order_acquire)) {
SessionFail(family, "%s", line);
}
g_sessionFaultCount.fetch_add(1, std::memory_order_relaxed);
latch.count.fetch_add(1, std::memory_order_relaxed);
LogFaultLine(line);
bool first = false;
{
const std::lock_guard<std::mutex> lock(latch.mutex);
if (!latch.latched.load(std::memory_order_relaxed)) {
std::snprintf(latch.latchedLine, sizeof(latch.latchedLine), "%s", line);
latch.latchedFamily = family;
latch.latched.store(true, std::memory_order_release);
first = true;
}
}
if (!first) {
// A second fault after the first one latched: the session is already declining, so
// this is logged and counted but it neither re-publishes nor replaces the cause.
MGLOG_E("MG_Remote server: SessionLatch - a further named fault after the latched one "
"(%s); it is declined with the rest of the session",
FatalFamilyName(family));
return false;
}
// ONE LINE THAT SAYS WHAT HAPPENS NEXT, so a log reader can tell a latched end from a
// crash without the supervisor's reap line.
MGLOG_E("MG_Remote server: SessionLatch{%s} - the first named fault of this session is "
"latched; every further record and control op is declined and the session "
"closes (exit %d) for the supervisor to serve the next connection (PH-1, "
"ID-P7-1)",
FatalFamilyName(family), kSessionLatchedExitCode);
// Before the peer hears of it: what the observer records must already hold when the client
// reacts (a lost compositor re-creates its window surface on a fresh session at once).
if (const SessionLatchObserver observer = g_sessionLatchObserver.load(std::memory_order_acquire))
observer();
PublishSessionFault(family, line);
return false;
}
void SessionFail(MGFatalFamily family, const char* fmt, ...) {
char line[512];
va_list args;
va_start(args, fmt);
FormatFaultLine(line, fmt, args);
va_end(args);
g_sessionFaultCount.fetch_add(1, std::memory_order_relaxed);
LogFaultLine(line);
PublishSessionFault(family, line);
std::abort();
}
namespace {
void PublishSessionFault(MGFatalFamily family, const char* line) {
// A SessionFault frame to the peer, BEFORE the abort, so the other side's log names the
// family instead of reading a bare EOF (5.2). Best-effort by nature: this thread is about
// to die, so a send that blocks or fails changes nothing it could have changed anyway.
//
// WHICHEVER ROLE THIS PROCESS IS. Under spawn only one session is active per process -
// the client has ClientSession, the server has ServerSession - so publishing on whatever
// control plane exists reaches the far side. Under inproc both exist and the frame goes
// to an in-process transport the aborting process will never read; harmless, and not
// worth a special case that would then be the untested one.
//
// `code` is the coarse seven-value projection; `family` carries the full word; `message`
// is the same line just logged. seq/op are not threaded through the 90 call sites and the
// message already names the op where it matters, so the frame carries what it can name
// honestly rather than a field that is always zero.
Transport::ITransport* control = nullptr;
if (Client::ClientSession* client = Client::ClientSession::Active()) {
control = client->Control_Plane();
}
if (control == nullptr) {
if (Server::ServerSession* server = Server::ServerSession::Active()) {
control = server->Control_Plane();
}
}
if (control != nullptr) {
::flatbuffers::FlatBufferBuilder builder(256);
const auto fatal = ::MobileGL::Wire::CreateFatalDirect(
builder, FatalCodeForFamily(family), line, FatalFamilyName(family));
const auto root = ::MobileGL::Wire::CreateCtrlEnvelope(
builder, ::MobileGL::Wire::CtrlMsg::Fatal, fatal.Union());
::MobileGL::Wire::FinishCtrlEnvelopeBuffer(builder, root);
(void)control->SendFrame(
MobileGLByteSpan{builder.GetBufferPointer(), builder.GetSize()});
}
}
} // namespace
// P13 W5: THE RECORD ARM'S DEATHS GO THROUGH THIS FUNNEL WHEREVER MG_Remote IS LINKED.
// Registered at static init rather than at role init: both roles, and unit cases that never
// run one, call into the record arm, and each of its deaths must count, publish and latch
// exactly as the direct SessionFail / SessionLatch call it replaced did. The line arrives
// formatted, so it is passed through "%s" - the same bytes reach the log and the frame.
namespace {
[[noreturn]] void RecordFailThroughFunnel(MGFatalFamily family, const char* line) {
SessionFail(family, "%s", line);
}
bool RecordLatchThroughFunnel(MGFatalFamily family, const char* line) {
return SessionLatch(family, "%s", line);
}
[[maybe_unused]] const bool g_recordFailHooksRegistered = [] {
MG_Pipe::MGPipeInstallRecordFailHooks(&RecordFailThroughFunnel, &RecordLatchThroughFunnel,
&SessionLatched);
return true;
}();
} // namespace
} // namespace MobileGL::MG_Remote