mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 18:13:23 +09:00
[WIP] (MG_Remote): implement P6.5 TCP and stream transport
This commit is contained in:
@@ -1,316 +1,117 @@
|
||||
// MobileGL - MobileGL/MG_Remote/Server/ServerMain.cpp
|
||||
// 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
|
||||
// MobileGL - separate-process server and TCP session supervisor.
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
// The server process's entry point. Package `sm` (CONTRACT-P6.md §3).
|
||||
//
|
||||
// ARCHITECTURE.md:488-496 has described this symbol for three phases and a6
|
||||
// found it existed nowhere in the tree. This is the real one.
|
||||
//
|
||||
// THE TWO PROCESSES ARE INDEPENDENT. This process is started by whoever starts
|
||||
// it - a shell, a test, an Android Service - and it LISTENS. The client is
|
||||
// started separately and CONNECTS. Nothing is inherited: no fds, no memory, no
|
||||
// parent.
|
||||
//
|
||||
// That is a deliberate change from this package's first shape, which forked the
|
||||
// server and handed it fds 3..6. Inheritance works and is less code, but it can
|
||||
// only ever produce a server that is a CHILD OF ITS CLIENT - the one arrangement
|
||||
// the end state cannot use, because there the server is a standing application
|
||||
// and the client is somewhere else entirely, possibly on another kernel.
|
||||
// CONTRACT-P6 §3.1 calls this Dial == Connect and had it as a later shape. It is
|
||||
// the primary one.
|
||||
//
|
||||
// WHAT THIS PACKAGE OWNS: the process, the rendezvous, the backend, the session,
|
||||
// the descriptor hand-off and the exit discipline. NOT what a control frame
|
||||
// means - that is `cp` - and not the data-plane seam, which is `lk`.
|
||||
//
|
||||
// _exit, NOT exit (CONTRACT-P6 §12.4). It decides whether g_processTeardown is
|
||||
// ever set, and therefore whether seven twin destructors take their early-out or
|
||||
// call into a driver whose context is already gone. The log is flushed first,
|
||||
// because _exit runs no atexit handler and a server that died without saying why
|
||||
// is the diagnostic this project refuses.
|
||||
|
||||
#include "../Transport/Doorbell.h" // kWaitForever, SocketDoorbell
|
||||
#include "../Transport/FdPassing.h"
|
||||
#include "../Transport/Doorbell.h"
|
||||
#include "../Transport/SocketTransport.h"
|
||||
#include "../Transport/WireLog.h"
|
||||
#include "../Protocol/SurfaceOpCodec.h"
|
||||
#include "ServerLoop.h"
|
||||
#include "ServerSession.h"
|
||||
#include "SurfaceControlFrame.h"
|
||||
|
||||
#include <Config.h> // MG_Config::Transport - D1 pins it to Spawn in this process
|
||||
#include <MG_Backend/MGPipe/PipeInputs.h> // D1c: MGPipeSetServerProcessRole
|
||||
#include <Config.h>
|
||||
#include <Init.h>
|
||||
#include <MG_Backend/MGPipe/PipeInputs.h>
|
||||
#include <MG_Backend/ServerRole.h>
|
||||
#include <MG_Util/Metrics/PipeStats.h> // step 1.6: the counters' latch, server-process half
|
||||
#include <Init.h> // MG_ConfigLoader::Init - the child loads its own config (step 1.5)
|
||||
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
#include <MG_Util/Metrics/PipeStats.h>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#if defined(__unix__) || defined(__APPLE__) || defined(__ANDROID__)
|
||||
# include <sys/socket.h>
|
||||
# include <unistd.h>
|
||||
#endif
|
||||
#include <sys/socket.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace {
|
||||
using namespace MobileGL::MG_Remote;
|
||||
using namespace MobileGL::MG_Remote::Transport;
|
||||
namespace Protocol = ::MobileGL::Wire;
|
||||
|
||||
// The server must not be able to become a client. CONTRACT-P6 §3.1: this is
|
||||
// catch (a), STRUCTURAL and separate from role selection, because forcing
|
||||
// MG_Config::Transport to Monolith - what ARCHITECTURE.md:488 says - would
|
||||
// flip 148 MG_Backend lines to the frontend arm in the one process with no
|
||||
// frontend. Catch (b) is the launcher's envp scrub, and the two report
|
||||
// through DIFFERENT exit codes so they are distinguishable: two safeties
|
||||
// that cannot be told apart are one safety.
|
||||
bool DialingIsForbidden() {
|
||||
const char* dial = std::getenv("MOBILEGL_IPC_DIAL");
|
||||
return dial != nullptr && std::strcmp(dial, "no") == 0;
|
||||
}
|
||||
void Refuse(SocketTransport& transport, Protocol::RefuseCode code, const char* detail) {
|
||||
flatbuffers::FlatBufferBuilder builder(256);
|
||||
auto refusal = Protocol::CreateRefuseDirect(builder, code, detail);
|
||||
auto envelope = Protocol::CreateCtrlEnvelope(builder, Protocol::CtrlMsg::Refuse, refusal.Union());
|
||||
Protocol::FinishCtrlEnvelopeBuffer(builder, envelope);
|
||||
transport.SendFrame({builder.GetBufferPointer(), builder.GetSize()});
|
||||
WireLogError("MG_Remote server: Refuse{%s} %s", Protocol::EnumNameRefuseCode(code), detail);
|
||||
}
|
||||
|
||||
bool LooksLikeAClientEnvironment() {
|
||||
static constexpr const char* kNames[] = {"MOBILEGL_TRANSPORT", "MOBILEGL_IPC_SERVER_PATH",
|
||||
"MOBILEGL_IPC_RING_MB", "MOBILEGL_IPC_STAGE_MB"};
|
||||
for (const char* name : kNames) {
|
||||
if (std::getenv(name) != nullptr) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
void ForwardLog(void* user, const char* text) {
|
||||
auto& transport = *static_cast<ITransport*>(user);
|
||||
flatbuffers::FlatBufferBuilder builder(512);
|
||||
auto log = Protocol::CreateLogLine(builder, Protocol::LogLevel::Info, builder.CreateString(text));
|
||||
auto envelope = Protocol::CreateCtrlEnvelope(builder, Protocol::CtrlMsg::LogLine, log.Union());
|
||||
Protocol::FinishCtrlEnvelopeBuffer(builder, envelope);
|
||||
(void)transport.SendFrame({builder.GetBufferPointer(), builder.GetSize()});
|
||||
}
|
||||
|
||||
// The rendezvous. A NAME, because independent processes have no other way to
|
||||
// find each other. No default: a default rendezvous is one that two unrelated
|
||||
// runs can collide on.
|
||||
std::string Endpoint(int argc, char** argv) {
|
||||
if (argc > 1 && argv[1] != nullptr && argv[1][0] != '\0') {
|
||||
return argv[1];
|
||||
}
|
||||
if (const char* fromEnv = std::getenv("MOBILEGL_IPC_ENDPOINT")) {
|
||||
return fromEnv;
|
||||
}
|
||||
return std::string();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" __attribute__((visibility("default"))) int mobilegl_server_main(int argc, char** argv) {
|
||||
using namespace MobileGL::MG_Remote;
|
||||
using namespace MobileGL::MG_Remote::Transport;
|
||||
struct FlushAck { ITransport* transport; std::uint64_t seq; };
|
||||
void SendLogAck(void* pointer) {
|
||||
auto& ack = *static_cast<FlushAck*>(pointer);
|
||||
flatbuffers::FlatBufferBuilder builder(64);
|
||||
auto flush = Protocol::CreateLogFlush(builder, ack.seq, true);
|
||||
auto envelope = Protocol::CreateCtrlEnvelope(builder, Protocol::CtrlMsg::LogFlush, flush.Union());
|
||||
Protocol::FinishCtrlEnvelopeBuffer(builder, envelope);
|
||||
(void)ack.transport->SendFrame({builder.GetBufferPointer(), builder.GetSize()});
|
||||
}
|
||||
|
||||
// Called only after fork: the supervisor never creates a backend or EGL context.
|
||||
[[noreturn]] void RunSession(std::unique_ptr<SocketTransport> control) {
|
||||
const int selfPid = static_cast<int>(::getpid());
|
||||
|
||||
// The arm-proof line (CONTRACT-P6 §9.5). ID-124 proved a lane can run
|
||||
// monolith and pass every other gate, so each entry leaves its own evidence.
|
||||
WireLogError("MG_Remote server: pid=%d transport=spawn role=server", selfPid);
|
||||
|
||||
if (!DialingIsForbidden()) {
|
||||
std::fprintf(stderr,
|
||||
"MG_Remote server: pid=%d refusing to start - MOBILEGL_IPC_DIAL=no was not "
|
||||
"set; anti-recursion catch (a) is missing\n",
|
||||
selfPid);
|
||||
std::fflush(nullptr);
|
||||
::_exit(64);
|
||||
const bool tcp = control->IsTcp();
|
||||
std::vector<std::uint8_t> helloFrame;
|
||||
std::uint64_t bytes = 0;
|
||||
auto received = control->ReceiveFrame({nullptr, 0}, &bytes, 10000);
|
||||
if (received != MOBILEGL_ERR_BUFFER_TOO_SMALL || bytes > 1024 * 1024) ::_exit(67);
|
||||
helloFrame.resize(static_cast<std::size_t>(bytes));
|
||||
if (control->ReceiveFrame({helloFrame.data(), helloFrame.size()}, &bytes, 0) != MOBILEGL_OK) ::_exit(67);
|
||||
flatbuffers::Verifier verifier(helloFrame.data(), helloFrame.size());
|
||||
if (!Protocol::VerifyCtrlEnvelopeBuffer(verifier)) ::_exit(67);
|
||||
const auto* hello = Protocol::GetCtrlEnvelope(helloFrame.data())->msg_as_Hello();
|
||||
if (!hello) ::_exit(67);
|
||||
if (tcp) {
|
||||
const char* expected = std::getenv("MOBILEGL_IPC_TOKEN");
|
||||
const std::string token = hello->token() ? hello->token()->str() : std::string();
|
||||
if (token != (expected ? expected : "")) {
|
||||
Refuse(*control, Protocol::RefuseCode::Authentication, "TCP token mismatch");
|
||||
::_exit(0);
|
||||
}
|
||||
}
|
||||
if (LooksLikeAClientEnvironment()) {
|
||||
std::fprintf(stderr,
|
||||
"MG_Remote server: pid=%d the envp scrub FAILED - a client knob survived "
|
||||
"into this process; catch (a) held, catch (b) did not\n",
|
||||
selfPid);
|
||||
std::fflush(nullptr);
|
||||
::_exit(65);
|
||||
}
|
||||
|
||||
const std::string endpoint = Endpoint(argc, argv);
|
||||
if (endpoint.empty()) {
|
||||
std::fprintf(stderr,
|
||||
"MG_Remote server: pid=%d no endpoint - pass one as argv[1] or set "
|
||||
"MOBILEGL_IPC_ENDPOINT\n",
|
||||
selfPid);
|
||||
std::fflush(nullptr);
|
||||
::_exit(71);
|
||||
}
|
||||
|
||||
// ---- 1. listen, BEFORE the backend. The client's bounded connect retry is
|
||||
// what tolerates the backend taking a while; a client that connected and then
|
||||
// waited for a Welcome a failed bring-up will never send would have to learn
|
||||
// about death from a timeout, which §5.4 forbids.
|
||||
int listenFd = -1;
|
||||
if (SocketTransport::Listen(endpoint, &listenFd) != MOBILEGL_OK) {
|
||||
std::fprintf(stderr, "MG_Remote server: pid=%d could not listen on \"%s\"\n", selfPid,
|
||||
endpoint.c_str());
|
||||
std::fflush(nullptr);
|
||||
::_exit(72);
|
||||
}
|
||||
WireLogError("MG_Remote server: pid=%d listening on \"%s\"", selfPid, endpoint.c_str());
|
||||
|
||||
// ---- 1.5. CONFIG, AND THEN D1.
|
||||
//
|
||||
// TWO SEPARATE DEFECTS LIVED HERE, and both were silent.
|
||||
//
|
||||
// (a) NOTHING LOADED THE CONFIG AT ALL. mobilegl_server_main calls
|
||||
// InitServerRoleForSpawn directly rather than MobileGL::Initialize, and
|
||||
// MG_ConfigLoader::Init is a step of the latter - so every MG_Config
|
||||
// value in this process was its STATIC DEFAULT. MOBILEGL_BACKEND_TYPE
|
||||
// survives the launcher's scrub on purpose, and it was being read by
|
||||
// nobody: a DirectVulkan session would have brought up a DirectGLES
|
||||
// server and then disagreed with its client about which backend the
|
||||
// capability mask describes.
|
||||
//
|
||||
// (b) THE CHILD RESOLVED Transport == Monolith. CONTRACT-P6 §3.1 (D1) says
|
||||
// it must read as Spawn, and a6 measured the cost of getting it wrong:
|
||||
// 226 live lines test `Transport != Monolith` to select the SERVER arm,
|
||||
// 148 of them in MG_Backend. Monolith flips all of them to frontend glue
|
||||
// in the one process that has no frontend. Observed, exactly as
|
||||
// predicted: TextureImpl::UnitTexturesByHandle() answered false, Clear
|
||||
// took the legacy arm that dereferences borrowed FRONTEND slot pointers,
|
||||
// and the apply thread took a SIGSEGV inside SyncNeccessaryTextures on
|
||||
// the first Clear record of the OpenRA retrace.
|
||||
//
|
||||
// IT IS SET HERE AND NOT THROUGH THE ENVIRONMENT because the environment is
|
||||
// where it must NOT appear: LooksLikeAClientEnvironment above exits 65 if
|
||||
// MOBILEGL_TRANSPORT survived into this process, and that check is
|
||||
// anti-recursion catch (b). D1's own answer is that Dial - not Transport -
|
||||
// is the anti-recursion axis, so Transport is free to say what is true.
|
||||
MobileGL::MG_ConfigLoader::Init();
|
||||
MobileGL::MG_Config::Transport = MobileGL::MG_Config::TransportMode::Spawn;
|
||||
// D1c/D10: THIS PROCESS IS THE SERVER, stated once and before any GL work.
|
||||
//
|
||||
// Four guards used to ask "am I on the apply thread" or "is a ClientSession active" - both
|
||||
// questions a server PROCESS answers wrongly: it has no ClientSession at all, and every one
|
||||
// of its threads is a server thread, not just the applier's. Saying it here is what arms
|
||||
// them; without it they are compiled in and permanently false, which is worse than absent
|
||||
// because it reads as coverage.
|
||||
MobileGL::MG_Pipe::MGPipeSetServerProcessRole(true);
|
||||
// ---- 1.6. THE MGPIPE COUNTERS, and this is the ONLY call site outside
|
||||
// MobileGL::Initialize().
|
||||
//
|
||||
// GATE 8'S SERVER HALF WAS STRUCTURALLY ZERO WITHOUT THIS, and the shape of the
|
||||
// defect is worth stating: the child never runs MobileGL::Initialize, so
|
||||
// PipeStats::Init() - a step of it - never ran here, and g_pipeStatsEnabled
|
||||
// stayed false. Every one of the ~80 `if (Enabled())` sites in the backends and
|
||||
// in MG_Remote (ServerLoop's own ServerWaits/ServerParks publish included)
|
||||
// compiled in and was permanently false in this process. The contract's
|
||||
// mandatory "socket doorbell vs inproc condvar" number therefore had no server
|
||||
// half under `spawn` at all, and its absence read exactly like a real zero:
|
||||
// `wait[srv=0 srvpark=0]` is what "the server never waited" looks like too.
|
||||
//
|
||||
// IT GOES AFTER THE CONFIG LOAD AND AFTER THE ROLE IS STATED, and both halves
|
||||
// of that order are load-bearing:
|
||||
//
|
||||
// * AFTER MG_ConfigLoader::Init, because Init() LATCHES the flag out of
|
||||
// MG_Config::Features.PipeStats, and the parser does not exist until the
|
||||
// line above has run. Calling it earlier would latch the static default
|
||||
// (false) and the MOBILEGL_PIPE_STATS the launcher passed would be read by
|
||||
// nobody - the same class of defect the config load itself used to have.
|
||||
// * AFTER MGPipeSetServerProcessRole, because that is what makes the LOG SINK
|
||||
// know this process is the server, and PipeStats' summary line is emitted
|
||||
// through MGLOG_I. The sink resolves its role from MOBILEGL_IPC_ROLE (which
|
||||
// the launcher sets and its envp scrub preserves) and would in fact be
|
||||
// right either way, but ordering it this way means the line cannot be filed
|
||||
// under the client by a future change to that resolution. What the contract
|
||||
// requires - that the summary lands in `<base>.server.log` - is asserted by
|
||||
// the lane, not assumed from this comment.
|
||||
//
|
||||
// MOBILEGL_PIPE_STATS / _PERIOD / _FILE MEAN EXACTLY WHAT THEY MEAN IN THE
|
||||
// CLIENT PROCESS, and that is the whole point of routing through Init(): all
|
||||
// three are read from the child's own environment by the same parser and
|
||||
// latched by the same function. Nothing about the stats channel is spawn-shaped.
|
||||
// One asymmetry is real and intended: this process has no backend Present of
|
||||
// its own, so its line cadence rides the present RECORDS it applies
|
||||
// (PipeApplier -> DirectGLES/DirectVulkan Present -> OnPresent) rather than a
|
||||
// swap the server does not perform. A spawn session that presents N times
|
||||
// produces N windows here, which is the same N the client's own line counts.
|
||||
// The child bypasses MobileGL::Initialize: arm its own role counters.
|
||||
MobileGL::MG_Util::PipeStats::Init();
|
||||
WireLogError("MG_Remote server: pid=%d config loaded, backend=%d, Transport pinned to Spawn "
|
||||
"(D1: this process IS the server arm)",
|
||||
selfPid, static_cast<int>(MobileGL::MG_Config::ActiveBackendType));
|
||||
|
||||
// ---- 2. the backend, through the SAME entry point the inproc server role
|
||||
// uses (MG_Backend/ServerRole.h). No EGL yet: the context is created on the
|
||||
// apply thread when the client's first eglMakeCurrent crosses.
|
||||
if (!MobileGL::MG_Backend::InitServerRoleForSpawn()) {
|
||||
std::fprintf(stderr,
|
||||
"MG_Remote server: pid=%d could not create a backend; refusing to accept a "
|
||||
"session it could never apply\n",
|
||||
selfPid);
|
||||
::close(listenFd);
|
||||
::unlink(endpoint.c_str());
|
||||
std::fflush(nullptr);
|
||||
::_exit(66);
|
||||
const auto backend = static_cast<MobileGL::BackendType>(hello->backendType());
|
||||
if (backend != MobileGL::BackendType::DirectGLES && backend != MobileGL::BackendType::DirectVulkan) {
|
||||
Refuse(*control, Protocol::RefuseCode::Backend, "unsupported Hello.backendType");
|
||||
::_exit(0);
|
||||
}
|
||||
|
||||
// ---- 3. accept the client's TWO connections: control, then aux.
|
||||
std::unique_ptr<SocketTransport> control;
|
||||
if (SocketTransport::AcceptPair(listenFd, 30000, control) != MOBILEGL_OK) {
|
||||
std::fprintf(stderr, "MG_Remote server: pid=%d no client within 30 s\n", selfPid);
|
||||
::close(listenFd);
|
||||
::unlink(endpoint.c_str());
|
||||
std::fflush(nullptr);
|
||||
::_exit(73);
|
||||
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");
|
||||
::_exit(0);
|
||||
}
|
||||
// One session per process (CONTRACT-P6 §12.3), so the door closes behind the
|
||||
// one client that came through it. A second would collide on four process
|
||||
// globals and on DirectGLES's single per-process EGL tuple.
|
||||
::close(listenFd);
|
||||
::unlink(endpoint.c_str());
|
||||
|
||||
// ---- 4. the two bells, created HERE and handed over. NOT inherited: there
|
||||
// is no parent to inherit from. One socketpair per DIRECTION, because
|
||||
// SocketDoorbell is "write a byte to wake the peer, poll to be woken" and a
|
||||
// single pair would have each side reading the byte it wrote to the other.
|
||||
int serverBell[2] = {-1, -1};
|
||||
int clientBell[2] = {-1, -1};
|
||||
if (::socketpair(AF_UNIX, SOCK_STREAM, 0, serverBell) != 0 ||
|
||||
::socketpair(AF_UNIX, SOCK_STREAM, 0, clientBell) != 0) {
|
||||
std::fprintf(stderr, "MG_Remote server: pid=%d bell socketpair failed\n", selfPid);
|
||||
std::fflush(nullptr);
|
||||
::_exit(74);
|
||||
MobileGL::MG_Config::ActiveBackendType = backend;
|
||||
if (!MobileGL::MG_Backend::InitServerRoleForSpawn()) ::_exit(66);
|
||||
auto& session = Server::ServerSessionInstance();
|
||||
int serverBell[2] = {-1, -1}, clientBell[2] = {-1, -1};
|
||||
std::unique_ptr<SocketDoorbell> selfBell, peerBell;
|
||||
if (!tcp) {
|
||||
if (::socketpair(AF_UNIX, SOCK_STREAM, 0, serverBell) != 0 ||
|
||||
::socketpair(AF_UNIX, SOCK_STREAM, 0, clientBell) != 0) ::_exit(74);
|
||||
selfBell = std::make_unique<SocketDoorbell>(serverBell[0], serverBell[1], 1, true);
|
||||
peerBell = std::make_unique<SocketDoorbell>(-1, clientBell[1], 1, true);
|
||||
session.SetExternalDoorbells(selfBell.get(), peerBell.get());
|
||||
}
|
||||
// PARK AND NOTIFY ON DIFFERENT DESCRIPTORS, and this is not a refinement -
|
||||
// the single-fd form CANNOT WAKE ITSELF. send() on one end of a socketpair
|
||||
// is delivered to the OTHER end, so a one-fd bell built on serverBell[0]
|
||||
// writes to serverBell[1] and polls serverBell[0]: correct for waking a
|
||||
// PEER, useless for waking a thread in this process. The server needs
|
||||
// exactly that, because the control pump posts into the apply thread's
|
||||
// mailbox and then has to ring the bell that thread is parked on. Measured
|
||||
// symptom when it was wrong: every eglInitialize crossed, landed in the
|
||||
// mailbox, and timed out at 5 s because nothing ever woke mgl-srv-apply.
|
||||
SocketDoorbell selfBell(/*parkFd=*/serverBell[0], /*notifyFd=*/serverBell[1], /*code=*/1,
|
||||
/*ownsFds=*/true);
|
||||
// RINGS ONLY. The client parks on clientBell[0]; writing clientBell[1] is
|
||||
// what reaches it. parkFd = -1 says in the type that this side never waits.
|
||||
SocketDoorbell peerBell(/*parkFd=*/-1, /*notifyFd=*/clientBell[1], /*code=*/1,
|
||||
/*ownsFds=*/true);
|
||||
|
||||
Server::ServerSession& session = Server::ServerSessionInstance();
|
||||
session.SetExternalDoorbells(&selfBell, &peerBell);
|
||||
|
||||
// ---- 5. Accept: the ABI assertion, the four segments, and Welcome.
|
||||
const MobileGLResult accepted = session.Accept(*control);
|
||||
const auto accepted = session.Accept(*control, &helloFrame);
|
||||
if (accepted != MOBILEGL_OK) {
|
||||
std::fprintf(stderr, "MG_Remote server: pid=%d Accept failed (rc=%d)\n", selfPid,
|
||||
static_cast<int>(accepted));
|
||||
WireLogError("MG_Remote server: pid=%d Accept failed (rc=%d)", selfPid, static_cast<int>(accepted));
|
||||
std::fflush(nullptr);
|
||||
::_exit(67);
|
||||
::_exit(accepted == MOBILEGL_ERR_PROTOCOL_MISMATCH ? 0 : 67);
|
||||
}
|
||||
|
||||
// ---- 6. hand SIX descriptors over. THIS IS WHY SCM_RIGHTS IS IN THIS
|
||||
// DESIGN: the segments are anonymous - ASharedMemory_create has no filesystem
|
||||
// name at all, and memfd / shm_open+unlink are not openable either
|
||||
// (ShmSegmentPosix.cpp says so) - so the descriptor IS the only key, and this
|
||||
// process is the side that created them.
|
||||
//
|
||||
// The slot travels in the sideband rather than being inferred from arrival
|
||||
// order, so a reordered or dropped offer is a NAMED mismatch on arrival
|
||||
// instead of two segments quietly swapped.
|
||||
if (!tcp)
|
||||
{
|
||||
struct Sideband {
|
||||
std::uint32_t slot;
|
||||
@@ -359,87 +160,108 @@ extern "C" __attribute__((visibility("default"))) int mobilegl_server_main(int a
|
||||
::close(clientBell[0]);
|
||||
}
|
||||
|
||||
// ---- 7. the apply thread. It holds the native context for its whole life,
|
||||
// which is what turns MakeCurrent's cache-invalidation storm into a one-off.
|
||||
Server::ServerLoop& loop = Server::ServerLoopInstance();
|
||||
if (loop.Start(session) != MOBILEGL_OK) {
|
||||
std::fprintf(stderr, "MG_Remote server: pid=%d the apply thread did not start\n", selfPid);
|
||||
std::fflush(nullptr);
|
||||
::_exit(70);
|
||||
}
|
||||
|
||||
WireLogError("MG_Remote server: pid=%d transport=spawn role=server ready "
|
||||
"(backend up, seven descriptors handed over, apply thread running)",
|
||||
selfPid);
|
||||
|
||||
// ---- 8. the control pump. EOF is the whole exit condition: no timeout may
|
||||
// ever stand in for the death fact (§5.4), because a server one frame behind
|
||||
// is the intended steady state and a server that is gone is a different thing.
|
||||
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();
|
||||
if (loop.Start(session) != MOBILEGL_OK) ::_exit(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<std::uint8_t> buffer(64 * 1024);
|
||||
for (;;) {
|
||||
std::uint64_t size = 0;
|
||||
MobileGLMutableByteSpan span{buffer.data(), buffer.size()};
|
||||
const MobileGLResult result = control->ReceiveFrame(span, &size, kWaitForever);
|
||||
if (result == MOBILEGL_ERR_TRANSPORT_CLOSED) {
|
||||
WireLogError("MG_Remote server: pid=%d peer closed the control stream; exiting",
|
||||
selfPid);
|
||||
break;
|
||||
const auto result = control->ReceiveFrame({buffer.data(), buffer.size()}, &size, kWaitForever);
|
||||
if (result == MOBILEGL_ERR_BUFFER_TOO_SMALL) { buffer.resize(static_cast<std::size_t>(size)); continue; }
|
||||
if (result != MOBILEGL_OK) break;
|
||||
flatbuffers::Verifier check(buffer.data(), static_cast<std::size_t>(size));
|
||||
if (!Protocol::VerifyCtrlEnvelopeBuffer(check)) break;
|
||||
const auto* envelope = Protocol::GetCtrlEnvelope(buffer.data());
|
||||
if (const auto* flush = envelope->msg_as_LogFlush()) {
|
||||
if (flush->ack()) break;
|
||||
FlushAck ack{control.get(), flush->seq()};
|
||||
MobileGL::MG_Util::Debug::WithLogBarrier(&SendLogAck, &ack);
|
||||
continue;
|
||||
}
|
||||
if (result == MOBILEGL_ERR_BUFFER_TOO_SMALL) {
|
||||
buffer.resize(static_cast<std::size_t>(size));
|
||||
continue; // still queued: ITransport's contract
|
||||
}
|
||||
if (result != MOBILEGL_OK) {
|
||||
WireLogError("MG_Remote server: pid=%d control stream failed (rc=%d); exiting", selfPid,
|
||||
static_cast<int>(result));
|
||||
break;
|
||||
}
|
||||
// `cp`: decode, apply, reply. THIS THREAD IS NOT THE APPLY THREAD, which
|
||||
// is what CONTRACT-P6 §6.2 requires - a control arrival must not wake the
|
||||
// applier, and frame decode must not happen on the thread holding the
|
||||
// native context. ServerApplyWireSurfaceOp posts through ServerLoop's
|
||||
// existing one-slot mailbox, unchanged.
|
||||
const ::MobileGL::Wire::CtrlEnvelope* envelope = nullptr;
|
||||
{
|
||||
::flatbuffers::Verifier verifier(buffer.data(), static_cast<std::size_t>(size));
|
||||
if (::MobileGL::Wire::VerifyCtrlEnvelopeBuffer(verifier)) {
|
||||
envelope = ::MobileGL::Wire::GetCtrlEnvelope(buffer.data());
|
||||
}
|
||||
}
|
||||
if (envelope == nullptr || envelope->msg_type() != ::MobileGL::Wire::CtrlMsg::SurfaceOp ||
|
||||
envelope->msg_as_SurfaceOp() == nullptr) {
|
||||
WireLogError("MG_Remote server: pid=%d a control frame that is not a verifiable "
|
||||
"SurfaceOp arrived; ignoring it would be the silent drop this project "
|
||||
"refuses, so the session ends here",
|
||||
selfPid);
|
||||
break;
|
||||
}
|
||||
|
||||
const auto* operation = envelope->msg_as_SurfaceOp();
|
||||
if (!operation) break;
|
||||
Server::SurfaceControlFrame reply{};
|
||||
const MobileGLResult applied =
|
||||
ServerApplyWireSurfaceOp(*envelope->msg_as_SurfaceOp(), &reply);
|
||||
(void)applied; // carried in the reply's ok/result, not thrown away here
|
||||
|
||||
::flatbuffers::FlatBufferBuilder builder(256);
|
||||
(void)ServerApplyWireSurfaceOp(*operation, &reply);
|
||||
session.FlushDataProgress();
|
||||
if (session.DataLink()) reply.eventHead = session.DataLink()->EventPublishedHead();
|
||||
flatbuffers::FlatBufferBuilder builder(256);
|
||||
EncodeSurfaceReplyFrame(reply, &builder);
|
||||
if (control->SendFrame(
|
||||
MobileGLByteSpan{builder.GetBufferPointer(), builder.GetSize()}) != MOBILEGL_OK) {
|
||||
break;
|
||||
}
|
||||
if (control->SendFrame({builder.GetBufferPointer(), builder.GetSize()}) != MOBILEGL_OK) break;
|
||||
}
|
||||
|
||||
loop.Stop();
|
||||
session.Close();
|
||||
control->Shutdown();
|
||||
// Step 1.6's other half, and its position is the same argument MobileGL::Destroy
|
||||
// makes: AFTER loop.Stop(), because the apply thread is what publishes
|
||||
// ServerWaits / ServerParks and a dump taken while it still runs is a race; and
|
||||
// BEFORE _exit, because _exit runs no atexit handler and this is the only place
|
||||
// the server's run totals and its JSON dump can be written at all. Without it
|
||||
// the server printed whatever windows its last period boundary happened to
|
||||
// reach and lost the final one - the same "the last frame's numbers can be
|
||||
// lost" case Init.cpp's Shutdown call exists to avoid on the client side.
|
||||
// Publish the final server window while log forwarding is still attached.
|
||||
MobileGL::MG_Util::PipeStats::Shutdown();
|
||||
session.Close();
|
||||
MobileGL::MG_Util::Debug::SetLogForwarder(nullptr, nullptr);
|
||||
control->Shutdown();
|
||||
std::fflush(nullptr);
|
||||
::_exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" __attribute__((visibility("default"))) int mobilegl_server_main(int argc, char** argv) {
|
||||
const char* dial = std::getenv("MOBILEGL_IPC_DIAL");
|
||||
if (!dial || std::strcmp(dial, "no") != 0) {
|
||||
std::fprintf(stderr, "MG_Remote server: MOBILEGL_IPC_DIAL=no anti-recursion catch (a) missing\n");
|
||||
return 64;
|
||||
}
|
||||
for (const char* name : {"MOBILEGL_TRANSPORT", "MOBILEGL_IPC_SERVER_PATH", "MOBILEGL_IPC_RING_MB", "MOBILEGL_IPC_STAGE_MB"}) {
|
||||
if (std::getenv(name)) {
|
||||
std::fprintf(stderr, "MG_Remote server: envp scrub FAILED, catch (b): %s\n", name);
|
||||
return 65;
|
||||
}
|
||||
}
|
||||
std::string endpoint;
|
||||
bool serve = false;
|
||||
unsigned maxSessions = 0;
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
const std::string arg(argv[i]);
|
||||
if (arg == "--serve") serve = true;
|
||||
else if (arg == "--max-sessions" && i + 1 < argc) {
|
||||
char* end = nullptr;
|
||||
const auto count = std::strtoul(argv[++i], &end, 10);
|
||||
if (*end || count == 0 || count > 1000000) return 71;
|
||||
maxSessions = static_cast<unsigned>(count);
|
||||
} else if (endpoint.empty() && arg.compare(0, 2, "--") != 0) endpoint = arg;
|
||||
else return 71;
|
||||
}
|
||||
if (endpoint.empty()) if (const char* value = std::getenv("MOBILEGL_IPC_ENDPOINT")) endpoint = value;
|
||||
if (endpoint.empty()) return 71;
|
||||
int listener = -1;
|
||||
if (SocketTransport::Listen(endpoint, &listener) != MOBILEGL_OK) return 72;
|
||||
WireLogError("MG_Remote server: pid=%d listening on %s", static_cast<int>(::getpid()), endpoint.c_str());
|
||||
pid_t active = -1;
|
||||
unsigned sessions = 0;
|
||||
for (;;) {
|
||||
if (active > 0 && ::waitpid(active, nullptr, WNOHANG) == active) active = -1;
|
||||
if (maxSessions && sessions >= maxSessions && active < 0) break;
|
||||
std::unique_ptr<SocketTransport> control;
|
||||
const auto accepted = SocketTransport::AcceptPair(listener, serve ? 250 : 30000, control);
|
||||
if (accepted == MOBILEGL_ERR_TIMEOUT && serve) continue;
|
||||
if (accepted != MOBILEGL_OK) { ::close(listener); return 73; }
|
||||
if (!serve) {
|
||||
::close(listener);
|
||||
if (endpoint[0] != '@' && endpoint.compare(0, 6, "tcp://") != 0) ::unlink(endpoint.c_str());
|
||||
RunSession(std::move(control));
|
||||
}
|
||||
if (active > 0 && ::waitpid(active, nullptr, WNOHANG) == active) active = -1;
|
||||
if (active > 0 || (maxSessions && sessions >= maxSessions)) {
|
||||
Refuse(*control, Protocol::RefuseCode::Busy, "one session is already active");
|
||||
continue;
|
||||
}
|
||||
std::fflush(nullptr);
|
||||
const pid_t child = ::fork();
|
||||
if (child == 0) { ::close(listener); RunSession(std::move(control)); }
|
||||
if (child < 0) { Refuse(*control, Protocol::RefuseCode::Busy, "could not create session child"); continue; }
|
||||
active = child;
|
||||
++sessions;
|
||||
control->CloseLocalCopy();
|
||||
}
|
||||
::close(listener);
|
||||
if (endpoint[0] != '@' && endpoint.compare(0, 6, "tcp://") != 0) ::unlink(endpoint.c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user