diff --git a/MobileGL/MG_Remote/Client/ClientSession.cpp b/MobileGL/MG_Remote/Client/ClientSession.cpp index e045aff1..fb771d24 100644 --- a/MobileGL/MG_Remote/Client/ClientSession.cpp +++ b/MobileGL/MG_Remote/Client/ClientSession.cpp @@ -1034,6 +1034,21 @@ namespace MobileGL::MG_Remote::Client { } continue; } + if (envelope->msg_type() == ::MobileGL::Wire::CtrlMsg::Fatal) { + // `dl` step three: the server published a SessionFault before it aborted, so this + // is a death that NAMES ITSELF instead of arriving as a bare EOF. Log the family + // and detail, latch device-lost from it, and report the op declined - the same + // answer a hangup gives, now with a cause the guest's log can read. + if (const auto* fatal = envelope->msg_as_Fatal()) { + const char* fam = fatal->family() ? fatal->family()->c_str() : ""; + const char* msg = fatal->message() ? fatal->message()->c_str() : ""; + MGLOG_E("MG_Remote client: the server published a SessionFault before dying - " + "Fatal family %s: %s", fam, msg); + } + LatchDeviceLost("the server published a SessionFault and then aborted"); + frame.ok = false; + return MOBILEGL_ERR_TRANSPORT_CLOSED; + } if (envelope->msg_type() != ::MobileGL::Wire::CtrlMsg::SurfaceReply || envelope->msg_as_SurfaceReply() == nullptr) { MGLOG_E("MG_Remote client: expected a SurfaceReply, got message type %d", diff --git a/MobileGL/MG_Remote/FatalFunnel.cpp b/MobileGL/MG_Remote/FatalFunnel.cpp index a8892fe7..0c7e6825 100644 --- a/MobileGL/MG_Remote/FatalFunnel.cpp +++ b/MobileGL/MG_Remote/FatalFunnel.cpp @@ -9,11 +9,17 @@ #include #include "../Includes.h" // MGLOG_F and the umbrella; a .cpp may pull it, a Transport/ header may not +#include "Client/ClientSession.h" +#include "Protocol/generated/protocol_generated.h" +#include "Server/ServerSession.h" +#include "Transport/ITransport.h" #include #include #include +#include + namespace MobileGL::MG_Remote { namespace { @@ -50,11 +56,39 @@ namespace MobileGL::MG_Remote { std::fputc('\n', stderr); std::fflush(stderr); - // `dl` step three publishes a SessionFault{family, detail, seq, op} frame to the peer - // here, so the guest's log names which record ended the session instead of reading a bare - // EOF. The family's wire projection is FatalCodeForFamily(family); until the frame lands - // the family is still carried in the line above and counted here. - (void)family; + // 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()}); + } std::abort(); } diff --git a/MobileGL/MG_Remote/Protocol/generated/protocol_generated.h b/MobileGL/MG_Remote/Protocol/generated/protocol_generated.h index a209a7d1..f7aedf7f 100644 --- a/MobileGL/MG_Remote/Protocol/generated/protocol_generated.h +++ b/MobileGL/MG_Remote/Protocol/generated/protocol_generated.h @@ -1463,7 +1463,8 @@ struct Fatal FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { struct Traits; enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { VT_CODE = 4, - VT_MESSAGE = 6 + VT_MESSAGE = 6, + VT_FAMILY = 8 }; MobileGL::Wire::FatalCode code() const { return static_cast(GetField(VT_CODE, 0)); @@ -1471,12 +1472,17 @@ struct Fatal FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { const ::flatbuffers::String *message() const { return GetPointer(VT_MESSAGE); } + const ::flatbuffers::String *family() const { + return GetPointer(VT_FAMILY); + } template bool Verify(::flatbuffers::VerifierTemplate &verifier) const { return VerifyTableStart(verifier) && VerifyField(verifier, VT_CODE, 4) && VerifyOffset(verifier, VT_MESSAGE) && verifier.VerifyString(message()) && + VerifyOffset(verifier, VT_FAMILY) && + verifier.VerifyString(family()) && verifier.EndTable(); } }; @@ -1491,6 +1497,9 @@ struct FatalBuilder { void add_message(::flatbuffers::Offset<::flatbuffers::String> message) { fbb_.AddOffset(Fatal::VT_MESSAGE, message); } + void add_family(::flatbuffers::Offset<::flatbuffers::String> family) { + fbb_.AddOffset(Fatal::VT_FAMILY, family); + } explicit FatalBuilder(::flatbuffers::FlatBufferBuilder &_fbb) : fbb_(_fbb) { start_ = fbb_.StartTable(); @@ -1505,8 +1514,10 @@ struct FatalBuilder { inline ::flatbuffers::Offset CreateFatal( ::flatbuffers::FlatBufferBuilder &_fbb, MobileGL::Wire::FatalCode code = MobileGL::Wire::FatalCode::None, - ::flatbuffers::Offset<::flatbuffers::String> message = 0) { + ::flatbuffers::Offset<::flatbuffers::String> message = 0, + ::flatbuffers::Offset<::flatbuffers::String> family = 0) { FatalBuilder builder_(_fbb); + builder_.add_family(family); builder_.add_message(message); builder_.add_code(code); return builder_.Finish(); @@ -1520,12 +1531,15 @@ struct Fatal::Traits { inline ::flatbuffers::Offset CreateFatalDirect( ::flatbuffers::FlatBufferBuilder &_fbb, MobileGL::Wire::FatalCode code = MobileGL::Wire::FatalCode::None, - const char *message = nullptr) { + const char *message = nullptr, + const char *family = nullptr) { auto message__ = message ? _fbb.CreateString(message) : 0; + auto family__ = family ? _fbb.CreateString(family) : 0; return MobileGL::Wire::CreateFatal( _fbb, code, - message__); + message__, + family__); } struct LogLine FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { diff --git a/MobileGL/MG_Remote/Protocol/protocol.fbs b/MobileGL/MG_Remote/Protocol/protocol.fbs index fe5429dc..46c7a8a6 100644 --- a/MobileGL/MG_Remote/Protocol/protocol.fbs +++ b/MobileGL/MG_Remote/Protocol/protocol.fbs @@ -264,6 +264,11 @@ enum FatalCode : uint { table Fatal { code: FatalCode; message: string; + // P6 dl (CONTRACT-P6 5.2, D2). The internal MGFatalFamily word, so a peer that receives this + // frame before the sender aborts can name WHICH family ended the session instead of reading a + // bare EOF. `code` stays the coarse seven-value projection; this carries the full vocabulary. + // Append-only: an older peer that does not read it still gets `code` and `message`. + family: string; } // Severity-graded per plan B section 8.2: <= Warn is lossy, >= Error is diff --git a/MobileGL/MG_Test/Wire/FatalFamilyTest.cpp b/MobileGL/MG_Test/Wire/FatalFamilyTest.cpp index 623a21a0..160286ae 100644 --- a/MobileGL/MG_Test/Wire/FatalFamilyTest.cpp +++ b/MobileGL/MG_Test/Wire/FatalFamilyTest.cpp @@ -9,7 +9,9 @@ // P6 `dl` (CONTRACT-P6 5.2). The Fatal-family table and its projection onto the wire FatalCode. #include +#include +#include #include #include @@ -81,3 +83,28 @@ TEST(FatalFamily, TheAnchorFamiliesProjectWhereTheirMeaningSays) { // An unmigrated verb is a protocol-level "cannot honour this", not a crash. EXPECT_EQ(FatalCodeForFamily(MGFatalFamily::UnmigratedVerb), FatalCode::ProtocolCorruption); } + +TEST(FatalFamily, TheSessionFaultFrameCarriesTheFamilyAndItsCode) { + // `dl` step three (5.2): the Fatal frame a dying session publishes carries the full family + // WORD beside the coarse code, so a peer names which family ended the session instead of + // reading a bare EOF. This is the wire round-trip that proves the field is there and that the + // code the frame carries is the family's own projection. + for (const MGFatalFamily family : kAllFamilies) { + ::flatbuffers::FlatBufferBuilder builder(256); + const auto fatal = ::MobileGL::Wire::CreateFatalDirect( + builder, FatalCodeForFamily(family), "detail line", FatalFamilyName(family)); + const auto root = ::MobileGL::Wire::CreateCtrlEnvelope( + builder, ::MobileGL::Wire::CtrlMsg::Fatal, fatal.Union()); + ::MobileGL::Wire::FinishCtrlEnvelopeBuffer(builder, root); + + ::flatbuffers::Verifier verifier(builder.GetBufferPointer(), builder.GetSize()); + ASSERT_TRUE(::MobileGL::Wire::VerifyCtrlEnvelopeBuffer(verifier)); + const auto* envelope = ::MobileGL::Wire::GetCtrlEnvelope(builder.GetBufferPointer()); + ASSERT_EQ(envelope->msg_type(), ::MobileGL::Wire::CtrlMsg::Fatal); + const auto* decoded = envelope->msg_as_Fatal(); + ASSERT_NE(decoded, nullptr); + ASSERT_NE(decoded->family(), nullptr) << "the family field did not survive the wire"; + EXPECT_STREQ(decoded->family()->c_str(), FatalFamilyName(family)); + EXPECT_EQ(decoded->code(), FatalCodeForFamily(family)); + } +}