From 96ab12b5abba62359fbbc3dc91b5fa886911dbea Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 23 Sep 2026 11:51:38 -0400 Subject: [PATCH] [MG_Remote, MG_Test] (P7): end a forfeited session whose peer keeps talking on control --- MobileGL/MG_Remote/Client/ClientSession.h | 7 ++-- MobileGL/MG_Remote/Server/ServerMain.cpp | 14 +++++--- .../MG_Test/Wire/EventForfeitPeerTest.cpp | 35 ++++++++++++++++++- 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/MobileGL/MG_Remote/Client/ClientSession.h b/MobileGL/MG_Remote/Client/ClientSession.h index 4c583b5a..af201bc5 100644 --- a/MobileGL/MG_Remote/Client/ClientSession.h +++ b/MobileGL/MG_Remote/Client/ClientSession.h @@ -94,9 +94,10 @@ namespace MobileGL::MG_Remote::Client { // PH-6's fuzz arm 3 ONLY (EventForfeitPeerTest): the control stream, so a peer that has // stopped draining can still put on it the frames no ClientSession path would send at that - // moment - a malformed one, a surface op queued behind a wait it did not drain for, or a - // half-close. Every production path that writes control holds m_remoteControlMutex and - // drains first; a caller of this does neither, which is the point. Null unless spawned. + // moment - a malformed one, a surface op queued behind a wait it did not drain for, a + // LogFlush every 100 ms, or a half-close. Every production path that writes control holds + // m_remoteControlMutex and drains first; a caller of this does neither, which is the + // point. Null unless spawned. Transport::SocketTransport* ControlSocketForTest() { return m_socketTransport.get(); } // CONTRACT-P6 4.3: THE PID THE SERVER STATED IN ITS Welcome, which is not the same fact diff --git a/MobileGL/MG_Remote/Server/ServerMain.cpp b/MobileGL/MG_Remote/Server/ServerMain.cpp index e2df8d5b..9e132c7d 100644 --- a/MobileGL/MG_Remote/Server/ServerMain.cpp +++ b/MobileGL/MG_Remote/Server/ServerMain.cpp @@ -369,18 +369,24 @@ void SendLogAck(void* pointer) { // later client Busy. Now each quiet interval asks whether the apply thread is still there; // when it is not (forfeit, or a dead data bell), the session takes the ordinary exit below: // Stop, Close, _exit(0). The interval is the supervisor's own accept poll. + // + // ASKED AT THE TOP OF EVERY TURN, NOT ONLY WHEN A WAIT TIMES OUT (PH-6 fix round). A peer that + // forfeited the reverse channel but keeps TALKING - a LogFlush or a surface op more often than + // every 250 ms, each of which this loop answers - never let ReceiveFrame time out, so the + // question was never asked and the session, with the supervisor's one slot, lived as long as + // the peer kept chattering. constexpr std::uint32_t kControlPollMs = 250; for (;;) { - std::uint64_t size = 0; - const auto result = control->ReceiveFrame({buffer.data(), buffer.size()}, &size, kControlPollMs); - if (result == MOBILEGL_ERR_TIMEOUT) { - if (loop.Running()) continue; + if (!loop.Running()) { WireLogError("MG_Remote server: pid=%d the apply thread has stopped (%s, %llu event(s) dropped); " "ending the session", selfPid, session.ReverseChannelForfeited() ? "ReverseChannelForfeit" : "its bell died", static_cast(session.ForfeitDrops())); break; } + std::uint64_t size = 0; + const auto result = control->ReceiveFrame({buffer.data(), buffer.size()}, &size, kControlPollMs); + if (result == MOBILEGL_ERR_TIMEOUT) continue; if (result == MOBILEGL_ERR_BUFFER_TOO_SMALL) { buffer.resize(static_cast(size)); continue; } if (result != MOBILEGL_OK) { // EOF or a transport error: the peer is gone. Said to the session BEFORE loop.Stop() diff --git a/MobileGL/MG_Test/Wire/EventForfeitPeerTest.cpp b/MobileGL/MG_Test/Wire/EventForfeitPeerTest.cpp index ca752e27..ce0a6866 100644 --- a/MobileGL/MG_Test/Wire/EventForfeitPeerTest.cpp +++ b/MobileGL/MG_Test/Wire/EventForfeitPeerTest.cpp @@ -29,7 +29,9 @@ // sends). Each plane runs in its own ctest entry under its own lane label. // // THE SCENARIOS (all but the first are the PH-6 fix round's): -// * the peer stops draining and stays - NotDraining after the knob; +// * the peer stops draining and stays - NotDraining after the knob - and keeps TALKING on +// control (a LogFlush every 100 ms), so the session must end without the control wait ever +// timing out; // * the peer is KILLED while the server waits, with a knob longer than ServerLoop::Stop()'s // 5000 ms join - PeerGone well inside the knob, exit 0 (whichever of the data bell's death and // the control EOF's stop reaches the waiting apply thread first); @@ -52,6 +54,7 @@ #include #include +#include #include #include #include @@ -80,6 +83,7 @@ #include #include #include +#include #include #include #include @@ -364,6 +368,20 @@ namespace { control->SendFrame(MobileGLByteSpan{builder.GetBufferPointer(), builder.GetSize()}) == MOBILEGL_OK; } + // A LogFlush that asks for an acknowledgement (ack=false is the REQUEST; the server's answer + // carries ack=true), exactly the frame ClientSession::SyncPeerLog sends - minus the drain that + // SyncPeerLog does first. + bool SendLogFlush(Client::ClientSession& client, std::uint64_t seq) { + flatbuffers::FlatBufferBuilder builder(64); + auto flush = ::MobileGL::Wire::CreateLogFlush(builder, seq, false); + auto envelope = + ::MobileGL::Wire::CreateCtrlEnvelope(builder, ::MobileGL::Wire::CtrlMsg::LogFlush, flush.Union()); + ::MobileGL::Wire::FinishCtrlEnvelopeBuffer(builder, envelope); + auto* control = client.ControlSocketForTest(); + return control != nullptr && + control->SendFrame(MobileGLByteSpan{builder.GetBufferPointer(), builder.GetSize()}) == MOBILEGL_OK; + } + // Four bytes: too short to carry a CtrlEnvelope, which ServerMain answers by ending the session // (P7 wave 0's named refusal) - a control stream that ENDS while the peer is still there. bool SendMalformedControlFrame(Client::ClientSession& client) { @@ -402,6 +420,21 @@ namespace { } if (!sent) ::_exit(kRawFrame); } + if (scenario == Scenario::StopsDraining) { + // SILENT ON SEG_EVENT, NOT ON CONTROL. A LogFlush every 100 ms, each of which the server + // answers (and this peer never reads): a peer that talks keeps the server's control + // wait from ever timing out, so the session may only end because its control loop asks + // about the apply thread on every turn and not only on a quiet one. Until the server has + // gone (a send fails) or the parent releases this peer. + std::uint64_t seq = 0x7f000000ull; + bool talking = true; + for (;;) { + struct pollfd wait{release, POLLIN, 0}; + if (::poll(&wait, 1, 100) != 0) break; + if (talking) talking = SendLogFlush(client, ++seq); + } + ::_exit(kChildOk); + } char go = 0; (void)::read(release, &go, 1); ::_exit(kChildOk);