From 5bb28140cbc24dddf3d4837efb338b948f63bca3 Mon Sep 17 00:00:00 2001 From: BZLZHH Date: Thu, 1 Oct 2026 10:31:20 -0400 Subject: [PATCH] [Feature] (Remote): the TCP supervisor serves sessions side by side A desktop is a compositor AND the GL programs run inside it, each a client with a session of its own. The TCP supervisor answered every authenticated Hello after the first with Refuse{Busy}, so with kwin connected any other program - eglinfo, glxinfo, glmark2 from konsole - waited out its Welcome and died on Fatal{CapsBeforeFirstSnapshot}. The fork shape now keeps a list of live children and forks for a Hello while fewer than eight are live. What made it a single slot was the data connection: a DataBind names its session only by the data nonce, which the child minted, so the supervisor could hand it to nothing but "the" child. The nonce is now minted by the supervisor just before fork(); the child adopts it (PresetDataNonceForThisProcess) instead of minting its own, and a DataBind goes to the child whose nonce it presents. Nothing on the wire changes. The thread shape (the in-process display server) still serves one session, Busy for a second as before. --- MobileGL/MG_IntegrationTest/CMakeLists.txt | 10 +-- MobileGL/MG_Remote/Server/ServerMain.cpp | 78 ++++++++++++++++++--- MobileGL/MG_Remote/Server/ServerSession.cpp | 22 +++++- MobileGL/MG_Remote/Server/ServerSession.h | 7 ++ 4 files changed, 101 insertions(+), 16 deletions(-) diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 8ce8a2df..7ec63f2d 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -3119,10 +3119,12 @@ if (MOBILEGL_BUILD_DISAGGREGATED) "MOBILEGL_IPC_LOG_FORWARD=1" "MOBILEGL_IPC_TOKEN=" "MGITEST_TCP_LANE=1" "MOBILEGL_ITEST_REQUIRE_GPU=1") set(mglD2Extra FIXTURES_REQUIRED mobilegl-tcp) - # The loopback supervisor serves ONE session (ServerMain.cpp answers a second Hello - # Refuse{Busy}), and every other Tcp entry holds this lock. Without it `ctest -L - # integration-gpu -j N` ran this case beside another Tcp case and whichever client - # connected second died Refuse{Busy} -> Fatal{CapsBeforeFirstSnapshot}. + # The loopback supervisor used to serve ONE session (a second Hello was answered + # Refuse{Busy}), and `ctest -L integration-gpu -j N` ran this case beside another Tcp + # case so that whichever client connected second died Refuse{Busy} -> + # Fatal{CapsBeforeFirstSnapshot}. It serves several side by side now (ServerMain.cpp + # TcpSupervisor, fork shape); every Tcp entry still holds this lock, so the cases do not + # share the fixture's one server and its log while they run. # scripts/ci/spawn_lane_parity.py and junit_tally.py --discovery now refuse any entry # on the fixture that does not hold it. set(mglD2Locks "${mglD2LogName}\;mobilegl-tcp") diff --git a/MobileGL/MG_Remote/Server/ServerMain.cpp b/MobileGL/MG_Remote/Server/ServerMain.cpp index b3a38187..9668d9c5 100644 --- a/MobileGL/MG_Remote/Server/ServerMain.cpp +++ b/MobileGL/MG_Remote/Server/ServerMain.cpp @@ -1035,12 +1035,32 @@ private: }; bool Reap() { - const bool reaped = m_handoffShape == Handoff::Thread ? ReapSessionThread() - : ReapActiveSession(m_active, m_sessionsFaulted); - if (m_active <= 0 && m_handoff >= 0) { ::close(m_handoff); m_handoff = -1; } + if (m_handoffShape == Handoff::Thread) { + const bool reaped = ReapSessionThread(); + if (m_active <= 0 && m_handoff >= 0) { ::close(m_handoff); m_handoff = -1; } + return reaped; + } + // The fork shape: every live child is asked, and a reaped one takes its hand-off with it. + bool reaped = false; + for (auto child = m_children.begin(); child != m_children.end();) { + if (ReapActiveSession(child->pid, m_sessionsFaulted)) { + if (child->handoff >= 0) ::close(child->handoff); + child = m_children.erase(child); + reaped = true; + } else { + ++child; + } + } + m_active = m_children.empty() ? -1 : m_children.back().pid; return reaped; } + // Whether an authenticated Hello has to be told Busy: the thread shape serves one session, the + // fork shape up to kMaxForkedSessions side by side. + bool SessionsFull() const { + return m_handoffShape == Handoff::Thread ? m_active > 0 : m_children.size() >= kMaxForkedSessions; + } + // P12 (D5): the thread shape's reap - the same line a forked child's reap prints, and the same // count, then the latch put back for the next session (ResetSessionLatch: the thread took no // process with it, so the latch it armed and maybe latched is still this process's). @@ -1082,7 +1102,7 @@ private: // make the loop wait here; a DataBind's routing takes one look (Judge, fix round). void WaitOutAnExitingSession() { const auto reapDeadline = Clock::now() + std::chrono::milliseconds(50); - while (m_active > 0 && Clock::now() < reapDeadline) { + while (SessionsFull() && Clock::now() < reapDeadline) { if (Reap()) break; ::usleep(1000); } @@ -1176,7 +1196,15 @@ private: // either has cut its hand-off already (the send below fails and is refused by name) or // refuses what is queued on its way out (ExitBeforeAccept / CloseHandoffRefusingQueued). Reap(); - if (m_active <= 0) { + // WHICH SESSION IT BELONGS TO. The thread shape has one; the fork shape knows every + // child's nonce (it minted them, Fork) and hands the connection to the child it names. + int handoff = m_active > 0 ? m_handoff : -1; + if (m_handoffShape == Handoff::Fork) { + handoff = -1; + for (const auto& child : m_children) + if (ConstantTimeNonceMatch(child.nonce, nonce)) handoff = child.handoff; + } + if (handoff < 0) { // StreamLink's frames may already follow the DataBind; read them away so the close // does not become an RST that discards the refusal. DrainUnread(fd); @@ -1186,7 +1214,7 @@ private: // The send is MSG_DONTWAIT and the child cuts its end the moment its own data // connection is bound (CloseHandoffRefusingQueued), so this either queues the // descriptor for a child that will read or refuse it, or fails and is refused here. - if (m_handoff >= 0 && FdPassing::SendFd(m_handoff, fd, MobileGLByteSpan{nonce, sizeof(nonce)}, + if (FdPassing::SendFd(handoff, fd, MobileGLByteSpan{nonce, sizeof(nonce)}, /*dontWait=*/true) == MOBILEGL_OK) { // The child holds its own descriptor now; ours goes without a shutdown(2), which // would disconnect the child's too. @@ -1217,9 +1245,11 @@ private: } m_backoff.NoteSuccess(peer.address); WaitOutAnExitingSession(); - if (m_active > 0) { + if (SessionsFull()) { // The frame is consumed, so closing cannot RST the refusal away. - Refuse(*connection, Protocol::RefuseCode::Busy, "one session is already active"); + Refuse(*connection, Protocol::RefuseCode::Busy, + m_handoffShape == Handoff::Thread ? "one session is already active" + : "the session limit is reached"); return; } Fork(std::move(connection), payload); @@ -1235,12 +1265,25 @@ private: StartSessionThread(std::move(connection), hello, pair); return; } + // The child's data nonce, minted HERE so this supervisor can route the child's DataBind to it + // among other live sessions; the child adopts it instead of minting its own. + LiveChild live; + if (SocketTransport::MintNonce(live.nonce, sizeof(live.nonce)) != MOBILEGL_OK) { + ::close(pair[0]); + ::close(pair[1]); + Refuse(*connection, Protocol::RefuseCode::Busy, "could not mint the session's data nonce"); + return; + } std::fflush(nullptr); const pid_t supervisor = ::getpid(); const pid_t child = ::fork(); if (child == 0) { // P12 (D8): the child dies with this supervisor, not after its client has gone. DieWithSupervisor(supervisor); + Server::PresetDataNonceForThisProcess(live.nonce, sizeof(live.nonce)); + // The other live sessions' hand-offs are the supervisor's. + for (const auto& other : m_children) + if (other.handoff >= 0) ::close(other.handoff); // The child owns this one connection and its end of the hand-off. Every OTHER pending // peer's descriptor is closed here, or the supervisor's refusal of that peer would not // end its connection while this session lives. @@ -1260,9 +1303,11 @@ private: Refuse(*connection, Protocol::RefuseCode::Busy, "could not create session child"); return; } + live.pid = child; + live.handoff = pair[0]; + m_children.push_back(live); m_active = child; ::close(pair[1]); - m_handoff = pair[0]; connection->CloseLocalCopy(); } @@ -1310,10 +1355,21 @@ private: std::atomic m_sessionDone{false}; std::atomic m_sessionExit{0}; unsigned long m_sessionOrdinal = 0; + // A live session: the newest child's pid for the fork shape, this process's for the thread shape. pid_t m_active = -1; - // The supervisor's end of the live child's data-connection hand-off (PH-7 (4)); -1 while no - // session is live. It goes with the child: a reaped session can be handed nothing. + // The thread shape's end of its session's data-connection hand-off (PH-7 (4)); -1 while no + // session is live. It goes with the session: a reaped session can be handed nothing. int m_handoff = -1; + // THE FORK SHAPE'S LIVE SESSIONS, side by side: a desktop is a compositor AND the GL programs + // run inside it, each its own client with its own session. Each child keeps its end of the + // hand-off and the data nonce this supervisor minted for it, which is how a DataBind finds it. + struct LiveChild { + pid_t pid = -1; + int handoff = -1; + std::uint8_t nonce[kDataNonceBytes] = {}; + }; + static constexpr std::size_t kMaxForkedSessions = 8; + std::vector m_children; std::vector m_pending; std::vector m_ready; // Accepting resumes at this time after accept(2) ran out of descriptors or memory. diff --git a/MobileGL/MG_Remote/Server/ServerSession.cpp b/MobileGL/MG_Remote/Server/ServerSession.cpp index faa7ac28..95691893 100644 --- a/MobileGL/MG_Remote/Server/ServerSession.cpp +++ b/MobileGL/MG_Remote/Server/ServerSession.cpp @@ -42,6 +42,24 @@ #endif namespace MobileGL::MG_Remote::Server { + namespace { + Uint8 g_presetDataNonce[Transport::kDataNonceBytes] = {}; + Bool g_presetDataNonceSet = false; + + Bool TakePresetDataNonce(Uint8* out, SizeT size) { + if (!g_presetDataNonceSet || size != sizeof(g_presetDataNonce)) return false; + std::memcpy(out, g_presetDataNonce, size); + g_presetDataNonceSet = false; + return true; + } + } // namespace + + void PresetDataNonceForThisProcess(const Uint8* nonce, SizeT size) { + if (nonce == nullptr || size != sizeof(g_presetDataNonce)) return; + std::memcpy(g_presetDataNonce, nonce, size); + g_presetDataNonceSet = true; + } + namespace { // F1 (P7 wave 2). THE ONLY KNOB THAT CAN MAKE THE FIRST CapsSnapshot LATE. @@ -716,7 +734,9 @@ namespace MobileGL::MG_Remote::Server { if (socket == nullptr || !socket->IsTcp() || !m_dataSource) return RefuseHandshake(transport, ::MobileGL::Wire::RefuseCode::LinkTerms, "stream needs a TCP control connection and a data-connection source"); - created = Transport::SocketTransport::MintNonce(dataNonce, sizeof(dataNonce)); + created = TakePresetDataNonce(dataNonce, sizeof(dataNonce)) + ? MOBILEGL_OK + : Transport::SocketTransport::MintNonce(dataNonce, sizeof(dataNonce)); if (created != MOBILEGL_OK) { // (F fix round) The peer is told by name instead of seeing a bare close. The // return value stays the fault it is - exit 67 in ServerMain, counted in diff --git a/MobileGL/MG_Remote/Server/ServerSession.h b/MobileGL/MG_Remote/Server/ServerSession.h index b11963af..c47131fa 100644 --- a/MobileGL/MG_Remote/Server/ServerSession.h +++ b/MobileGL/MG_Remote/Server/ServerSession.h @@ -63,6 +63,13 @@ namespace MobileGL::MG_Remote::Server { + // THE DATA NONCE A FORKED SESSION CHILD IS BORN WITH. The TCP supervisor serves sessions side by + // side and routes each data connection (DataBind) to the child whose nonce it presents, so it has + // to know every child's nonce: it mints the nonce itself just before fork() and the child adopts + // it here instead of minting its own. One use: the session that takes it clears it. Never set in + // a process that runs more than one session, so the shared storage is the child's alone. + void PresetDataNonceForThisProcess(const Uint8* nonce, SizeT size); + class ServerSession { public: static ServerSession* Active();