mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 18:13:23 +09:00
[Fix] (MG_Remote, P11 PAIR): a socket client's control and aux connections pair by the nonce each presents first, not by arrival order - a readiness probe or a second client can no longer take half a session, and a client that sends its Hello first is refused by name
This commit is contained in:
@@ -656,6 +656,9 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
MobileGL/MG_Remote/Server/PipeApplier.cpp
|
||||
MobileGL/MG_Remote/Server/ServerLoop.cpp
|
||||
MobileGL/MG_Remote/Server/ServerMain.cpp
|
||||
# P11 PAIR: a socket client's control and aux connections paired by the nonce each
|
||||
# presents first (PairBind), not by arrival order - both unix supervisors take pairs here.
|
||||
MobileGL/MG_Remote/Server/PairAcceptor.cpp
|
||||
MobileGL/MG_Remote/Server/ServerSpawn.cpp
|
||||
# P5f fc: the value frame that replaced the control mailbox's function-pointer payload,
|
||||
# and its codec against protocol.fbs's SurfaceOp/SurfaceReply (the spawn arm's seam).
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
// MobileGL - MobileGL/MG_Remote/Server/PairAcceptor.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
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
// P11 PAIR: see PairAcceptor.h for what is paired, what is refused and why.
|
||||
|
||||
#include "PairAcceptor.h"
|
||||
|
||||
#if !defined(_WIN32)
|
||||
|
||||
#include "../Handshake.h" // AuthenticatePeerToken / ValidatePeerHandshake, for a Hello sent first
|
||||
#include "../Transport/WireLog.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
|
||||
#include <poll.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace MobileGL::MG_Remote::Server {
|
||||
|
||||
namespace {
|
||||
namespace Protocol = ::MobileGL::Wire;
|
||||
using Transport::SocketTransport;
|
||||
using Transport::TransportRole;
|
||||
using Step = FirstFrameAssembler::Step;
|
||||
|
||||
// A pause after accept(2) ran out of descriptors or memory: the connection waits in the
|
||||
// backlog and the listener is fine (TcpSupervisor pauses the same way).
|
||||
constexpr std::uint32_t kAcceptPauseMs = 100;
|
||||
|
||||
std::int64_t MillisecondsUntil(PairAcceptor::Clock::time_point when, PairAcceptor::Clock::time_point now) {
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(when - now).count();
|
||||
}
|
||||
|
||||
// Whatever the peer already sent, read and dropped (non-blocking, bounded), so the close
|
||||
// after a refusal does not become an RST that discards the refusal (ServerMain.cpp's
|
||||
// DrainUnread; RefuseBusy's comment has the reason).
|
||||
void DrainUnread(int fd) {
|
||||
char sink[4096];
|
||||
for (int rounds = 0; rounds < 256; ++rounds) {
|
||||
if (::recv(fd, sink, sizeof(sink), MSG_DONTWAIT) <= 0) return;
|
||||
}
|
||||
}
|
||||
|
||||
void Refuse(SocketTransport& connection, 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);
|
||||
(void)connection.SendFrame({builder.GetBufferPointer(), builder.GetSize()});
|
||||
Transport::WireLogError("MG_Remote server: Refuse{%s} %s", Protocol::EnumNameRefuseCode(code), detail);
|
||||
}
|
||||
|
||||
// Refuses a connection held as a bare descriptor, and closes it (with shutdown(2): here the
|
||||
// descriptor is only ever the supervisor's).
|
||||
void RefuseAndClose(int fd, Protocol::RefuseCode code, const char* detail) {
|
||||
SocketTransport connection(fd, -1, TransportRole::Server);
|
||||
DrainUnread(fd);
|
||||
Refuse(connection, code, detail);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
PairAcceptor::PairAcceptor(int listenFd, std::uint32_t budgetMs) : m_listener(listenFd), m_budgetMs(budgetMs) {}
|
||||
|
||||
PairAcceptor::~PairAcceptor() {
|
||||
for (auto& waiting : m_waiting) Drop(waiting, true, kPairStopped);
|
||||
for (const auto& pair : m_ready) {
|
||||
RefuseAndClose(pair.first, Protocol::RefuseCode::Busy, kPairStopped);
|
||||
::close(pair.second);
|
||||
}
|
||||
}
|
||||
|
||||
void PairAcceptor::CloseInForkedChild() {
|
||||
for (auto& waiting : m_waiting)
|
||||
if (waiting.fd >= 0) ::close(waiting.fd);
|
||||
for (const auto& pair : m_ready) {
|
||||
::close(pair.first);
|
||||
::close(pair.second);
|
||||
}
|
||||
m_waiting.clear();
|
||||
m_ready.clear();
|
||||
}
|
||||
|
||||
MobileGLResult PairAcceptor::Accept(std::uint32_t timeoutMs, std::unique_ptr<SocketTransport>& outServer) {
|
||||
outServer.reset();
|
||||
if (m_listener < 0) return MOBILEGL_ERR_INVALID_ARGUMENT;
|
||||
const auto until = Clock::now() + std::chrono::milliseconds(timeoutMs);
|
||||
std::vector<pollfd> fds;
|
||||
std::vector<std::size_t> polled; // index into m_waiting of fds[1..]
|
||||
for (;;) {
|
||||
if (!m_ready.empty()) {
|
||||
const auto pair = m_ready.front();
|
||||
m_ready.pop_front();
|
||||
outServer = std::make_unique<SocketTransport>(pair.first, pair.second, TransportRole::Server);
|
||||
return MOBILEGL_OK;
|
||||
}
|
||||
auto now = Clock::now();
|
||||
Expire(now);
|
||||
// While accepting is paused the listener sits out the poll (it stays readable and would
|
||||
// spin the loop), and the pause's end is a wakeup.
|
||||
const bool acceptPaused = now < m_acceptResumeAt;
|
||||
fds.clear();
|
||||
polled.clear();
|
||||
fds.push_back({acceptPaused ? -1 : m_listener, POLLIN, 0});
|
||||
std::int64_t wait = std::clamp<std::int64_t>(MillisecondsUntil(until, now), 0, 1 << 30);
|
||||
for (std::size_t index = 0; index < m_waiting.size(); ++index) {
|
||||
const Waiting& waiting = m_waiting[index];
|
||||
wait = std::clamp<std::int64_t>(MillisecondsUntil(waiting.deadline, now) + 1, 0, wait);
|
||||
// A bound connection is not read again: on control its Hello follows, and that is
|
||||
// the session's to read. It only waits for its partner or its deadline.
|
||||
if (waiting.bound) continue;
|
||||
fds.push_back({waiting.fd, POLLIN, 0});
|
||||
polled.push_back(index);
|
||||
}
|
||||
if (acceptPaused) wait = std::clamp<std::int64_t>(MillisecondsUntil(m_acceptResumeAt, now) + 1, 0, wait);
|
||||
const int ready = ::poll(fds.data(), static_cast<nfds_t>(fds.size()), static_cast<int>(wait));
|
||||
if (ready < 0 && errno != EINTR) {
|
||||
Transport::WireLogError("MG_Remote server: the pairing poll failed: %s", std::strerror(errno));
|
||||
return MOBILEGL_ERR_TRANSPORT_CLOSED;
|
||||
}
|
||||
if (ready > 0) {
|
||||
for (std::size_t slot = 0; slot < polled.size(); ++slot)
|
||||
if (fds[slot + 1].revents != 0) Read(m_waiting[polled[slot]]);
|
||||
Compact();
|
||||
if ((fds[0].revents & (POLLIN | POLLERR | POLLHUP)) != 0) {
|
||||
const MobileGLResult accepted = AcceptNew(Clock::now());
|
||||
if (accepted != MOBILEGL_OK) return accepted;
|
||||
}
|
||||
}
|
||||
if (m_ready.empty() && Clock::now() >= until) return MOBILEGL_ERR_TIMEOUT;
|
||||
}
|
||||
}
|
||||
|
||||
// Accepts everything the listener has; each connection waits in the set until it identifies.
|
||||
MobileGLResult PairAcceptor::AcceptNew(Clock::time_point now) {
|
||||
for (int burst = 0; burst < 64; ++burst) {
|
||||
int fd = -1;
|
||||
const MobileGLResult accepted = SocketTransport::AcceptOne(m_listener, 0, &fd);
|
||||
if (accepted == MOBILEGL_ERR_TIMEOUT) return MOBILEGL_OK;
|
||||
if (accepted == MOBILEGL_ERR_OUT_OF_MEMORY) {
|
||||
m_acceptResumeAt = now + std::chrono::milliseconds(kAcceptPauseMs);
|
||||
MGLOG_W("MG_Remote server: accept paused for %u ms: out of descriptors or memory with %zu "
|
||||
"connections waiting to be paired",
|
||||
kAcceptPauseMs, m_waiting.size());
|
||||
return MOBILEGL_OK;
|
||||
}
|
||||
if (accepted != MOBILEGL_OK) {
|
||||
Transport::WireLogError("MG_Remote server: the listener failed while pairing (rc=%d)",
|
||||
static_cast<int>(accepted));
|
||||
return MOBILEGL_ERR_TRANSPORT_CLOSED;
|
||||
}
|
||||
if (m_waiting.size() >= kPairMaxUnpaired) {
|
||||
// The oldest connection that has not said who it is goes first; with none, the oldest.
|
||||
auto victim = std::find_if(m_waiting.begin(), m_waiting.end(),
|
||||
[](const Waiting& waiting) { return !waiting.bound; });
|
||||
if (victim == m_waiting.end()) victim = m_waiting.begin();
|
||||
MGLOG_W("MG_Remote server: %zu connections are waiting to be paired; the oldest %s one is dropped",
|
||||
m_waiting.size(), victim->bound ? "identified" : "unidentified");
|
||||
Drop(*victim, true, kPairFull);
|
||||
m_waiting.erase(victim);
|
||||
}
|
||||
Waiting waiting;
|
||||
waiting.fd = fd;
|
||||
waiting.deadline = now + std::chrono::milliseconds(m_budgetMs);
|
||||
m_waiting.push_back(std::move(waiting));
|
||||
}
|
||||
return MOBILEGL_OK;
|
||||
}
|
||||
|
||||
void PairAcceptor::Read(Waiting& waiting) {
|
||||
switch (waiting.frame.Pump(waiting.fd)) {
|
||||
case Step::NeedMore:
|
||||
return;
|
||||
case Step::Complete:
|
||||
Judge(waiting);
|
||||
return;
|
||||
case Step::EndedEmpty:
|
||||
// A readiness probe - a connect() and a close() - costs an accept and nothing else.
|
||||
MGLOG_D("MG_Remote server: a connection closed before presenting a PairBind (a probe); dropped");
|
||||
::close(waiting.fd);
|
||||
waiting.fd = -1;
|
||||
return;
|
||||
case Step::NotAFrame:
|
||||
RefuseAndClose(waiting.fd, Protocol::RefuseCode::MalformedHello, kFirstFrameNotAFrame);
|
||||
waiting.fd = -1;
|
||||
return;
|
||||
case Step::EndedPartial:
|
||||
RefuseAndClose(waiting.fd, Protocol::RefuseCode::MalformedHello, kFirstFrameTruncated);
|
||||
waiting.fd = -1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// A complete first frame. A PairBind either completes a pair or waits for its partner.
|
||||
void PairAcceptor::Judge(Waiting& waiting) {
|
||||
std::uint8_t nonce[Transport::kPairNonceBytes] = {};
|
||||
bool aux = false;
|
||||
if (!Transport::DecodePairBind(waiting.frame.Payload(), nonce, &aux)) {
|
||||
RefuseUnpairable(waiting);
|
||||
return;
|
||||
}
|
||||
for (auto& other : m_waiting) {
|
||||
if (&other == &waiting || other.fd < 0 || !other.bound ||
|
||||
!Transport::ConstantTimeNonceMatch(other.nonce, nonce))
|
||||
continue;
|
||||
if (other.aux == aux) {
|
||||
MGLOG_W("MG_Remote server: a second %s connection presented a pair nonce that is already waiting",
|
||||
aux ? "aux" : "control");
|
||||
Drop(waiting, false, kPairDuplicate);
|
||||
return;
|
||||
}
|
||||
m_ready.emplace_back(aux ? other.fd : waiting.fd, aux ? waiting.fd : other.fd);
|
||||
other.fd = -1;
|
||||
waiting.fd = -1;
|
||||
return;
|
||||
}
|
||||
waiting.bound = true;
|
||||
waiting.aux = aux;
|
||||
std::memcpy(waiting.nonce, nonce, sizeof(nonce));
|
||||
}
|
||||
|
||||
// A first frame that is not a PairBind. Nothing can be paired with it, and it is told so by name.
|
||||
void PairAcceptor::RefuseUnpairable(Waiting& waiting) {
|
||||
const auto& payload = waiting.frame.Payload();
|
||||
const int fd = waiting.fd;
|
||||
waiting.fd = -1;
|
||||
SocketTransport connection(fd, -1, TransportRole::Server);
|
||||
DrainUnread(fd);
|
||||
const FirstFrameShape shape = ClassifyFirstFrame(payload.data(), payload.size());
|
||||
if (shape == FirstFrameShape::Hello) {
|
||||
// A client from before PairBind (control revision < 4) writes its Hello first. It is
|
||||
// asked what RunSession would ask, in RunSession's order - the token, then the wire - so
|
||||
// an older client hears Refuse{WireFingerprint} exactly as a session would answer it.
|
||||
const auto* hello = Protocol::GetCtrlEnvelope(payload.data())->msg_as_Hello();
|
||||
if (AuthenticatePeerToken(connection, hello->token()) != MOBILEGL_OK) return;
|
||||
if (ValidatePeerHandshake(connection, hello->abiMajor(), hello->abiMinor(), hello->wireFingerprint(),
|
||||
hello->buildFingerprint() ? hello->buildFingerprint()->c_str() : nullptr,
|
||||
hello->dialMode()) != MOBILEGL_OK)
|
||||
return;
|
||||
Refuse(connection, Protocol::RefuseCode::MalformedHello, kPairHelloFirst);
|
||||
return;
|
||||
}
|
||||
if (shape == FirstFrameShape::DataBind) {
|
||||
// RunSession's own words for a DataBind that reaches a listener with no session for it.
|
||||
Refuse(connection, Protocol::RefuseCode::Authentication, "data connection names no live session");
|
||||
return;
|
||||
}
|
||||
Refuse(connection, Protocol::RefuseCode::MalformedHello, FirstFrameRefusalDetail(shape));
|
||||
}
|
||||
|
||||
// Past its deadline a connection is dropped; nothing that is still inside its budget is touched.
|
||||
void PairAcceptor::Expire(Clock::time_point now) {
|
||||
for (auto& waiting : m_waiting) {
|
||||
if (waiting.fd < 0 || now < waiting.deadline) continue;
|
||||
if (waiting.bound)
|
||||
MGLOG_W("MG_Remote server: a%s connection's partner never presented its pair nonce within %u ms; "
|
||||
"dropped",
|
||||
waiting.aux ? "n aux" : " control", m_budgetMs);
|
||||
Drop(waiting, false, waiting.bound ? kPairNoPartner : kPairNoBind);
|
||||
}
|
||||
Compact();
|
||||
}
|
||||
|
||||
void PairAcceptor::Drop(Waiting& waiting, bool busy, const char* detail) {
|
||||
if (waiting.fd < 0) return;
|
||||
if (waiting.bound && waiting.aux) {
|
||||
::shutdown(waiting.fd, SHUT_RDWR);
|
||||
::close(waiting.fd);
|
||||
} else {
|
||||
RefuseAndClose(waiting.fd, busy ? Protocol::RefuseCode::Busy : Protocol::RefuseCode::Authentication,
|
||||
detail);
|
||||
}
|
||||
waiting.fd = -1;
|
||||
}
|
||||
|
||||
void PairAcceptor::Compact() {
|
||||
m_waiting.erase(std::remove_if(m_waiting.begin(), m_waiting.end(),
|
||||
[](const Waiting& waiting) { return waiting.fd < 0; }),
|
||||
m_waiting.end());
|
||||
}
|
||||
|
||||
} // namespace MobileGL::MG_Remote::Server
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,131 @@
|
||||
// MobileGL - MobileGL/MG_Remote/Server/PairAcceptor.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
|
||||
|
||||
// P11 PAIR. A SOCKET CLIENT'S TWO CONNECTIONS ARE PAIRED BY WHO THEY ARE, NOT BY WHEN THEY ARRIVED.
|
||||
//
|
||||
// SocketTransport::ConnectTo opens two connections to one listener - control, then aux (the
|
||||
// SCM_RIGHTS socket; on tcp:// the data connection). The server used to take "the next two accepted
|
||||
// connections, first is control" (SocketTransport::AcceptPair). That was a protocol only while
|
||||
// nothing else could reach the listener, and on the device it was not (B0-CROSS-APP.md F2): the
|
||||
// server app's readiness probe - one connect() and close() - became the next client's control
|
||||
// connection, that session read the probe's socket and ended ("control peer closed before sending
|
||||
// a first frame"), and the client got no Welcome. Two clients connecting at the same moment could
|
||||
// just as well swap halves.
|
||||
//
|
||||
// Now each connection's first frame is a PairBind (protocol.fbs, control revision 4): the client's
|
||||
// nonce, the same on both, and which half it is. This acceptor keeps every accepted connection in a
|
||||
// small set until it has presented one, and hands out a control and an aux connection only when
|
||||
// their nonces match. What it does with everything else:
|
||||
// - a connection that closes having sent nothing (a readiness probe) is closed, quietly;
|
||||
// - a first frame that is not a PairBind is refused by name. A Hello is a client from before
|
||||
// PairBind: it is asked what the session would ask, in the session's order - the token, then the
|
||||
// wire (Handshake.h) - so it hears Refuse{WireFingerprint} as it would from any session, and one
|
||||
// that passes both is still refused, because it cannot be paired;
|
||||
// - a connection that has not presented a PairBind, or whose partner has not presented the same
|
||||
// nonce, within the budget is dropped - a control connection with a refusal, an aux connection
|
||||
// (whose client reads descriptors there, not frames) by closing it - and nobody else's
|
||||
// connection is touched;
|
||||
// - past kPairMaxUnpaired connections the oldest one that has not identified itself (else the
|
||||
// oldest) is dropped the same way.
|
||||
//
|
||||
// It never reads a byte past a PairBind (FirstFrameAssembler): on control the Hello follows it, and
|
||||
// the session reads that itself; on aux the descriptors follow it.
|
||||
//
|
||||
// Stateful because pairs complete out of order: with A-control, B-control, B-aux, A-aux, B is
|
||||
// handed out first and A-control has to still be here for the call that hands out A.
|
||||
|
||||
#pragma once
|
||||
|
||||
#if !defined(_WIN32)
|
||||
|
||||
#include "../Transport/PairBind.h"
|
||||
#include "../Transport/SocketTransport.h"
|
||||
#include "PreAuthGate.h" // FirstFrameAssembler
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL::MG_Remote::Server {
|
||||
|
||||
// How long an accepted connection may stay unpaired: to present its PairBind, and then to be
|
||||
// joined by its partner. The old AcceptPair's wait for a second connection, and ConnectTo's
|
||||
// budget for opening it.
|
||||
inline constexpr std::uint32_t kPairBudgetMs = 2000;
|
||||
// Connections held unpaired at once. A client needs two, and the rendezvous is local.
|
||||
inline constexpr std::size_t kPairMaxUnpaired = 16;
|
||||
|
||||
// The refusal details a peer (and the log) sees.
|
||||
inline constexpr const char* kPairNoBind = "no PairBind within the pairing budget";
|
||||
inline constexpr const char* kPairNoPartner =
|
||||
"no aux connection presented this connection's pair nonce within the pairing budget";
|
||||
inline constexpr const char* kPairHelloFirst =
|
||||
"the control connection's first frame is a Hello, not a PairBind (control revision 4 pairs by nonce)";
|
||||
inline constexpr const char* kPairDuplicate = "a connection with this pair nonce and role is already waiting";
|
||||
inline constexpr const char* kPairFull = "too many connections are waiting to be paired";
|
||||
inline constexpr const char* kPairStopped = "the server stopped accepting before this connection was paired";
|
||||
|
||||
class PairAcceptor {
|
||||
public:
|
||||
using Clock = std::chrono::steady_clock;
|
||||
|
||||
// `listenFd` stays the caller's. `budgetMs` is kPairBudgetMs outside the unit test.
|
||||
explicit PairAcceptor(int listenFd, std::uint32_t budgetMs = kPairBudgetMs);
|
||||
// Every connection still held is let go of (kPairStopped, Busy), with shutdown(2).
|
||||
~PairAcceptor();
|
||||
PairAcceptor(const PairAcceptor&) = delete;
|
||||
PairAcceptor& operator=(const PairAcceptor&) = delete;
|
||||
|
||||
// One client's pair, waiting at most `timeoutMs` for it: OK with `outServer` holding the
|
||||
// control connection as its stream and the aux connection; MOBILEGL_ERR_TIMEOUT when no pair
|
||||
// completed in time (connections still unpaired are KEPT for the next call); and
|
||||
// MOBILEGL_ERR_TRANSPORT_CLOSED when the listener itself failed.
|
||||
MobileGLResult Accept(std::uint32_t timeoutMs, std::unique_ptr<Transport::SocketTransport>& outServer);
|
||||
|
||||
// A forked session child's copy of every held descriptor, closed WITHOUT shutdown(2) - that
|
||||
// would disconnect the supervisor's copy too - and without a word: they are the supervisor's.
|
||||
void CloseInForkedChild();
|
||||
|
||||
// Connections accepted and not yet handed out as a pair.
|
||||
std::size_t Unpaired() const { return m_waiting.size(); }
|
||||
|
||||
private:
|
||||
struct Waiting {
|
||||
int fd = -1;
|
||||
Clock::time_point deadline{};
|
||||
FirstFrameAssembler frame;
|
||||
bool bound = false; // its PairBind has arrived
|
||||
bool aux = false;
|
||||
std::uint8_t nonce[Transport::kPairNonceBytes] = {};
|
||||
};
|
||||
|
||||
MobileGLResult AcceptNew(Clock::time_point now);
|
||||
void Read(Waiting& waiting);
|
||||
void Judge(Waiting& waiting);
|
||||
void RefuseUnpairable(Waiting& waiting);
|
||||
void Expire(Clock::time_point now);
|
||||
// A control connection, or one that never said which it is, is refused (Busy when it was
|
||||
// crowded out or the server stopped, else Authentication) and closed; an aux connection is
|
||||
// closed - its client reads descriptors there, never frames.
|
||||
void Drop(Waiting& waiting, bool busy, const char* detail);
|
||||
void Compact();
|
||||
|
||||
int m_listener;
|
||||
std::uint32_t m_budgetMs;
|
||||
std::vector<Waiting> m_waiting; // accept order, oldest first
|
||||
std::deque<std::pair<int, int>> m_ready; // {control, aux}, not yet handed out
|
||||
Clock::time_point m_acceptResumeAt{};
|
||||
};
|
||||
|
||||
} // namespace MobileGL::MG_Remote::Server
|
||||
|
||||
#endif
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "../Handshake.h"
|
||||
#include "../FatalFunnel.h"
|
||||
#include "InProcessServer.h"
|
||||
#include "PairAcceptor.h"
|
||||
#include "PreAuthGate.h"
|
||||
#include "ServerDisplay.h"
|
||||
#include "ServerLoop.h"
|
||||
@@ -340,7 +341,7 @@ void DelaySessionHandshakeForTest() {
|
||||
|
||||
// Called only after fork (or, in the single-session shape, instead of it): the supervisor never
|
||||
// creates a backend or EGL context. `dataSource` is set on TCP (PH-7 (4)) and empty on a unix
|
||||
// endpoint, whose second connection is the SCM_RIGHTS socket AcceptPair already paired.
|
||||
// endpoint, whose second connection is the SCM_RIGHTS socket PairAcceptor already paired.
|
||||
// `sourceFd` is the descriptor that source reads from - the child's end of the hand-off
|
||||
// socketpair under --serve, the listener itself in the single-session shape - and -1 when there
|
||||
// is none; it is let go of the moment Accept returns, and on every exit before that
|
||||
@@ -1308,16 +1309,17 @@ private:
|
||||
};
|
||||
|
||||
// THE UNIX SHAPE OF THE IN-PROCESS DISPLAY SERVER (P12 D5 extended to unix endpoints). The
|
||||
// endpoint is an @abstract name or a filesystem path. A client's control AND data connections
|
||||
// pair by arrival order (AcceptPair) and the shm segments' descriptors cross by SCM_RIGHTS, so
|
||||
// there is no DataBind to route and no pre-auth gate - both are TCP exposure machinery, and a
|
||||
// unix peer is local by construction (the forked unix supervisor runs without either). Sessions
|
||||
// take the TCP in-process shape's thread hand-off: one at a time, Busy for a second client, the
|
||||
// latch reset between sessions, stop by g_inProcessStop.
|
||||
// endpoint is an @abstract name or a filesystem path. A client's control AND aux connections
|
||||
// pair by the nonce each presents first (PairAcceptor, P11 PAIR - never by arrival order) and the
|
||||
// shm segments' descriptors cross by SCM_RIGHTS, so there is no DataBind to route and no pre-auth
|
||||
// gate - both are TCP exposure machinery, and a unix peer is local by construction (the forked
|
||||
// unix supervisor runs without either). Sessions take the TCP in-process shape's thread hand-off:
|
||||
// one at a time, Busy for a second client, the latch reset between sessions, stop by
|
||||
// g_inProcessStop.
|
||||
class UnixInProcessSupervisor {
|
||||
public:
|
||||
UnixInProcessSupervisor(int listener, unsigned long& sessionsFaulted)
|
||||
: m_listener(listener), m_sessionsFaulted(sessionsFaulted) {}
|
||||
: m_listener(listener), m_pairs(listener), m_sessionsFaulted(sessionsFaulted) {}
|
||||
|
||||
~UnixInProcessSupervisor() {
|
||||
// Run() only returns with the session thread joined; a listener failure (73) is the
|
||||
@@ -1330,7 +1332,7 @@ public:
|
||||
if (g_inProcessStop.load(std::memory_order_acquire)) return StopServing();
|
||||
Reap();
|
||||
std::unique_ptr<SocketTransport> control;
|
||||
const auto accepted = SocketTransport::AcceptPair(m_listener, 250, control);
|
||||
const auto accepted = m_pairs.Accept(250, control);
|
||||
if (accepted == MOBILEGL_ERR_TIMEOUT) continue;
|
||||
if (accepted != MOBILEGL_OK) {
|
||||
WireLogError("MG_Remote server: the in-process unix listener failed (rc=%d)",
|
||||
@@ -1401,6 +1403,8 @@ private:
|
||||
}
|
||||
|
||||
int m_listener;
|
||||
// The connections that have not been paired yet live here between Accept calls.
|
||||
Server::PairAcceptor m_pairs;
|
||||
unsigned long& m_sessionsFaulted;
|
||||
std::thread m_sessionThread;
|
||||
std::atomic<bool> m_sessionDone{false};
|
||||
@@ -1472,6 +1476,10 @@ extern "C" __attribute__((visibility("default"))) int mobilegl_server_main(int a
|
||||
// From here: the unix endpoint (--serve or not), and the single-session TCP shape.
|
||||
pid_t active = -1;
|
||||
const auto reap = [&]() { return ReapActiveSession(active, sessionsFaulted); };
|
||||
// P11 PAIR: a unix client's two connections, paired by the nonce each presents first. The
|
||||
// connections not yet paired stay in it between calls.
|
||||
std::unique_ptr<Server::PairAcceptor> pairs;
|
||||
if (!tcpEndpoint) pairs = std::make_unique<Server::PairAcceptor>(listener);
|
||||
for (;;) {
|
||||
reap();
|
||||
std::unique_ptr<SocketTransport> control;
|
||||
@@ -1481,11 +1489,14 @@ extern "C" __attribute__((visibility("default"))) int mobilegl_server_main(int a
|
||||
accepted = SocketTransport::AcceptOne(listener, 30000, &fd);
|
||||
if (accepted == MOBILEGL_OK) control = std::make_unique<SocketTransport>(fd, -1, TransportRole::Server);
|
||||
} else {
|
||||
accepted = SocketTransport::AcceptPair(listener, serve ? 250 : 30000, control);
|
||||
accepted = pairs->Accept(serve ? 250 : 30000, control);
|
||||
}
|
||||
if (accepted == MOBILEGL_ERR_TIMEOUT && serve) continue;
|
||||
if (accepted != MOBILEGL_OK) { LogSupervisorSummary(sessionsFaulted); ::close(listener); return 73; }
|
||||
if (accepted != MOBILEGL_OK) { pairs.reset(); LogSupervisorSummary(sessionsFaulted); ::close(listener); return 73; }
|
||||
if (!serve) {
|
||||
// The process IS this one session: whatever else is waiting to be paired is let go of by
|
||||
// name now, not held for the session's lifetime.
|
||||
pairs.reset();
|
||||
if (tcpEndpoint) {
|
||||
// The listener stays open until this session's data connection has arrived on
|
||||
// it; RunSession closes it the moment Accept returns. The process IS the session,
|
||||
@@ -1518,6 +1529,10 @@ extern "C" __attribute__((visibility("default"))) int mobilegl_server_main(int a
|
||||
// P12 (D8): the unix `--serve` child dies with its supervisor too.
|
||||
DieWithSupervisor(supervisor);
|
||||
::close(listener);
|
||||
// Every other waiting connection is the supervisor's: closed here without a word, or the
|
||||
// supervisor's refusal of one would not end it while this session lives (TcpSupervisor's
|
||||
// fork does the same with its pending peers).
|
||||
if (pairs) pairs->CloseInForkedChild();
|
||||
ExitSessionProcess(RunSession(std::move(control), {}, {}, -1, DataSource::None, 10000));
|
||||
}
|
||||
if (child < 0) {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// MobileGL - MobileGL/MG_Remote/Transport/PairBind.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
|
||||
|
||||
// P11 PAIR. The PairBind frame (protocol.fbs), both directions of it: SocketTransport::ConnectTo
|
||||
// writes one as the first frame of each of a client's two connections, and Server/PairAcceptor
|
||||
// reads them and pairs the two halves by the nonce. Here, not in Handshake.h beside the DataBind
|
||||
// codec, because the transport itself writes it and Handshake.h brings the logger's umbrella
|
||||
// header with it (WireLog.h says why Transport keeps that out).
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../Protocol/generated/protocol_generated.h"
|
||||
#include "AuthToken.h" // kDataNonceBytes
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL::MG_Remote::Transport {
|
||||
|
||||
// 128 bits from the client's CSPRNG, the width of Welcome.dataNonce, so the nonce comparison
|
||||
// (ConstantTimeNonceMatch) is the same one.
|
||||
inline constexpr std::size_t kPairNonceBytes = kDataNonceBytes;
|
||||
|
||||
inline std::vector<std::uint8_t> EncodePairBind(const std::uint8_t (&nonce)[kPairNonceBytes], bool aux) {
|
||||
::flatbuffers::FlatBufferBuilder builder(64);
|
||||
auto bind = ::MobileGL::Wire::CreatePairBind(builder, builder.CreateVector(nonce, kPairNonceBytes), aux);
|
||||
auto envelope = ::MobileGL::Wire::CreateCtrlEnvelope(builder, ::MobileGL::Wire::CtrlMsg::PairBind,
|
||||
bind.Union());
|
||||
::MobileGL::Wire::FinishCtrlEnvelopeBuffer(builder, envelope);
|
||||
return std::vector<std::uint8_t>(builder.GetBufferPointer(), builder.GetBufferPointer() + builder.GetSize());
|
||||
}
|
||||
|
||||
// True when `frame` is a verifiable CtrlEnvelope carrying a PairBind whose nonce is exactly
|
||||
// kPairNonceBytes; the nonce and the role are copied out. Anything else is false.
|
||||
inline bool DecodePairBind(const std::vector<std::uint8_t>& frame, std::uint8_t (&nonce)[kPairNonceBytes],
|
||||
bool* outAux) {
|
||||
if (outAux == nullptr || frame.size() < 8 || !::MobileGL::Wire::CtrlEnvelopeBufferHasIdentifier(frame.data()))
|
||||
return false;
|
||||
::flatbuffers::Verifier verifier(frame.data(), frame.size());
|
||||
if (!::MobileGL::Wire::VerifyCtrlEnvelopeBuffer(verifier)) return false;
|
||||
const auto* bind = ::MobileGL::Wire::GetCtrlEnvelope(frame.data())->msg_as_PairBind();
|
||||
if (bind == nullptr || bind->nonce() == nullptr || bind->nonce()->size() != kPairNonceBytes) return false;
|
||||
std::memcpy(nonce, bind->nonce()->data(), kPairNonceBytes);
|
||||
*outAux = bind->aux();
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace MobileGL::MG_Remote::Transport
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "AuthToken.h"
|
||||
#include "Doorbell.h" // kWaitForever
|
||||
#include "FdPassing.h"
|
||||
#include "PairBind.h" // P11 PAIR: ConnectTo's first frame on each connection
|
||||
#include "WireLog.h"
|
||||
|
||||
#include <cerrno>
|
||||
@@ -315,6 +316,31 @@ namespace MobileGL::MG_Remote::Transport {
|
||||
::usleep(5000);
|
||||
}
|
||||
}
|
||||
|
||||
// One more connection to `path` whose first bytes are `payload`, framed: a DataBind
|
||||
// (ConnectDataConnection) or a PairBind (ConnectTo). `what` names the frame in the log.
|
||||
MobileGLResult ConnectPresenting(const std::string& path, std::uint32_t timeoutMs, const void* payload,
|
||||
std::uint64_t size, const char* what, int* outFd) {
|
||||
*outFd = -1;
|
||||
std::vector<std::uint8_t> framed;
|
||||
const MobileGLResult appended = AppendFrame(framed, payload, size);
|
||||
if (appended != MOBILEGL_OK) return appended;
|
||||
int fd = -1;
|
||||
const MobileGLResult connected = ConnectOne(path, timeoutMs, &fd);
|
||||
if (connected != MOBILEGL_OK) return connected;
|
||||
std::size_t written = 0;
|
||||
while (written < framed.size()) {
|
||||
const ssize_t n = ::send(fd, framed.data() + written, framed.size() - written, MSG_NOSIGNAL);
|
||||
if (n > 0) { written += static_cast<std::size_t>(n); continue; }
|
||||
if (n < 0 && errno == EINTR) continue;
|
||||
WireLogError("MG_Remote SocketTransport: the %s could not be sent (%zu of %zu bytes): %s", what,
|
||||
written, framed.size(), std::strerror(errno));
|
||||
::close(fd);
|
||||
return MOBILEGL_ERR_TRANSPORT_CLOSED;
|
||||
}
|
||||
*outFd = fd;
|
||||
return MOBILEGL_OK;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
MobileGLResult SocketTransport::Listen(const std::string& path, int* outListenFd) {
|
||||
@@ -370,68 +396,30 @@ namespace MobileGL::MG_Remote::Transport {
|
||||
return MOBILEGL_OK;
|
||||
}
|
||||
|
||||
MobileGLResult SocketTransport::AcceptPair(int listenFd, std::uint32_t timeoutMs,
|
||||
std::unique_ptr<SocketTransport>& outServer) {
|
||||
outServer.reset();
|
||||
if (listenFd < 0) {
|
||||
return MOBILEGL_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
int accepted[2] = {-1, -1};
|
||||
for (int index = 0; index < 2; ++index) {
|
||||
const std::uint32_t budget = index == 0 ? timeoutMs : 2000;
|
||||
for (;;) {
|
||||
const int ready = WaitReadable(listenFd, budget);
|
||||
if (ready <= 0) {
|
||||
for (int fd : accepted) {
|
||||
if (fd >= 0) ::close(fd);
|
||||
}
|
||||
// Half a client is not a client. Both connections or neither, so
|
||||
// a peer that died between the two cannot leave a session that
|
||||
// looks up but has no way to receive a descriptor.
|
||||
if (index != 0 || ready < 0) WireLogError("MG_Remote SocketTransport: only %d of 2 connections arrived within "
|
||||
"%u ms", index, timeoutMs);
|
||||
return ready == 0 ? MOBILEGL_ERR_TIMEOUT : MOBILEGL_ERR_TRANSPORT_CLOSED;
|
||||
}
|
||||
accepted[index] = ::accept(listenFd, nullptr, nullptr);
|
||||
if (accepted[index] >= 0) break;
|
||||
// EINTR (this process reaps SIGCHLD by construction) and ECONNABORTED
|
||||
// (the peer reset between poll() and accept()) are not transport
|
||||
// failures: the listener is still good. Re-poll within the budget
|
||||
// rather than returning TRANSPORT_CLOSED, which the --serve
|
||||
// supervisor turns into a full server exit (return 73).
|
||||
if (errno == EINTR || errno == ECONNABORTED) continue;
|
||||
for (int fd : accepted) {
|
||||
if (fd >= 0) ::close(fd);
|
||||
}
|
||||
WireLogError("MG_Remote SocketTransport: accept failed: %s", std::strerror(errno));
|
||||
return MOBILEGL_ERR_TRANSPORT_CLOSED;
|
||||
}
|
||||
::fcntl(accepted[index], F_SETFD, FD_CLOEXEC);
|
||||
if (TcpSocket(accepted[index]) && !ConfigureTcp(accepted[index])) {
|
||||
for (int fd : accepted) if (fd >= 0) ::close(fd);
|
||||
return MOBILEGL_ERR_UNSUPPORTED;
|
||||
}
|
||||
}
|
||||
// First connection is control, second is aux. The order IS the protocol;
|
||||
// it is stated here and in ConnectTo and nowhere else.
|
||||
outServer = std::make_unique<SocketTransport>(accepted[0], accepted[1],
|
||||
TransportRole::Server);
|
||||
return MOBILEGL_OK;
|
||||
}
|
||||
|
||||
MobileGLResult SocketTransport::ConnectTo(const std::string& path, std::uint32_t timeoutMs,
|
||||
std::unique_ptr<SocketTransport>& outClient) {
|
||||
outClient.reset();
|
||||
// P11 PAIR. Both connections present the same nonce as their first frame, each saying
|
||||
// which half it is; the server pairs them by it (Server/PairAcceptor.h), so neither the
|
||||
// order they arrive in nor anybody else's connection in between decides the pairing.
|
||||
std::uint8_t nonce[kPairNonceBytes];
|
||||
const MobileGLResult minted = MintNonce(nonce, sizeof(nonce));
|
||||
if (minted != MOBILEGL_OK) return minted;
|
||||
const auto controlBind = EncodePairBind(nonce, false);
|
||||
int control = -1;
|
||||
const MobileGLResult first = ConnectOne(path, timeoutMs, &control);
|
||||
const MobileGLResult first =
|
||||
ConnectPresenting(path, timeoutMs, controlBind.data(), controlBind.size(), "control connection's PairBind",
|
||||
&control);
|
||||
if (first != MOBILEGL_OK) {
|
||||
return first;
|
||||
}
|
||||
const auto auxBind = EncodePairBind(nonce, true);
|
||||
int aux = -1;
|
||||
// The second connect has a SHORT budget: the server is provably up - we
|
||||
// just connected to it - so a slow second connection means something is
|
||||
// wrong rather than something is starting.
|
||||
const MobileGLResult second = ConnectOne(path, 2000, &aux);
|
||||
const MobileGLResult second =
|
||||
ConnectPresenting(path, 2000, auxBind.data(), auxBind.size(), "aux connection's PairBind", &aux);
|
||||
if (second != MOBILEGL_OK) {
|
||||
::close(control);
|
||||
return second;
|
||||
@@ -455,24 +443,8 @@ namespace MobileGL::MG_Remote::Transport {
|
||||
MobileGLByteSpan firstFrame, int* outFd) {
|
||||
if (outFd == nullptr || !TcpName(path)) return MOBILEGL_ERR_INVALID_ARGUMENT;
|
||||
*outFd = -1;
|
||||
std::vector<std::uint8_t> framed;
|
||||
const MobileGLResult appended = AppendFrame(framed, firstFrame.data, firstFrame.size);
|
||||
if (appended != MOBILEGL_OK) return appended;
|
||||
int fd = -1;
|
||||
const MobileGLResult connected = ConnectOne(path, timeoutMs, &fd);
|
||||
if (connected != MOBILEGL_OK) return connected;
|
||||
std::size_t written = 0;
|
||||
while (written < framed.size()) {
|
||||
const ssize_t n = ::send(fd, framed.data() + written, framed.size() - written, MSG_NOSIGNAL);
|
||||
if (n > 0) { written += static_cast<std::size_t>(n); continue; }
|
||||
if (n < 0 && errno == EINTR) continue;
|
||||
WireLogError("MG_Remote SocketTransport: the data connection's DataBind could not be "
|
||||
"sent (%zu of %zu bytes): %s", written, framed.size(), std::strerror(errno));
|
||||
::close(fd);
|
||||
return MOBILEGL_ERR_TRANSPORT_CLOSED;
|
||||
}
|
||||
*outFd = fd;
|
||||
return MOBILEGL_OK;
|
||||
return ConnectPresenting(path, timeoutMs, firstFrame.data, firstFrame.size, "data connection's DataBind",
|
||||
outFd);
|
||||
}
|
||||
|
||||
MobileGLResult SocketTransport::AcceptOne(int listenFd, std::uint32_t timeoutMs, int* outFd) {
|
||||
@@ -484,7 +456,9 @@ namespace MobileGL::MG_Remote::Transport {
|
||||
if (ready < 0) return MOBILEGL_ERR_TRANSPORT_CLOSED;
|
||||
const int fd = ::accept(listenFd, nullptr, nullptr);
|
||||
if (fd < 0) {
|
||||
// The same two non-failures AcceptPair re-polls on.
|
||||
// EINTR (this process reaps SIGCHLD by construction) and ECONNABORTED (the peer
|
||||
// reset between poll() and accept()) are not listener failures: re-poll rather
|
||||
// than return TRANSPORT_CLOSED, which a supervisor turns into a full server exit (73).
|
||||
if (errno == EINTR || errno == ECONNABORTED) continue;
|
||||
const int error = errno;
|
||||
WireLogError("MG_Remote SocketTransport: accept failed: %s", std::strerror(error));
|
||||
|
||||
@@ -78,10 +78,16 @@ namespace MobileGL::MG_Remote::Transport {
|
||||
// offer is a sendmsg whose ancillary data rides with specific BYTES, and
|
||||
// interleaving those bytes with the framed control stream would make the
|
||||
// frame reassembler and the descriptor receiver race for the same bytes.
|
||||
// So the client connects twice to the same listening path - first
|
||||
// connection is control, second is aux - and the server accepts them in
|
||||
// that order. One name, no extra configuration, and FdPassing keeps the
|
||||
// dedicated socket it was written for.
|
||||
// So the client connects twice to the same listening path - control, then
|
||||
// aux - and each connection's first frame is a PairBind carrying one nonce
|
||||
// and which half it is. One name, no extra configuration, and FdPassing
|
||||
// keeps the dedicated socket it was written for.
|
||||
//
|
||||
// P11 PAIR: THE SERVER PAIRS THEM BY THAT NONCE, NOT BY ARRIVAL ORDER
|
||||
// (Server/PairAcceptor.h). "First is control, second is aux" was a protocol
|
||||
// only while nothing else could reach the listener: a readiness probe's
|
||||
// lone connect() + close() became the next client's control connection, and
|
||||
// two clients connecting at once could swap halves (B0-CROSS-APP.md F2).
|
||||
//
|
||||
// `path` is a filesystem AF_UNIX path. ARCHITECTURE.md §15.1 ruled
|
||||
// against one for the FORK shape, where the fds were inherited and a
|
||||
@@ -92,27 +98,26 @@ namespace MobileGL::MG_Remote::Transport {
|
||||
// the machine.
|
||||
static MobileGLResult Listen(const std::string& path, int* outListenFd);
|
||||
|
||||
// Accepts one client's TWO connections, in order.
|
||||
static MobileGLResult AcceptPair(int listenFd, std::uint32_t timeoutMs,
|
||||
std::unique_ptr<SocketTransport>& outServer);
|
||||
|
||||
// The client's half: connects twice to `path`.
|
||||
// The client's half: connects twice to `path`, each connection presenting a PairBind
|
||||
// (the same fresh nonce; control, then aux) as its first frame. The server's half is
|
||||
// Server::PairAcceptor.
|
||||
static MobileGLResult ConnectTo(const std::string& path, std::uint32_t timeoutMs,
|
||||
std::unique_ptr<SocketTransport>& outClient);
|
||||
|
||||
// ---- PH-7 (4), ID-P7-3: a TCP data connection is BOUND, not paired --------
|
||||
//
|
||||
// AcceptPair/ConnectTo pair a session's two connections by ARRIVAL ORDER inside a
|
||||
// window: first is control, second is data. That is a protocol only while nothing sits
|
||||
// between the two processes. Over a Windows `adb forward` the two arrive reordered, the
|
||||
// data connection is read as control, and every session child exits 67 waiting for a
|
||||
// The server used to pair a session's two connections by ARRIVAL ORDER inside a window
|
||||
// (AcceptPair): first is control, second is data. That is a protocol only while nothing
|
||||
// sits between the two processes. Over a Windows `adb forward` the two arrive reordered,
|
||||
// the data connection is read as control, and every session child exits 67 waiting for a
|
||||
// Hello that went down the other socket - and anybody who can reach the port can race a
|
||||
// connection into the gap and become somebody else's data plane. So on TCP the session
|
||||
// is ONE connection until it is authenticated; the server mints `Welcome.dataNonce`,
|
||||
// and the client then opens its data connection and presents the nonce as that
|
||||
// connection's first frame (DataBind). The server matches the value, not the order.
|
||||
// Unix endpoints keep AcceptPair: their second connection is the SCM_RIGHTS socket of a
|
||||
// same-user rendezvous, not a data plane anybody else can reach.
|
||||
// Unix endpoints keep their two connections from the start (the second is the SCM_RIGHTS
|
||||
// socket the Welcome's descriptors cross), and since P11 PAIR they too are matched by a
|
||||
// value - the client's PairBind nonce - not by order.
|
||||
|
||||
// Connects the control connection only on `tcp://`; identical to ConnectTo otherwise.
|
||||
static MobileGLResult ConnectControl(const std::string& path, std::uint32_t timeoutMs,
|
||||
@@ -122,7 +127,8 @@ namespace MobileGL::MG_Remote::Transport {
|
||||
// StreamLink to.
|
||||
static MobileGLResult ConnectDataConnection(const std::string& path, std::uint32_t timeoutMs,
|
||||
MobileGLByteSpan firstFrame, int* outFd);
|
||||
// Accepts ONE connection (TCP options applied, CLOEXEC). MOBILEGL_ERR_TIMEOUT when none.
|
||||
// Accepts ONE connection (TCP options applied, CLOEXEC; EINTR and ECONNABORTED re-polled -
|
||||
// the listener is still good). MOBILEGL_ERR_TIMEOUT when none.
|
||||
// MOBILEGL_ERR_OUT_OF_MEMORY when accept(2) ran out of descriptors or memory (EMFILE,
|
||||
// ENFILE, ENOBUFS, ENOMEM): the connection stays in the backlog and the listener is fine,
|
||||
// so a caller that serves many peers can wait and try again (the TCP supervisor does);
|
||||
|
||||
Reference in New Issue
Block a user