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();