diff --git a/MobileGL/MG_Remote/FatalFunnel.cpp b/MobileGL/MG_Remote/FatalFunnel.cpp index c5efd76a..c7cb0ef6 100644 --- a/MobileGL/MG_Remote/FatalFunnel.cpp +++ b/MobileGL/MG_Remote/FatalFunnel.cpp @@ -106,6 +106,15 @@ namespace MobileGL::MG_Remote { void ArmSessionLatch() { g_latchArmed.store(true, std::memory_order_release); } + void ResetSessionLatch() { + const std::lock_guard lock(g_latchMutex); + g_latchArmed.store(false, std::memory_order_release); + g_latchCount.store(0, std::memory_order_relaxed); + g_latchedFamily = MGFatalFamily::ProtocolCorruption; + g_latchedLine[0] = '\0'; + g_latched.store(false, std::memory_order_release); + } + bool SessionLatchArmed() { return g_latchArmed.load(std::memory_order_acquire); } bool SessionLatched() { return g_latched.load(std::memory_order_acquire); } diff --git a/MobileGL/MG_Remote/FatalFunnel.h b/MobileGL/MG_Remote/FatalFunnel.h index 41b709a9..0f89bb75 100644 --- a/MobileGL/MG_Remote/FatalFunnel.h +++ b/MobileGL/MG_Remote/FatalFunnel.h @@ -77,6 +77,15 @@ namespace MobileGL::MG_Remote { // SessionFail calls: the string must carry its `Fatal{Word` and the word must have a .def row. void ArmSessionLatch(); bool SessionLatchArmed(); + // P12 (D5): THE IN-PROCESS DISPLAY SERVER RUNS SESSIONS ONE AFTER ANOTHER IN ONE PROCESS, so + // the latch that a session child took to its _exit has to be put back for the next session: + // disarmed, unlatched, the count and the first fault's line cleared. Called between sessions + // (the in-process supervisor, after it has joined the session thread) and by nothing else; the + // next RunSession arms it again. SessionFaultCount() is the process's telemetry and is kept. + // Between sessions the latch is unarmed, so a fault there is a SessionFail - which in the + // in-process shape takes the display Activity's process down with it: the crash isolation a + // forked session child gave is what running in-process gives up (documented, D5). + void ResetSessionLatch(); // True once the first fault has latched. One acquire load: DrainRing asks it before every pop. bool SessionLatched(); // The first latched fault - its family and its line, verbatim. Meaningful only once diff --git a/MobileGL/MG_Remote/Server/InProcessServer.h b/MobileGL/MG_Remote/Server/InProcessServer.h new file mode 100644 index 00000000..a133136b --- /dev/null +++ b/MobileGL/MG_Remote/Server/InProcessServer.h @@ -0,0 +1,48 @@ +// MobileGL - MobileGL/MG_Remote/Server/InProcessServer.h +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +// P12 (on-screen server window), D5. THE IN-PROCESS DISPLAY SERVER'S ENTRY POINTS. +// +// The on-screen server cannot be the exec'd supervisor that forks a child per session: the window +// it renders into (the display Activity's SurfaceView) is an ANativeWindow in the APP's process, and +// neither exec nor fork carries one. So the Activity's own process serves: a thread it starts calls +// mobilegl_server_serve_inprocess, which is `libMobileGLServer.so tcp://... --serve` with THREADS +// instead of fork - the same pre-auth, the same Busy for an authenticated second Hello while a +// session is live, one session at a time, each session a thread of this process, the latch reset +// between sessions and the backend pinned for the process lifetime. Crash isolation is what it +// gives up: a session's abort (an unarmed SessionFail) takes this process - the Activity - with it. +// +// PRECONDITIONS, the same as mobilegl_server_main's, set in the environment BEFORE libMobileGL is +// loaded (Os.setenv from Java): MOBILEGL_IPC_DIAL=no (else 64), none of MOBILEGL_TRANSPORT / +// MOBILEGL_IPC_SERVER_PATH / MOBILEGL_IPC_RING_MB / MOBILEGL_IPC_STAGE_MB (else 65), +// MOBILEGL_IPC_ROLE=server (forced if missing, as the supervisor does), MOBILEGL_IPC_TOKEN (required +// for a non-loopback listen; >= the minimum length), optionally MOBILEGL_BACKEND_TYPE (the pin), +// MOBILEGL_IPC_LOG_FORWARD, MOBILEGL_LOG_FILE_PATH. +// +// THE DISPLAY is separate: the JNI glue installs ServerDisplayInstance() (ServerDisplay.h) and +// attaches/detaches the window from the SurfaceHolder callbacks. A server with no display installed +// serves offscreen sessions only and refuses a ServerOwned surface by name. + +#pragma once + +extern "C" { + +// Serves `endpoint` (tcp://host:port only) on the CALLING thread until mobilegl_server_stop_inprocess +// is called and the live session (if any) has ended, then returns 0. Other returns, all logged by +// name: 64 / 65 (the environment preconditions above), 71 (not a tcp:// endpoint), 72 (could not +// listen - EADDRINUSE is retried for up to ~5 s first, the previous server may still be dying), 73 +// (the listener failed), 74 (a server is already serving in this process). Re-callable after it +// returned. +__attribute__((visibility("default"))) int mobilegl_server_serve_inprocess(const char* endpoint); + +// Asks the in-process server to stop and returns at once (it does not wait: call it from a UI +// thread). The live session ends within one control slice (~100 ms) - or when the op it is running +// returns (a ServerOwned wait for a window is interrupted too) - and then serve returns 0. +__attribute__((visibility("default"))) void mobilegl_server_stop_inprocess(void); + +} // extern "C" diff --git a/MobileGL/MG_Remote/Server/ServerMain.cpp b/MobileGL/MG_Remote/Server/ServerMain.cpp index f61c9b74..b2776dfb 100644 --- a/MobileGL/MG_Remote/Server/ServerMain.cpp +++ b/MobileGL/MG_Remote/Server/ServerMain.cpp @@ -7,7 +7,9 @@ #include "../Protocol/SurfaceOpCodec.h" #include "../Handshake.h" #include "../FatalFunnel.h" +#include "InProcessServer.h" #include "PreAuthGate.h" +#include "ServerDisplay.h" #include "ServerLoop.h" #include "ServerSession.h" #include "SurfaceControlFrame.h" @@ -25,14 +27,19 @@ #include #include #include +#include #include #include +#include #include #include #include #include #include #include +#if defined(__linux__) +#include // PR_SET_PDEATHSIG (P12 D8); Android defines __linux__ as well +#endif namespace { using namespace MobileGL::MG_Remote; @@ -232,12 +239,50 @@ void CloseHandoffRefusingQueued(int handoff) { // exit drops queued descriptors exactly as a close does: those peers saw their connections end // with no frame. On any other source (the single-session listener, the unix endpoint's none) there // is nothing queued to refuse, and this is the `fflush` + `_exit` it replaces. -[[noreturn]] void ExitBeforeAccept(int sourceFd, DataSource sourceKind, int code) { +// +// P12 (D5): RunSession RETURNS its exit code now instead of `_exit`ing, so the in-process display +// server can run one session after another on a thread. This is the part of the old +// ExitBeforeAccept that belongs to the session; the `fflush` + `_exit` belong to the process and +// moved to the fork sites, which are `ExitSessionProcess(RunSession(...))` - the forked shapes end +// exactly as before. +int LeaveBeforeAccept(int sourceFd, DataSource sourceKind, int code) { if (sourceFd >= 0 && sourceKind == DataSource::Handoff) CloseHandoffRefusingQueued(sourceFd); + return code; +} + +// P12 (D5). A forked session child (and the single-session shapes, whose process IS the session) +// ends here, with RunSession's code, exactly as RunSession's own `_exit`s used to end it. +[[noreturn]] void ExitSessionProcess(int code) { std::fflush(nullptr); ::_exit(code); } +// P12 (D8). A FORKED SESSION CHILD DIES WITH ITS SUPERVISOR. The child used to outlive a supervisor +// that was killed (MobileGLServerService.onDestroy destroys only the supervisor), holding its client +// and its GPU context until that client went away (notes/p65/code-review-findings.md:9). With the +// parent-death signal the kernel SIGKILLs it the moment the supervisor goes - and the getppid() +// check closes the race of a supervisor that died between fork() and the prctl. Linux (and Android) +// only; the signal fires when the THREAD that forked exits, and both fork sites run on the +// supervisor's main thread, which is the process. +void DieWithSupervisor(pid_t supervisor) { +#if defined(__linux__) + (void)::prctl(PR_SET_PDEATHSIG, SIGKILL); + if (::getppid() != supervisor) ::_exit(0); +#else + (void)supervisor; +#endif +} + +// P12 (D5): THE IN-PROCESS DISPLAY SERVER'S PROCESS-WIDE STATE. One server per process, sessions +// one at a time (TcpSupervisor's thread hand-off). `g_inProcessStop` is raised by +// mobilegl_server_stop_inprocess and read by the supervisor's loop and by a live session's control +// loop; `g_inProcessBackendPin` is the backend the first session chose when no MOBILEGL_BACKEND_TYPE +// pins one - Espryt and Magma never share a process (BenchService.java's rule), so a later session +// asking for the other is refused like a pinned-backend mismatch. +std::atomic g_inProcessServing{false}; +std::atomic g_inProcessStop{false}; +std::atomic g_inProcessBackendPin{-1}; + // A TEST-ONLY lever (f2-auth fix round, ID-P7-44): MOBILEGL_TEST_DELAY_SESSION_HANDSHAKE_MS holds a // session child for that many milliseconds before it looks at its Hello, so a control can queue // DataBinds on the hand-off of a child that is about to refuse that Hello - a window of @@ -265,9 +310,18 @@ void DelaySessionHandshakeForTest() { // `helloFrame` is the first frame when somebody has already read and authenticated it - the TCP // supervisor always has (PH-7 (5)) - and empty when this function reads it itself, within // `helloWaitMs`. -[[noreturn]] void RunSession(std::unique_ptr control, std::vector helloFrame, - Server::ServerSession::DataConnectionSource dataSource, int sourceFd, - DataSource sourceKind, std::uint32_t helloWaitMs) { +// +// P12 (D5): IT RETURNS THE SESSION'S EXIT CODE, and never exits. A forked child and a +// single-session process pass the code straight to ExitSessionProcess - so every one of those shapes +// ends with the `_exit` code it always had - while `inProcess` (the in-process display server's +// session thread) returns it to the supervisor thread and the process lives on. What a process exit +// used to clean up for free, `inProcess` cleans up by hand on every way out: the backend (the loop's +// Stop drops one no thread owns), the session singleton, the stats window, the log forwarder and the +// progress sink, both of which point at `control`. The latch is reset by the supervisor between +// sessions (ResetSessionLatch). +int RunSession(std::unique_ptr control, std::vector helloFrame, + Server::ServerSession::DataConnectionSource dataSource, int sourceFd, DataSource sourceKind, + std::uint32_t helloWaitMs, bool inProcess = false) { const int selfPid = static_cast(::getpid()); const bool tcp = control->IsTcp(); if (helloFrame.empty()) { @@ -278,13 +332,13 @@ void DelaySessionHandshakeForTest() { const auto received = control->ReceiveFrame({nullptr, 0}, &bytes, helloWaitMs); if (received == MOBILEGL_ERR_TIMEOUT) { Refuse(*control, Protocol::RefuseCode::Authentication, Server::kPreAuthDeadline); - ExitBeforeAccept(sourceFd, sourceKind, 0); + return LeaveBeforeAccept(sourceFd, sourceKind, 0); } if (received == MOBILEGL_ERR_PROTOCOL_MISMATCH || (received == MOBILEGL_ERR_BUFFER_TOO_SMALL && bytes > Server::kPreAuthFirstFrameMaxBytes)) { DrainUnread(control->StreamFd()); Refuse(*control, Protocol::RefuseCode::MalformedHello, Server::kFirstFrameNotAFrame); - ExitBeforeAccept(sourceFd, sourceKind, 0); + return LeaveBeforeAccept(sourceFd, sourceKind, 0); } if (received == MOBILEGL_ERR_TRANSPORT_CLOSED) { // Fix round: this used to be a bare `_exit(0)` whatever had arrived. A close INSIDE @@ -295,7 +349,7 @@ void DelaySessionHandshakeForTest() { Refuse(*control, Protocol::RefuseCode::MalformedHello, Server::kFirstFrameTruncated); else WireLogError("MG_Remote server: pid=%d control peer closed before sending a first frame", selfPid); - ExitBeforeAccept(sourceFd, sourceKind, 0); + return LeaveBeforeAccept(sourceFd, sourceKind, 0); } if (received == MOBILEGL_ERR_BUFFER_TOO_SMALL) { // The reassembler holds the whole frame already; this second call copies it out. @@ -303,12 +357,12 @@ void DelaySessionHandshakeForTest() { if (control->ReceiveFrame({helloFrame.data(), helloFrame.size()}, &bytes, 0) != MOBILEGL_OK) { WireLogError("MG_Remote server: pid=%d first frame of %llu bytes could not be copied out", selfPid, static_cast(helloFrame.size())); - ExitBeforeAccept(sourceFd, sourceKind, 0); + return LeaveBeforeAccept(sourceFd, sourceKind, 0); } } else if (received != MOBILEGL_OK) { WireLogError("MG_Remote server: pid=%d first frame could not be read (rc=%d)", selfPid, static_cast(received)); - ExitBeforeAccept(sourceFd, sourceKind, 0); + return LeaveBeforeAccept(sourceFd, sourceKind, 0); } // MOBILEGL_OK with nothing copied is a ZERO-LENGTH frame. It was the one complete frame // this read exited on without a word (fix round); `helloFrame` stays empty and the @@ -327,11 +381,11 @@ void DelaySessionHandshakeForTest() { // arm is the single-session shape's.) if (shape == Server::FirstFrameShape::DataBind) { Refuse(*control, Protocol::RefuseCode::Authentication, "data connection names no live session"); - ExitBeforeAccept(sourceFd, sourceKind, 0); + return LeaveBeforeAccept(sourceFd, sourceKind, 0); } if (shape != Server::FirstFrameShape::Hello) { Refuse(*control, Protocol::RefuseCode::MalformedHello, Server::FirstFrameRefusalDetail(shape)); - ExitBeforeAccept(sourceFd, sourceKind, 0); + return LeaveBeforeAccept(sourceFd, sourceKind, 0); } const auto* hello = Protocol::GetCtrlEnvelope(helloFrame.data())->msg_as_Hello(); // PH-7 (1). The SAME policy ServerSession::Accept applies, from Handshake.h, so the @@ -348,11 +402,11 @@ void DelaySessionHandshakeForTest() { // this build's stamp. An unauthenticated peer now learns only Refuse{Authentication}. On TCP // --serve the supervisor has already asked (the answer is the same; it is asked again so this // function holds the rule on its own for the unix and single-session shapes). - if (AuthenticatePeerToken(*control, hello->token()) != MOBILEGL_OK) ExitBeforeAccept(sourceFd, sourceKind, 0); + if (AuthenticatePeerToken(*control, hello->token()) != MOBILEGL_OK) return LeaveBeforeAccept(sourceFd, sourceKind, 0); // Reject incompatible wire before backend bring-up can obscure the cause. if (ValidatePeerHandshake(*control, hello->abiMajor(), hello->abiMinor(), hello->wireFingerprint(), hello->buildFingerprint() ? hello->buildFingerprint()->c_str() : nullptr, - hello->dialMode()) != MOBILEGL_OK) ExitBeforeAccept(sourceFd, sourceKind, 0); + hello->dialMode()) != MOBILEGL_OK) return LeaveBeforeAccept(sourceFd, sourceKind, 0); MobileGL::MG_ConfigLoader::Init(); MobileGL::MG_Config::Transport = MobileGL::MG_Config::TransportMode::Spawn; MobileGL::MG_Pipe::MGPipeSetServerProcessRole(true); @@ -360,32 +414,79 @@ void DelaySessionHandshakeForTest() { // it cleanly instead of aborting it (FatalFunnel.h's latch block). Armed here and nowhere // else: inproc keeps every Fatal (client and server share that process), and the supervisor // that forked this child never applies a record. + // + // P12 (D5): and the in-process display server arms it for EVERY session, here, the same way; + // its supervisor resets it between sessions (ResetSessionLatch), so a session that latched does + // not decline the next one. ArmSessionLatch(); // The child bypasses MobileGL::Initialize: arm its own role counters. MobileGL::MG_Util::PipeStats::Init(); + auto& session = Server::ServerSessionInstance(); + auto& loop = Server::ServerLoopInstance(); + // P12 (D5): WHAT A PROCESS EXIT CLEANED UP FOR FREE, THE IN-PROCESS SHAPE CLEANS UP BY HAND, + // on every way out from here - so the next session in this process starts where a fresh + // session child would. A no-op for every forked / single-session shape: those `_exit` with the + // code, exactly as before. + bool backendBuilt = false; + bool acceptTried = false; + const auto endSession = [&](int code) -> int { + if (!inProcess) return code; + // Both point at `control`, which dies with this frame. + loop.SetControlProgressSink(nullptr, nullptr); + MobileGL::MG_Util::Debug::SetLogForwarder(nullptr, nullptr); + if (backendBuilt) { + // Stop() joins a running apply thread (it destroys the backend on it), or - never + // started - drops the backend here; either way the display lease goes with it. + loop.Stop(); + session.SetBackend(nullptr); + } + if (acceptTried) session.Close(); + MobileGL::MG_Util::PipeStats::Shutdown(); + return code; + }; const auto backend = static_cast(hello->backendType()); if (backend != MobileGL::BackendType::DirectGLES && backend != MobileGL::BackendType::DirectVulkan) { Refuse(*control, Protocol::RefuseCode::Backend, "unsupported Hello.backendType"); - ExitBeforeAccept(sourceFd, sourceKind, 0); + return endSession(LeaveBeforeAccept(sourceFd, sourceKind, 0)); } const char* pinned = std::getenv("MOBILEGL_BACKEND_TYPE"); if (pinned && *pinned && backend != MobileGL::MG_Config::ActiveBackendType) { Refuse(*control, Protocol::RefuseCode::Backend, "Hello.backendType disagrees with pinned backend"); - ExitBeforeAccept(sourceFd, sourceKind, 0); + return endSession(LeaveBeforeAccept(sourceFd, sourceKind, 0)); + } + if (inProcess) { + // P12 (D5): THE BACKEND IS PINNED FOR THE PROCESS'S LIFETIME. With no MOBILEGL_BACKEND_TYPE + // the first session's backend becomes the pin: Espryt and Magma must never share a process + // (BenchService.java:12-17), and here every session shares this one. The refusal is the + // pinned-backend refusal above, word for word. + int pin = g_inProcessBackendPin.load(std::memory_order_acquire); + if (pin < 0 && g_inProcessBackendPin.compare_exchange_strong(pin, static_cast(backend), + std::memory_order_acq_rel)) { + WireLogError("MG_Remote server: pid=%d in-process display server pinned backend type %u for the " + "process lifetime (the first session's; MOBILEGL_BACKEND_TYPE unset)", + selfPid, static_cast(backend)); + pin = static_cast(backend); + } + if (pin != static_cast(backend)) { + Refuse(*control, Protocol::RefuseCode::Backend, "Hello.backendType disagrees with pinned backend"); + return endSession(LeaveBeforeAccept(sourceFd, sourceKind, 0)); + } } MobileGL::MG_Config::ActiveBackendType = backend; - if (!MobileGL::MG_Backend::InitServerRoleForSpawn()) ExitBeforeAccept(sourceFd, sourceKind, 66); - auto& session = Server::ServerSessionInstance(); + backendBuilt = true; // InitServerRoleForSpawn may build it and then fail: Stop() drops either way + if (!MobileGL::MG_Backend::InitServerRoleForSpawn()) return endSession(LeaveBeforeAccept(sourceFd, sourceKind, 66)); int serverBell[2] = {-1, -1}, clientBell[2] = {-1, -1}; std::unique_ptr selfBell, peerBell; if (!tcp) { if (::socketpair(AF_UNIX, SOCK_STREAM, 0, serverBell) != 0 || - ::socketpair(AF_UNIX, SOCK_STREAM, 0, clientBell) != 0) ExitBeforeAccept(sourceFd, sourceKind, 74); + ::socketpair(AF_UNIX, SOCK_STREAM, 0, clientBell) != 0) + return endSession(LeaveBeforeAccept(sourceFd, sourceKind, 74)); selfBell = std::make_unique(serverBell[0], serverBell[1], 1, true); peerBell = std::make_unique(-1, clientBell[1], 1, true); session.SetExternalDoorbells(selfBell.get(), peerBell.get()); } if (tcp) session.SetDataConnectionSource(std::move(dataSource)); + acceptTried = true; const auto accepted = session.Accept(*control, &helloFrame); // THE SOURCE IS DONE WHEN ACCEPT IS, whichever way Accept went (F fix round). Under --serve // this is the child's end of the hand-off pair: closing it turns every later DataBind the @@ -406,10 +507,10 @@ void DelaySessionHandshakeForTest() { } if (accepted != MOBILEGL_OK) { WireLogError("MG_Remote server: pid=%d Accept failed (rc=%d)", selfPid, static_cast(accepted)); - std::fflush(nullptr); // A refusal and a peer that went away before its data connection bound (PH-7 (4): the // client closed after Welcome) are outcomes, not faults; the supervisor counts only 67. - ::_exit(accepted == MOBILEGL_ERR_PROTOCOL_MISMATCH || accepted == MOBILEGL_ERR_TRANSPORT_CLOSED ? 0 : 67); + return endSession(accepted == MOBILEGL_ERR_PROTOCOL_MISMATCH || accepted == MOBILEGL_ERR_TRANSPORT_CLOSED ? 0 + : 67); } if (!tcp) { @@ -425,16 +526,14 @@ void DelaySessionHandshakeForTest() { if (fd < 0) { std::fprintf(stderr, "MG_Remote server: pid=%d segment %u has no descriptor\n", selfPid, tag); - std::fflush(nullptr); - ::_exit(68); + return endSession(68); } Sideband sideband{tag, session.Shm().AnnouncedSize(slot)}; if (control->ShareFd(fd, MobileGLByteSpan{&sideband, sizeof(sideband)}) != MOBILEGL_OK) { std::fprintf(stderr, "MG_Remote server: pid=%d could not share segment %u\n", selfPid, tag); - std::fflush(nullptr); - ::_exit(69); + return endSession(69); } } // SLOTS 4, 5 AND 6 ARE THE BELLS - three, not two, and the third is the @@ -451,8 +550,7 @@ void DelaySessionHandshakeForTest() { MOBILEGL_OK) { std::fprintf(stderr, "MG_Remote server: pid=%d could not share bell %u\n", selfPid, index); - std::fflush(nullptr); - ::_exit(69); + return endSession(69); } } // clientBell[0] is the CLIENT's park end and no bell here owns it; the @@ -474,12 +572,11 @@ void DelaySessionHandshakeForTest() { const char* forwarding = std::getenv("MOBILEGL_IPC_LOG_FORWARD"); if (tcp && (!forwarding || std::strcmp(forwarding, "0") != 0)) MobileGL::MG_Util::Debug::SetLogForwarder(&ForwardLog, control.get()); - auto& loop = Server::ServerLoopInstance(); // p7/spawnhang: this thread posts every surface op the client sends (ServerApplyWireSurfaceOp, // below) and is the one that says "still running" while the apply thread runs it. Cleared // after the loop, on this same thread, so no post can be in flight when it goes. loop.SetControlProgressSink(&SendSurfaceProgress, control.get()); - if (loop.Start(session) != MOBILEGL_OK) ::_exit(70); + if (loop.Start(session) != MOBILEGL_OK) return endSession(70); WireLogError("MG_Remote server: pid=%d transport=spawn role=server ready control=%s data=%s", selfPid, tcp ? "tcp" : "unix", tcp ? "stream" : "shm"); std::vector buffer(64 * 1024); @@ -510,6 +607,13 @@ void DelaySessionHandshakeForTest() { constexpr std::uint32_t kControlSliceMs = 100; for (;;) { if (SessionLatched()) break; + // P12 (D5): the in-process display server is stopping (mobilegl_server_stop_inprocess). + // The session ends by the same orderly path as a peer's EOF, within one slice. + if (inProcess && g_inProcessStop.load(std::memory_order_acquire)) { + WireLogError("MG_Remote server: pid=%d the in-process display server is stopping; ending the session", + selfPid); + break; + } if (!loop.Running()) { WireLogError("MG_Remote server: pid=%d the apply thread has stopped (%s, %llu event(s) dropped); " "ending the session", @@ -632,9 +736,11 @@ void DelaySessionHandshakeForTest() { } session.Close(); MobileGL::MG_Util::Debug::SetLogForwarder(nullptr, nullptr); - // _exit closes control after cleanup/flush; peer EOF is the client's fence. - std::fflush(nullptr); - ::_exit(latched ? kSessionLatchedExitCode : 0); + // P12 (D5): the backend loop.Stop() destroyed is not this session's to point at any more. + if (inProcess) session.SetBackend(nullptr); + // The caller's exit (a forked child's ExitSessionProcess) closes control after cleanup/flush - + // or, in-process, `control` is destroyed on the way out; peer EOF is the client's fence. + return latched ? kSessionLatchedExitCode : 0; } // P7 wave 0, Ph slice (1) (ID-P7-1). THE SUPERVISOR OBSERVES AND NAMES A SESSION'S DEATH. @@ -724,16 +830,37 @@ class TcpSupervisor { public: using Clock = std::chrono::steady_clock; - TcpSupervisor(int listener, const Server::PreAuthKnobs& knobs, unsigned long& sessionsFaulted) - : m_listener(listener), m_knobs(knobs), m_backoff(knobs), m_sessionsFaulted(sessionsFaulted) {} + // P12 (D5): HOW AN AUTHENTICATED SESSION IS HANDED OFF. `Fork` is the exec'd supervisor's shape + // (a session child per session, crash isolation, D9's eglTerminate isolation). `Thread` is the + // in-process display server's: the session runs on a thread of THIS process, because the window + // it renders into lives here and nowhere else (an ANativeWindow does not survive fork or exec). + // Everything before the hand-off - pre-auth, the backoff, Busy for an authenticated second + // Hello, DataBind routing over the socketpair - is the same code for both. What differs is what + // a separate fd table used to do for free: the thread shape closes neither the listener nor the + // other pending peers (they are the supervisor's, and still in use), keeps the session's end of + // the hand-off (the session closes it), and "reaps" by joining the thread. + enum class Handoff { Fork, Thread }; - // Returns only on a failure of the listener itself (73, as the old loop did). + TcpSupervisor(int listener, const Server::PreAuthKnobs& knobs, unsigned long& sessionsFaulted, + Handoff handoff = Handoff::Fork, const std::atomic* stop = nullptr) + : m_listener(listener), m_knobs(knobs), m_backoff(knobs), m_sessionsFaulted(sessionsFaulted), + m_handoffShape(handoff), m_stop(stop) {} + + ~TcpSupervisor() { + // A session thread is never left running behind its supervisor: Run() only returns after + // StopServing joined it, but a listener failure (73) returns from the loop directly. + if (m_sessionThread.joinable()) m_sessionThread.join(); + } + + // Returns only on a failure of the listener itself (73, as the old loop did) - or, with a stop + // flag (the in-process display server), 0 once the flag is raised and the live session ended. int Run() { WireLogError("MG_Remote server: pre-auth deadline=%u ms, at most %u pending, backoff after %u failures " "from %u ms (0 = off)", m_knobs.deadlineMs, m_knobs.maxPending, m_knobs.backoffAfter, m_knobs.backoffBaseMs); std::vector fds; for (;;) { + if (m_stop != nullptr && m_stop->load(std::memory_order_acquire)) return StopServing(); Reap(); auto now = Clock::now(); // While accepting is paused (out of descriptors, AcceptNew) the listener sits out the @@ -819,11 +946,45 @@ private: }; bool Reap() { - const bool reaped = ReapActiveSession(m_active, m_sessionsFaulted); + 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; } return reaped; } + // 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). + bool ReapSessionThread() { + if (m_active <= 0 || !m_sessionDone.load(std::memory_order_acquire)) return false; + if (m_sessionThread.joinable()) m_sessionThread.join(); + const int code = m_sessionExit.load(std::memory_order_acquire); + m_active = -1; + if (code != 0) ++m_sessionsFaulted; + ResetSessionLatch(); + WireLogError("MG_Remote server: in-process session #%lu pid=%d reaped exit=%d%s sessionsFaulted=%lu", + m_sessionOrdinal, static_cast(::getpid()), code, + code == kSessionLatchedExitCode ? " (latched fault)" : "", m_sessionsFaulted); + return true; + } + + // P12 (D5): the in-process server is stopping. Nobody new is served (every pending peer is told + // so by name), the live session - which reads the same flag each control slice - is waited out + // and reaped, and the supervisor returns 0. + int StopServing() { + for (auto& peer : m_pending) { + if (peer.fd >= 0) RefuseAndClose(peer.fd, Protocol::RefuseCode::Busy, "the server is stopping"); + peer.fd = -1; + } + m_pending.clear(); + while (m_active > 0) { + if (!Reap()) ::usleep(10000); + } + if (m_handoff >= 0) { ::close(m_handoff); m_handoff = -1; } + WireLogError("MG_Remote server: pid=%d in-process display server stopped", static_cast(::getpid())); + return 0; + } + // exit_group closes files just before waitpid can observe the exit. Allow that small // scheduling window before deciding whether a session is live - the old loop gave it to every // connection it accepted while a child existed; this gives it to the one decision that @@ -981,9 +1142,16 @@ private: Refuse(*connection, Protocol::RefuseCode::Busy, "could not create the data-connection hand-off"); return; } + if (m_handoffShape == Handoff::Thread) { + StartSessionThread(std::move(connection), hello, pair); + 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); // 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. @@ -994,8 +1162,8 @@ private: for (const auto& other : m_ready) if (other.fd >= 0) ::close(other.fd); const int controlFd = connection->StreamFd(); - RunSession(std::move(connection), hello, HandoffSource(pair[1], controlFd), pair[1], DataSource::Handoff, - 0); + ExitSessionProcess(RunSession(std::move(connection), hello, HandoffSource(pair[1], controlFd), pair[1], + DataSource::Handoff, 0)); } if (child < 0) { ::close(pair[0]); @@ -1009,10 +1177,50 @@ private: connection->CloseLocalCopy(); } + // P12 (D5): THE THREAD HAND-OFF. The session thread owns the connection (moved in) and the + // hand-off's session end (pair[1]; RunSession cuts and closes it when Accept returns); this + // thread keeps pair[0] to route DataBinds, exactly as the fork shape's parent does. There is no + // CloseLocalCopy: nothing was duplicated. `m_active` is this process's pid while the session + // lives - "a session is live" is all the rest of the supervisor asks of it. + void StartSessionThread(std::unique_ptr connection, const std::vector& hello, + const int (&pair)[2]) { + if (m_sessionThread.joinable()) m_sessionThread.join(); // reaped already; never live here + const int controlFd = connection->StreamFd(); + const int sessionEnd = pair[1]; + m_sessionDone.store(false, std::memory_order_release); + m_sessionExit.store(0, std::memory_order_release); + ++m_sessionOrdinal; + try { + m_sessionThread = std::thread( + [this, controlFd, sessionEnd, hello, conn = std::move(connection)]() mutable { + const int code = RunSession(std::move(conn), hello, HandoffSource(sessionEnd, controlFd), sessionEnd, + DataSource::Handoff, 0, /*inProcess=*/true); + m_sessionExit.store(code, std::memory_order_release); + m_sessionDone.store(true, std::memory_order_release); + }); + } catch (...) { + ::close(pair[0]); + ::close(pair[1]); + WireLogError("MG_Remote server: could not start the in-process session thread; the connection is " + "closed (it cannot be refused: it moved into the failed thread)"); + return; + } + m_active = ::getpid(); + m_handoff = pair[0]; + WireLogError("MG_Remote server: in-process session #%lu started on a thread (pid=%d)", m_sessionOrdinal, + static_cast(::getpid())); + } + int m_listener; Server::PreAuthKnobs m_knobs; Server::AuthBackoff m_backoff; unsigned long& m_sessionsFaulted; + Handoff m_handoffShape = Handoff::Fork; + const std::atomic* m_stop = nullptr; + std::thread m_sessionThread; + std::atomic m_sessionDone{false}; + std::atomic m_sessionExit{0}; + unsigned long m_sessionOrdinal = 0; 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. @@ -1023,9 +1231,10 @@ private: static constexpr std::uint32_t kAcceptPauseMs = 100; Clock::time_point m_acceptResumeAt{}; }; -} -extern "C" __attribute__((visibility("default"))) int mobilegl_server_main(int argc, char** argv) { +// The server's environment preconditions, shared by the exec'd supervisor and the in-process +// display server (P12 D5: "same env preconditions as mobilegl_server_main"). 0 when they hold. +int CheckServerEnvironment() { // The supervisor and every forked session child log under the server role. // Set it here rather than trusting the launcher to have done so: log role is // resolved from this env at first use, and a launcher that omits it would @@ -1047,6 +1256,12 @@ extern "C" __attribute__((visibility("default"))) int mobilegl_server_main(int a return 65; } } + return 0; +} +} + +extern "C" __attribute__((visibility("default"))) int mobilegl_server_main(int argc, char** argv) { + if (const int refused = CheckServerEnvironment(); refused != 0) return refused; std::string endpoint; bool serve = false; for (int i = 1; i < argc; ++i) { @@ -1100,12 +1315,12 @@ extern "C" __attribute__((visibility("default"))) int mobilegl_server_main(int a // so there is no fork to spare; the Hello is still read within the pre-auth // deadline and the token is still asked first. const int controlFd = control->StreamFd(); - RunSession(std::move(control), {}, ListenerSource(listener, controlFd), listener, - DataSource::Listener, knobs.deadlineMs); + ExitSessionProcess(RunSession(std::move(control), {}, ListenerSource(listener, controlFd), listener, + DataSource::Listener, knobs.deadlineMs)); } ::close(listener); if (endpoint[0] != '@') ::unlink(endpoint.c_str()); - RunSession(std::move(control), {}, {}, -1, DataSource::None, 10000); + ExitSessionProcess(RunSession(std::move(control), {}, {}, -1, DataSource::None, 10000)); } reap(); // exit_group closes files just before waitpid can observe the exit. @@ -1120,10 +1335,13 @@ extern "C" __attribute__((visibility("default"))) int mobilegl_server_main(int a continue; } std::fflush(nullptr); + const pid_t supervisor = ::getpid(); const pid_t child = ::fork(); if (child == 0) { + // P12 (D8): the unix `--serve` child dies with its supervisor too. + DieWithSupervisor(supervisor); ::close(listener); - RunSession(std::move(control), {}, {}, -1, DataSource::None, 10000); + ExitSessionProcess(RunSession(std::move(control), {}, {}, -1, DataSource::None, 10000)); } if (child < 0) { RefuseBusy(*control, "could not create session child"); @@ -1137,3 +1355,62 @@ extern "C" __attribute__((visibility("default"))) int mobilegl_server_main(int a if (endpoint[0] != '@' && endpoint.compare(0, 6, "tcp://") != 0) ::unlink(endpoint.c_str()); return 0; } + +// P12 (on-screen server window), D5. See InProcessServer.h for the contract. +extern "C" __attribute__((visibility("default"))) int mobilegl_server_serve_inprocess(const char* endpoint) { + if (const int refused = CheckServerEnvironment(); refused != 0) return refused; + if (endpoint == nullptr || std::strncmp(endpoint, "tcp://", 6) != 0) { + WireLogError("MG_Remote server: the in-process display server serves tcp:// only; '%s' is not one", + endpoint == nullptr ? "(null)" : endpoint); + return 71; + } + bool idle = false; + if (!g_inProcessServing.compare_exchange_strong(idle, true, std::memory_order_acq_rel)) { + WireLogError("MG_Remote server: an in-process display server is already serving in this process; " + "refusing a second one on %s", endpoint); + return 74; + } + // The stop flag is NOT cleared here: a stop asked for before this thread got going still + // stops it. It is cleared when serve returns, for the next call. + // D8: THE LISTEN RETRIES FOR UP TO ~5 s. The previous server on this port - the exec'd supervisor + // the Activity just stopped, or this process's own previous run - may still be dying, and its + // listener with it. Only a failed bind/listen is retried; a refusal of the configuration (a short + // token, a non-loopback listen without one) is not going to get better. + int listener = -1; + const auto listenDeadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(5000); + for (;;) { + const MobileGLResult listened = SocketTransport::Listen(endpoint, &listener); + if (listened == MOBILEGL_OK) break; + if (listened != MOBILEGL_ERR_UNSUPPORTED || g_inProcessStop.load(std::memory_order_acquire) || + std::chrono::steady_clock::now() >= listenDeadline) { + WireLogError("MG_Remote server: the in-process display server could not listen on %s (rc=%d)", endpoint, + static_cast(listened)); + g_inProcessStop.store(false, std::memory_order_release); + g_inProcessServing.store(false, std::memory_order_release); + return 72; + } + std::this_thread::sleep_for(std::chrono::milliseconds(250)); + } + // THE READINESS LINE, the supervisor's own words plus which shape this is. + WireLogError("MG_Remote server: pid=%d listening on %s (in-process display server, display %s)", + static_cast(::getpid()), endpoint, + Server::ServerDisplayInstance().HasDisplay() ? "installed" : "NOT installed - offscreen only"); + unsigned long sessionsFaulted = 0; + int result = 0; + { + TcpSupervisor supervisor(listener, Server::PreAuthKnobs::FromEnvironment(), sessionsFaulted, + TcpSupervisor::Handoff::Thread, &g_inProcessStop); + result = supervisor.Run(); + } + LogSupervisorSummary(sessionsFaulted); + ::close(listener); + g_inProcessStop.store(false, std::memory_order_release); + g_inProcessServing.store(false, std::memory_order_release); + return result; +} + +extern "C" __attribute__((visibility("default"))) void mobilegl_server_stop_inprocess(void) { + g_inProcessStop.store(true, std::memory_order_release); + // A session thread waiting for the display's window (a ServerOwned creation) stops waiting. + Server::ServerDisplayInstance().Interrupt(); +}