[MG_Remote, MG_Test] (P7): end a forfeited session whose peer keeps talking on control

This commit is contained in:
2026-09-23 13:44:13 -04:00
parent db4943601f
commit 96ab12b5ab
3 changed files with 48 additions and 8 deletions
+4 -3
View File
@@ -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
+10 -4
View File
@@ -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<unsigned long long>(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<std::size_t>(size)); continue; }
if (result != MOBILEGL_OK) {
// EOF or a transport error: the peer is gone. Said to the session BEFORE loop.Stop()
+34 -1
View File
@@ -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 <MG_Remote/Client/ClientSession.h>
#include <MG_Remote/Protocol/SurfaceOpCodec.h>
#include <MG_Remote/Protocol/generated/protocol_generated.h>
#include <MG_Remote/Server/ServerLoop.h>
#include <MG_Remote/Server/ServerSpawn.h>
#include <MG_Remote/Server/SurfaceControlFrame.h>
@@ -80,6 +83,7 @@
#include <csignal>
#include <fcntl.h>
#include <netinet/in.h>
#include <poll.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>
@@ -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);