Files
MobileGL/MobileGL/MG_Remote/Transport/SocketTransport.cpp
T

700 lines
32 KiB
C++

// MobileGL - MobileGL/MG_Remote/Transport/SocketTransport.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
#include "SocketTransport.h"
#include "Doorbell.h" // kWaitForever
#include "FdPassing.h"
#include "WireLog.h"
#include <cerrno>
#include <cstddef>
#include <cstring>
#include <memory>
#include <chrono>
#include <cstdlib>
#include <algorithm>
#include <cstdio>
#if defined(__unix__) || defined(__APPLE__) || defined(__ANDROID__)
# define MOBILEGL_SOCKET_TRANSPORT_POSIX 1
# include <poll.h>
# include <sys/stat.h>
# include <sys/un.h>
# include <sys/socket.h>
# include <sys/types.h>
# include <unistd.h>
# include <fcntl.h>
# include <netdb.h>
# include <netinet/in.h>
# include <netinet/tcp.h>
#else
# define MOBILEGL_SOCKET_TRANSPORT_POSIX 0
#endif
namespace MobileGL::MG_Remote::Transport {
#if MOBILEGL_SOCKET_TRANSPORT_POSIX
namespace {
constexpr std::uint64_t kPumpChunkBytes = 64ull * 1024;
bool TcpName(const std::string& name) { return name.compare(0, 6, "tcp://") == 0; }
bool TcpAddress(const std::string& name, addrinfo** result) {
*result = nullptr;
const std::string address = name.substr(6);
std::string host, port;
if (!address.empty() && address[0] == '[') {
const auto end = address.find(']');
if (end == std::string::npos || end + 1 >= address.size() || address[end + 1] != ':')
return false;
host = address.substr(1, end - 1);
port = address.substr(end + 2);
} else {
const auto colon = address.find(':');
if (colon == std::string::npos || address.find(':', colon + 1) != std::string::npos)
return false;
host = address.substr(0, colon);
port = address.substr(colon + 1);
}
if (host.empty() || port.empty() || port.find_first_not_of("0123456789") != std::string::npos)
return false;
char* end = nullptr;
const auto number = std::strtoul(port.c_str(), &end, 10);
if (*end || number == 0 || number > 65535) return false;
addrinfo hints{};
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
hints.ai_flags = AI_NUMERICSERV;
return ::getaddrinfo(host.c_str(), port.c_str(), &hints, result) == 0;
}
bool Loopback(const sockaddr* address) {
if (address->sa_family == AF_INET)
return (ntohl(reinterpret_cast<const sockaddr_in*>(address)->sin_addr.s_addr) >> 24) == 127;
if (address->sa_family == AF_INET6)
return IN6_IS_ADDR_LOOPBACK(&reinterpret_cast<const sockaddr_in6*>(address)->sin6_addr);
return false;
}
bool TcpSocket(int fd) {
sockaddr_storage address{};
socklen_t size = sizeof(address);
return ::getsockname(fd, reinterpret_cast<sockaddr*>(&address), &size) == 0 &&
(address.ss_family == AF_INET || address.ss_family == AF_INET6);
}
bool ConfigureTcp(int fd) {
const int one = 1, idle = 2, interval = 1, count = 3, timeout = 5000;
if (::setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one)) != 0 ||
::setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &one, sizeof(one)) != 0) return false;
#if defined(TCP_KEEPIDLE)
if (::setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, &idle, sizeof(idle)) != 0) return false;
#endif
#if defined(TCP_KEEPINTVL)
if (::setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, &interval, sizeof(interval)) != 0) return false;
#endif
#if defined(TCP_KEEPCNT)
if (::setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, &count, sizeof(count)) != 0) return false;
#endif
#if defined(TCP_USER_TIMEOUT)
if (::setsockopt(fd, IPPROTO_TCP, TCP_USER_TIMEOUT, &timeout, sizeof(timeout)) != 0) return false;
#endif
return true;
}
MobileGLResult ConnectTcp(const std::string& name, std::uint32_t timeoutMs, int* outFd) {
addrinfo* addresses = nullptr;
if (!TcpAddress(name, &addresses)) return MOBILEGL_ERR_INVALID_ARGUMENT;
const auto deadline = std::chrono::steady_clock::now() +
std::chrono::milliseconds(std::max(1u, timeoutMs));
do {
for (auto* address = addresses; address; address = address->ai_next) {
const int fd = ::socket(address->ai_family, SOCK_STREAM, IPPROTO_TCP);
if (fd < 0) continue;
const int flags = ::fcntl(fd, F_GETFL, 0);
::fcntl(fd, F_SETFL, flags | O_NONBLOCK);
bool connected = ::connect(fd, address->ai_addr, address->ai_addrlen) == 0;
if (!connected && errno == EINPROGRESS) {
const auto remaining = std::chrono::duration_cast<std::chrono::milliseconds>(
deadline - std::chrono::steady_clock::now()).count();
pollfd pfd{fd, POLLOUT, 0};
int rc;
do { rc = ::poll(&pfd, 1, static_cast<int>(std::max<std::int64_t>(0, remaining))); }
while (rc < 0 && errno == EINTR && std::chrono::steady_clock::now() < deadline);
int error = 0;
socklen_t length = sizeof(error);
connected = rc > 0 && ::getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &length) == 0 && error == 0;
}
if (connected && ConfigureTcp(fd)) {
::fcntl(fd, F_SETFL, flags);
::fcntl(fd, F_SETFD, FD_CLOEXEC);
*outFd = fd;
::freeaddrinfo(addresses);
return MOBILEGL_OK;
}
::close(fd);
if (std::chrono::steady_clock::now() >= deadline) break;
}
::usleep(5000);
} while (std::chrono::steady_clock::now() < deadline);
::freeaddrinfo(addresses);
WireLogError("MG_Remote SocketTransport: TCP connect to %s exhausted %u ms", name.c_str(), timeoutMs);
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
MobileGLResult ListenTcp(const std::string& name, int* outFd) {
addrinfo* addresses = nullptr;
if (!TcpAddress(name, &addresses)) return MOBILEGL_ERR_INVALID_ARGUMENT;
const char* token = std::getenv("MOBILEGL_IPC_TOKEN");
const bool authenticated = token != nullptr && token[0] != '\0';
MobileGLResult result = MOBILEGL_ERR_UNSUPPORTED;
for (auto* address = addresses; address; address = address->ai_next) {
if (!authenticated && !Loopback(address->ai_addr)) {
WireLogError("MG_Remote: Refuse{AuthenticationRequired} non-loopback TCP listen needs MOBILEGL_IPC_TOKEN");
result = MOBILEGL_ERR_PROTOCOL_MISMATCH;
continue;
}
const int fd = ::socket(address->ai_family, SOCK_STREAM, IPPROTO_TCP);
if (fd < 0) continue;
const int one = 1;
::setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
::fcntl(fd, F_SETFD, FD_CLOEXEC);
if (::bind(fd, address->ai_addr, address->ai_addrlen) == 0 && ::listen(fd, 8) == 0) {
*outFd = fd;
result = MOBILEGL_OK;
break;
}
::close(fd);
}
::freeaddrinfo(addresses);
return result;
}
void CloseIfOpen(int& fd) {
if (fd >= 0) {
::close(fd);
fd = -1;
}
}
// poll() for readability. Returns:
// 1 readable 0 timed out -1 error (errno set)
// EINTR is retried rather than reported: a signal is not a transport
// event, and reporting it as one would make every caller's timeout
// budget a lie on a process that takes signals (and `sm`'s child takes
// SIGCHLD by construction).
int WaitReadable(int fd, std::uint32_t timeoutMs) {
for (;;) {
struct pollfd pfd {};
pfd.fd = fd;
pfd.events = POLLIN;
const int timeout =
timeoutMs == kWaitForever ? -1 : static_cast<int>(timeoutMs);
const int rc = ::poll(&pfd, 1, timeout);
if (rc < 0 && errno == EINTR) {
continue;
}
return rc;
}
}
} // namespace
namespace {
// A filesystem AF_UNIX path, bounded by sun_path. Refused BY NAME when it
// does not fit rather than silently truncated - a truncated path is a
// different socket, and both sides would then "succeed" onto two
// different names.
// A LEADING '@' MEANS THE ABSTRACT NAMESPACE, which is the convention systemd and D-Bus
// use and the only rendezvous that works on Android.
//
// A filesystem AF_UNIX endpoint needs a writable directory, and an Android app has no
// /tmp - measured, not assumed: the first device run of the spawn arm launched its server
// fine and then died on `could not connect to "/tmp/mgl-15731-...sock" ... No such file or
// directory`, because bind() in the child had nowhere to put the node. The app's own cache
// dir would work, but only if every integrator remembered to say where it is, and the
// library has no way to ask.
//
// An abstract name has no filesystem presence at all: no directory, no mode bits, no stale
// node to unlink after a crash - which is the bind() failure `Listen` currently works
// around. It is scoped to the network namespace, so the two processes of ONE app find each
// other and nothing else can, which is exactly the reach this needs.
//
// Linux-only by construction. A path without '@' still works everywhere, and that is what
// an explicit endpoint (P12's "connect to something somebody else is listening on") uses.
bool IsAbstractEndpoint(const std::string& path) {
return !path.empty() && path[0] == '@';
}
bool FillAddress(const std::string& path, sockaddr_un* out, socklen_t* outLen) {
std::memset(out, 0, sizeof(*out));
out->sun_family = AF_UNIX;
if (path.empty() || path.size() >= sizeof(out->sun_path)) {
WireLogError("MG_Remote SocketTransport: endpoint path is empty or longer than "
"sun_path allows (%zu >= %zu): \"%s\"",
path.size(), sizeof(out->sun_path), path.c_str());
return false;
}
if (IsAbstractEndpoint(path)) {
// sun_path[0] STAYS NUL and the rest is the name. The length carries the name's
// end - there is no terminator - so it is offsetof + 1 + (size - 1), which is
// offsetof + size. Getting this wrong does not fail: it binds a DIFFERENT name
// (one padded with NULs), and the peer then connects to something that is not
// there.
std::memcpy(out->sun_path + 1, path.c_str() + 1, path.size() - 1);
*outLen = static_cast<socklen_t>(offsetof(sockaddr_un, sun_path) + path.size());
return true;
}
std::memcpy(out->sun_path, path.c_str(), path.size());
*outLen = static_cast<socklen_t>(offsetof(sockaddr_un, sun_path) + path.size() + 1);
return true;
}
MobileGLResult ConnectOne(const std::string& path, std::uint32_t timeoutMs, int* outFd) {
if (TcpName(path)) return ConnectTcp(path, timeoutMs, outFd);
sockaddr_un address{};
socklen_t length = 0;
if (!FillAddress(path, &address, &length)) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
// Retry with a bound budget: the server may not have reached bind()
// yet. An exhausted budget is a NAMED refusal, never a fallback -
// CONTRACT-P6 §3.4 calls for a bounded retry and says so.
const std::uint32_t deadline = timeoutMs == 0 ? 1 : timeoutMs;
for (std::uint32_t waited = 0;; waited += 5) {
const int fd = ::socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) {
WireLogError("MG_Remote SocketTransport: socket() failed: %s",
std::strerror(errno));
return MOBILEGL_ERR_UNSUPPORTED;
}
if (::connect(fd, reinterpret_cast<sockaddr*>(&address), length) == 0) {
*outFd = fd;
return MOBILEGL_OK;
}
const int failure = errno;
::close(fd);
if (waited >= deadline) {
WireLogError("MG_Remote SocketTransport: could not connect to \"%s\" within "
"%u ms: %s - REFUSING BY NAME, there is no monolith fallback "
"from here",
path.c_str(), deadline, std::strerror(failure));
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
::usleep(5000);
}
}
} // namespace
MobileGLResult SocketTransport::Listen(const std::string& path, int* outListenFd) {
if (outListenFd == nullptr) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
*outListenFd = -1;
if (TcpName(path)) return ListenTcp(path, outListenFd);
sockaddr_un address{};
socklen_t length = 0;
if (!FillAddress(path, &address, &length)) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
// A stale socket file from a server that died is the normal case, not the
// exceptional one, and bind() fails on it with EADDRINUSE. Unlinking first
// is what every AF_UNIX server does; the alternative is a server that
// cannot restart until someone cleans up by hand. An ABSTRACT name has no
// node to go stale and unlink("@...") would delete a file of that literal
// name if one happened to exist.
const bool abstractEndpoint = IsAbstractEndpoint(path);
if (!abstractEndpoint) {
::unlink(path.c_str());
}
const int listenFd = ::socket(AF_UNIX, SOCK_STREAM, 0);
if (listenFd < 0) {
WireLogError("MG_Remote SocketTransport: socket() failed: %s", std::strerror(errno));
return MOBILEGL_ERR_UNSUPPORTED;
}
if (::bind(listenFd, reinterpret_cast<sockaddr*>(&address), length) != 0) {
WireLogError("MG_Remote SocketTransport: bind(\"%s\") failed: %s", path.c_str(),
std::strerror(errno));
::close(listenFd);
return MOBILEGL_ERR_UNSUPPORTED;
}
// 0600: the rendezvous is between one user's processes. An AF_UNIX path
// is a filesystem object and its mode is the only access control there is.
// An abstract name is not a filesystem object and has none; its reach is
// the network namespace, which for an Android app is the app.
if (!abstractEndpoint) {
::chmod(path.c_str(), 0600);
}
if (::listen(listenFd, 4) != 0) {
WireLogError("MG_Remote SocketTransport: listen(\"%s\") failed: %s", path.c_str(),
std::strerror(errno));
::close(listenFd);
if (!abstractEndpoint) {
::unlink(path.c_str());
}
return MOBILEGL_ERR_UNSUPPORTED;
}
*outListenFd = listenFd;
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 int ready = WaitReadable(listenFd, index == 0 ? timeoutMs : 2000);
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) {
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();
int control = -1;
const MobileGLResult first = ConnectOne(path, timeoutMs, &control);
if (first != MOBILEGL_OK) {
return first;
}
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);
if (second != MOBILEGL_OK) {
::close(control);
return second;
}
outClient = std::make_unique<SocketTransport>(control, aux, TransportRole::Client);
return MOBILEGL_OK;
}
MobileGLResult SocketTransport::CreatePair(std::unique_ptr<SocketTransport>& outClient,
std::unique_ptr<SocketTransport>& outServer) {
outClient.reset();
outServer.reset();
int stream[2] = {-1, -1};
// SOCK_STREAM, not SOCK_SEQPACKET: Doorbell.h:390-398 already chose a
// stream socket for the bell so that a closed peer surfaces as
// POLLIN|POLLHUP with recv()==0 rather than as a hang, and the control
// plane must not disagree with the bell about what death looks like.
// Framing.h is what makes a stream safe: the message boundary is in the
// payload, not in the socket.
if (::socketpair(AF_UNIX, SOCK_STREAM, 0, stream) != 0) {
WireLogError("MG_Remote SocketTransport: socketpair(SOCK_STREAM) failed: %s",
std::strerror(errno));
return MOBILEGL_ERR_UNSUPPORTED;
}
int aux[2] = {-1, -1};
const MobileGLResult auxResult = FdPassing::CreateSocketPair(aux);
if (auxResult != MOBILEGL_OK) {
::close(stream[0]);
::close(stream[1]);
WireLogError("MG_Remote SocketTransport: the aux (SCM_RIGHTS) pair failed");
return auxResult;
}
outClient = std::make_unique<SocketTransport>(stream[0], aux[0], TransportRole::Client);
outServer = std::make_unique<SocketTransport>(stream[1], aux[1], TransportRole::Server);
return MOBILEGL_OK;
}
SocketTransport::SocketTransport(int streamFd, int auxFd, TransportRole role)
: m_streamFd(streamFd), m_auxFd(auxFd), m_tcp(TcpSocket(streamFd)), m_role(role) {}
int SocketTransport::TakeDataFd() {
if (!m_tcp) return -1;
const int fd = m_auxFd;
m_auxFd = -1;
return fd;
}
void SocketTransport::CloseLocalCopy() {
CloseIfOpen(m_streamFd);
CloseIfOpen(m_auxFd);
m_closed = true;
}
SocketTransport::~SocketTransport() { Shutdown(); }
MobileGLResult SocketTransport::SendFrame(MobileGLByteSpan bytes) {
if (bytes.size > kMaxFramePayloadSize) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
if (bytes.size != 0 && bytes.data == nullptr) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
std::lock_guard<std::mutex> lock(m_sendMutex);
if (m_closed || m_streamFd < 0) {
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
// Framed into ONE buffer and written as one span. Not because a stream
// needs it - it does not - but because a partial write of the header
// followed by a failure would leave the peer's reassembler parked on a
// length it will never receive, which is indistinguishable from a slow
// sender and therefore not detectable at all.
std::vector<std::uint8_t> framed;
framed.reserve(static_cast<std::size_t>(kFrameHeaderSize + bytes.size));
const MobileGLResult appended = AppendFrame(framed, bytes.data, bytes.size);
if (appended != MOBILEGL_OK) {
return appended;
}
std::uint64_t written = 0;
while (written < framed.size()) {
const ssize_t n = ::send(m_streamFd, framed.data() + written,
static_cast<std::size_t>(framed.size() - written),
MSG_NOSIGNAL);
if (n > 0) {
written += static_cast<std::uint64_t>(n);
continue;
}
if (n < 0 && errno == EINTR) {
continue;
}
// EPIPE / ECONNRESET are the peer being gone, which is a transport
// state and not an error to shout about: the device-lost latch (dl)
// is what turns it into a GL-visible fact.
if (n < 0 && (errno == EPIPE || errno == ECONNRESET)) {
m_closed = true;
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
// SendFrame is also the log sink: do not recurse into the logger.
std::fprintf(stderr, "MG_Remote SocketTransport: send failed after %llu of %llu bytes: %s\n",
static_cast<unsigned long long>(written),
static_cast<unsigned long long>(framed.size()), std::strerror(errno));
m_closed = true;
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
return MOBILEGL_OK;
}
MobileGLResult SocketTransport::PumpOnce(std::uint32_t timeoutMs) {
// Caller holds m_recvMutex.
if (m_failed) {
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
if (m_streamFd < 0) {
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
const int ready = WaitReadable(m_streamFd, timeoutMs);
if (ready == 0) {
return MOBILEGL_ERR_TIMEOUT;
}
if (ready < 0) {
WireLogError("MG_Remote SocketTransport: poll failed: %s", std::strerror(errno));
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
std::uint8_t chunk[kPumpChunkBytes];
for (;;) {
const ssize_t n = ::recv(m_streamFd, chunk, sizeof(chunk), 0);
if (n > 0) {
const MobileGLResult fed =
m_reader.Feed(chunk, static_cast<std::uint64_t>(n));
if (fed != MOBILEGL_OK) {
// Bad magic or an over-long length. FrameReader latches
// itself failed and never recovers; mirror that here so a
// caller cannot retry its way past a corrupt stream.
m_failed = true;
WireLogError("MG_Remote SocketTransport: framing violated on the stream; "
"the transport is latched failed");
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
return MOBILEGL_OK;
}
if (n == 0) {
// Clean EOF. The peer is gone - but anything already reassembled
// stays readable, which ITransport promises in as many words.
m_closed = true;
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
if (errno == EINTR) {
continue;
}
if (errno == EAGAIN || errno == EWOULDBLOCK) {
return MOBILEGL_ERR_TIMEOUT;
}
if (errno == ECONNRESET) {
m_closed = true;
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
WireLogError("MG_Remote SocketTransport: recv failed: %s", std::strerror(errno));
m_closed = true;
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
}
MobileGLResult SocketTransport::ReceiveFrame(MobileGLMutableByteSpan buffer,
std::uint64_t* outSize,
std::uint32_t timeoutMs) {
if (outSize == nullptr) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
*outSize = 0;
std::lock_guard<std::mutex> lock(m_recvMutex);
if (m_failed) {
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
for (;;) {
// Drain the reassembler FIRST, every time. A message that is already
// whole must be returned even when the peer has since closed, and
// must be returned without a syscall when the caller is polling.
if (m_reader.HasMessage()) {
return m_reader.TakeMessage(buffer, outSize);
}
if (m_closed) {
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
const MobileGLResult pumped = PumpOnce(timeoutMs);
if (pumped == MOBILEGL_OK) {
continue; // maybe a whole message now; maybe a fragment - loop decides
}
if (pumped == MOBILEGL_ERR_TRANSPORT_CLOSED && m_reader.HasMessage()) {
return m_reader.TakeMessage(buffer, outSize);
}
return pumped;
}
}
std::uint64_t SocketTransport::PeekFrameSize() {
std::lock_guard<std::mutex> lock(m_recvMutex);
if (m_failed) {
return 0;
}
if (!m_reader.HasMessage() && !m_closed) {
// A non-blocking pump, so a caller that has not received yet can
// still size its buffer. Errors are not reported here: PeekFrameSize
// answers "how big is the next message", and 0 means "none buffered",
// which is the truthful answer for every failure this can hit.
(void)PumpOnce(0);
}
return m_reader.PendingMessageSize();
}
MobileGLResult SocketTransport::ShareFd(int fd, MobileGLByteSpan sideband) {
if (m_tcp || m_auxFd < 0) {
// A link that declared no descriptor passing. Rule G: it does not
// invent a third answer, and the caller transfers the bytes instead.
return MOBILEGL_ERR_UNSUPPORTED;
}
std::lock_guard<std::mutex> lock(m_sendMutex);
if (m_closed) {
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
return FdPassing::SendFd(m_auxFd, fd, sideband);
}
MobileGLResult SocketTransport::ReceiveFd(int* outFd, MobileGLMutableByteSpan sideband,
std::uint64_t* outSidebandSize,
std::uint32_t timeoutMs) {
if (m_tcp || m_auxFd < 0) {
return MOBILEGL_ERR_UNSUPPORTED;
}
return FdPassing::ReceiveFd(m_auxFd, outFd, sideband, outSidebandSize, timeoutMs);
}
void SocketTransport::Shutdown() {
// Wake a blocking ReceiveFrame before taking its lock.
if (m_streamFd >= 0) ::shutdown(m_streamFd, SHUT_RDWR);
// Idempotent, and tears down BOTH directions - which on a socket is what
// closing does anyway. Taking both locks in a fixed order (send, then
// recv) so a concurrent sender and the dedicated reader cannot deadlock
// against each other here; every other path takes exactly one.
std::lock_guard<std::mutex> sendLock(m_sendMutex);
std::lock_guard<std::mutex> recvLock(m_recvMutex);
if (m_streamFd >= 0) {
// shutdown() before close() so a peer blocked in recv() wakes now
// rather than whenever the last duplicate of this fd goes away.
::shutdown(m_streamFd, SHUT_RDWR);
}
CloseIfOpen(m_streamFd);
CloseIfOpen(m_auxFd);
m_closed = true;
}
#else // !MOBILEGL_SOCKET_TRANSPORT_POSIX
// Windows has no SCM_RIGHTS and ShmSegment::Adopt is POSIX-only, so P6 lands
// POSIX only (CONTRACT-P6 §2.6). The refusal is NAMED at construction rather
// than at first use: a transport that accepts a connection and then fails
// every operation is the silent-fallback shape this project forbids.
MobileGLResult SocketTransport::CreatePair(std::unique_ptr<SocketTransport>&,
std::unique_ptr<SocketTransport>&) {
WireLogError("MG_Remote SocketTransport: unsupported on this platform - P6 lands POSIX "
"only (CONTRACT-P6 §2.6); `unix:` and `pipe:` stay named refusals");
return MOBILEGL_ERR_UNSUPPORTED;
}
SocketTransport::SocketTransport(int, int, TransportRole role) : m_role(role) {}
SocketTransport::~SocketTransport() = default;
int SocketTransport::TakeDataFd() { return -1; }
void SocketTransport::CloseLocalCopy() {}
MobileGLResult SocketTransport::SendFrame(MobileGLByteSpan) { return MOBILEGL_ERR_UNSUPPORTED; }
MobileGLResult SocketTransport::ReceiveFrame(MobileGLMutableByteSpan, std::uint64_t*,
std::uint32_t) {
return MOBILEGL_ERR_UNSUPPORTED;
}
std::uint64_t SocketTransport::PeekFrameSize() { return 0; }
MobileGLResult SocketTransport::ShareFd(int, MobileGLByteSpan) {
return MOBILEGL_ERR_UNSUPPORTED;
}
MobileGLResult SocketTransport::ReceiveFd(int*, MobileGLMutableByteSpan, std::uint64_t*,
std::uint32_t) {
return MOBILEGL_ERR_UNSUPPORTED;
}
MobileGLResult SocketTransport::PumpOnce(std::uint32_t) { return MOBILEGL_ERR_UNSUPPORTED; }
void SocketTransport::Shutdown() { m_closed = true; }
#endif
} // namespace MobileGL::MG_Remote::Transport