mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
368 lines
16 KiB
C++
368 lines
16 KiB
C++
// MobileGL - MobileGL/MG_Remote/Server/ServerSpawn.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 "ServerSpawn.h"
|
|
|
|
#include "../Transport/FdPassing.h"
|
|
#include "../Transport/WireLog.h"
|
|
|
|
#include <Config.h>
|
|
|
|
#include <cerrno>
|
|
#include <cstring>
|
|
#include <vector>
|
|
|
|
#if defined(__unix__) || defined(__APPLE__) || defined(__ANDROID__)
|
|
# define MOBILEGL_SERVER_SPAWN_POSIX 1
|
|
# include <dirent.h>
|
|
# include <fcntl.h>
|
|
# include <dlfcn.h>
|
|
# include <sys/socket.h>
|
|
# include <sys/stat.h>
|
|
# include <sys/types.h>
|
|
# include <sys/wait.h>
|
|
# include <unistd.h>
|
|
#else
|
|
# define MOBILEGL_SERVER_SPAWN_POSIX 0
|
|
#endif
|
|
|
|
namespace MobileGL::MG_Remote::Server {
|
|
|
|
#if MOBILEGL_SERVER_SPAWN_POSIX
|
|
|
|
namespace {
|
|
|
|
constexpr int kChildStreamFd = 3;
|
|
constexpr int kChildAuxFd = 4;
|
|
constexpr int kChildServerBellFd = 5;
|
|
constexpr int kChildClientBellFd = 6;
|
|
|
|
bool IsExecutableFile(const std::string& path) {
|
|
if (path.empty()) {
|
|
return false;
|
|
}
|
|
struct stat st {};
|
|
if (::stat(path.c_str(), &st) != 0) {
|
|
return false;
|
|
}
|
|
return S_ISREG(st.st_mode) && ::access(path.c_str(), X_OK) == 0;
|
|
}
|
|
|
|
// MOBILEGL_IPC_SERVER_PATH, then dladdr beside our own library.
|
|
// The fallback exists because the integration tests link MobileGL_s
|
|
// statically and trace replay's executable does not live in the library
|
|
// directory - ARCHITECTURE.md §15.1 names both.
|
|
std::string ResolveImage(const std::string& requested) {
|
|
if (!requested.empty()) {
|
|
return requested; // caller's explicit choice; validated below
|
|
}
|
|
const std::string configured(MG_Config::Ipc.ServerPath.c_str());
|
|
if (!configured.empty()) {
|
|
return configured;
|
|
}
|
|
Dl_info info{};
|
|
if (::dladdr(reinterpret_cast<void*>(&ResolveImage), &info) != 0 &&
|
|
info.dli_fname != nullptr) {
|
|
const std::string self(info.dli_fname);
|
|
const std::size_t slash = self.find_last_of('/');
|
|
if (slash != std::string::npos) {
|
|
return self.substr(0, slash + 1) + "libMobileGLServer.so";
|
|
}
|
|
}
|
|
return std::string();
|
|
}
|
|
|
|
// Every MOBILEGL_TRANSPORT and MOBILEGL_IPC_* goes. Built BEFORE fork:
|
|
// between fork and execve only async-signal-safe calls are allowed, and
|
|
// allocating a vector of strings is not one of them.
|
|
//
|
|
// EXCEPT A KNOB THE SERVER READS, named here one by one. P7 wave 4 M2's
|
|
// MOBILEGL_IPC_WIRE_DEFERRED_MB bounds the server's own orphaned wire buffer stores;
|
|
// nothing in the client reads it, so scrubbing it did not make the child safer - it made
|
|
// the knob mean something under inproc and silently nothing under spawn, the worst of
|
|
// the three states (the tcp server reads its supervisor's environment, as for every
|
|
// knob). It names no endpoint, no role and no segment size, so neither anti-recursion
|
|
// catch has anything to say about it.
|
|
//
|
|
// M2 round 2 (review): three more the server reads and that were silently defaulting in
|
|
// the spawned child for the same reason - MOBILEGL_IPC_SPIN_US (the doorbell spin budget,
|
|
// read by BOTH roles, so the child should spin as the parent was told to),
|
|
// MOBILEGL_IPC_SERVER_AFFINITY (the apply thread's CPU mask, read only by the server) and
|
|
// MOBILEGL_IPC_AUDIT (the server's 0xDD fill of retired stage bytes). Each is a tuning or
|
|
// audit value, none names an endpoint, a role, a path or a segment size, and ServerMain's
|
|
// catch (b) keeps verifying the four it verifies (TRANSPORT, SERVER_PATH, RING_MB,
|
|
// STAGE_MB). Everything else under the prefix still goes.
|
|
//
|
|
// PH-6 (ID-P7-2) adds MOBILEGL_IPC_EVENT_WAIT_MS on the same argument: it is the server's
|
|
// patience with a client that has stopped draining SEG_EVENT, nothing on the client reads
|
|
// it, and scrubbing it would make a spawned server wait the default whatever the lane set.
|
|
bool ShouldScrub(const char* entry) {
|
|
static constexpr const char* kServerOwned[] = {
|
|
"MOBILEGL_IPC_WIRE_DEFERRED_MB=", "MOBILEGL_IPC_SPIN_US=", "MOBILEGL_IPC_SERVER_AFFINITY=",
|
|
"MOBILEGL_IPC_AUDIT=", "MOBILEGL_IPC_EVENT_WAIT_MS=",
|
|
};
|
|
for (const char* kept : kServerOwned) {
|
|
if (std::strncmp(entry, kept, std::strlen(kept)) == 0) {
|
|
return false;
|
|
}
|
|
}
|
|
static constexpr const char* kPrefixes[] = {"MOBILEGL_TRANSPORT=", "MOBILEGL_IPC_"};
|
|
for (const char* prefix : kPrefixes) {
|
|
if (std::strncmp(entry, prefix, std::strlen(prefix)) == 0) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
struct ScrubbedEnv {
|
|
std::vector<std::string> storage;
|
|
std::vector<char*> pointers; // NULL-terminated, valid while `storage` lives
|
|
int removed = 0;
|
|
};
|
|
|
|
ScrubbedEnv BuildChildEnv(const std::string& imageDir) {
|
|
// `environ` is glibc/bionic's, declared by <unistd.h> at global
|
|
// scope. Re-declaring it inside this anonymous namespace made it a
|
|
// NEW symbol that nothing defines - which the linker caught, and
|
|
// which is worth a comment because the error names a mangled name
|
|
// that looks nothing like the variable.
|
|
ScrubbedEnv env;
|
|
for (char** e = ::environ; e != nullptr && *e != nullptr; ++e) {
|
|
if (ShouldScrub(*e)) {
|
|
++env.removed;
|
|
continue;
|
|
}
|
|
env.storage.emplace_back(*e);
|
|
}
|
|
// (a) of the two catches: the child is told, structurally, that it
|
|
// must not dial. Separate from which ROLE it plays - CONTRACT-P6
|
|
// §3.1 is the whole argument for why those are two axes.
|
|
env.storage.emplace_back("MOBILEGL_IPC_ROLE=server");
|
|
env.storage.emplace_back("MOBILEGL_IPC_DIAL=no");
|
|
// LD_LIBRARY_PATH, BECAUSE THE SERVER IS EXEC'd AND NOT dlopen'd.
|
|
//
|
|
// It links libMobileGL.so, and an exec'd process gets a FRESH linker
|
|
// namespace with the default search path - not the one the app built
|
|
// for its own libraries. An APK's lib/<abi>/ is not on that path, and
|
|
// the failure is total and immediate:
|
|
//
|
|
// CANNOT LINK EXECUTABLE ".../libMobileGLServer.so":
|
|
// library "libMobileGL.so" not found: needed by main executable
|
|
//
|
|
// It is invisible on desktop, where CMake gives build-tree targets an
|
|
// absolute RPATH of their own - so the WSL server had been resolving
|
|
// its dependency through a path no phone has.
|
|
//
|
|
// $ORIGIN WOULD BE THE TIDIER ANSWER and the target asks for it, but
|
|
// the NDK toolchain drops RPATH from the link: measured with readelf
|
|
// on the APK's own copy, which carries NEEDED libMobileGL.so and no
|
|
// RUNPATH at all. So the launcher states it instead - it is the one
|
|
// component that has already resolved where the image lives, and a
|
|
// pair that is not co-located is not a pair.
|
|
if (!imageDir.empty()) {
|
|
env.storage.emplace_back("LD_LIBRARY_PATH=" + imageDir);
|
|
}
|
|
env.pointers.reserve(env.storage.size() + 1);
|
|
for (auto& entry : env.storage) {
|
|
env.pointers.push_back(entry.data());
|
|
}
|
|
env.pointers.push_back(nullptr);
|
|
return env;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int CountOwnChildren() {
|
|
const pid_t self = ::getpid();
|
|
DIR* proc = ::opendir("/proc");
|
|
if (proc == nullptr) {
|
|
return -1; // not Linux-shaped; the gate must say so rather than read 0
|
|
}
|
|
int children = 0;
|
|
while (const dirent* entry = ::readdir(proc)) {
|
|
if (entry->d_name[0] < '0' || entry->d_name[0] > '9') {
|
|
continue;
|
|
}
|
|
std::string statPath = std::string("/proc/") + entry->d_name + "/stat";
|
|
FILE* file = std::fopen(statPath.c_str(), "r");
|
|
if (file == nullptr) {
|
|
continue;
|
|
}
|
|
// `comm` can contain spaces and parentheses, so parse from the LAST
|
|
// ')' rather than by field index - the classic /proc/pid/stat trap.
|
|
char buffer[512] = {};
|
|
const std::size_t read = std::fread(buffer, 1, sizeof(buffer) - 1, file);
|
|
std::fclose(file);
|
|
if (read == 0) {
|
|
continue;
|
|
}
|
|
const char* close = std::strrchr(buffer, ')');
|
|
if (close == nullptr) {
|
|
continue;
|
|
}
|
|
int ppid = 0;
|
|
char state = 0;
|
|
if (std::sscanf(close + 1, " %c %d", &state, &ppid) == 2 && ppid == self) {
|
|
++children;
|
|
}
|
|
}
|
|
::closedir(proc);
|
|
return children;
|
|
}
|
|
|
|
MobileGLResult LaunchServer(const std::string& imagePath, const std::string& endpoint,
|
|
LaunchedServer* out) {
|
|
return LaunchServerWithArgs(imagePath, endpoint, {}, out);
|
|
}
|
|
|
|
MobileGLResult LaunchServerWithArgs(const std::string& imagePath, const std::string& endpoint,
|
|
const std::vector<std::string>& extraArgs,
|
|
LaunchedServer* out) {
|
|
if (out == nullptr || endpoint.empty()) {
|
|
return MOBILEGL_ERR_INVALID_ARGUMENT;
|
|
}
|
|
*out = LaunchedServer{};
|
|
|
|
const std::string image = ResolveImage(imagePath);
|
|
if (!IsExecutableFile(image)) {
|
|
// NAMED, never a fallback. ConfigLoader.cpp's own comment names the
|
|
// accident this prevents: a lane that asked for spawn, silently got
|
|
// monolith, and went green on the wrong arm.
|
|
Transport::WireLogError(
|
|
"MG_Remote spawn: the server image is not executable: \"%s\" - refusing. Set "
|
|
"MOBILEGL_IPC_SERVER_PATH, or place libMobileGLServer.so beside libMobileGL.so. "
|
|
"There is NO monolith fallback from here.",
|
|
image.empty() ? "<unresolved>" : image.c_str());
|
|
return MOBILEGL_ERR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
// The image's directory, which is where its libMobileGL.so is.
|
|
const std::string::size_type slash = image.find_last_of('/');
|
|
ScrubbedEnv env =
|
|
BuildChildEnv(slash == std::string::npos ? std::string() : image.substr(0, slash));
|
|
// Built before the fork: the child may only make async-signal-safe calls until execve.
|
|
std::vector<std::string> args;
|
|
args.reserve(2 + extraArgs.size());
|
|
args.push_back(image);
|
|
args.push_back(endpoint);
|
|
for (const std::string& arg : extraArgs) args.push_back(arg);
|
|
std::vector<char*> argvStorage;
|
|
argvStorage.reserve(args.size() + 1);
|
|
for (std::string& arg : args) argvStorage.push_back(arg.data());
|
|
argvStorage.push_back(nullptr);
|
|
char** argv = argvStorage.data();
|
|
|
|
// A close-on-exec pipe so an execve that fails reports its errno back
|
|
// instead of vanishing: the app process's stderr is /dev/null on Android
|
|
// (ARCHITECTURE.md §15.2), so a silent exec failure would look exactly
|
|
// like a server that started and then died.
|
|
int report[2] = {-1, -1};
|
|
if (::pipe(report) != 0) {
|
|
return MOBILEGL_ERR_UNSUPPORTED;
|
|
}
|
|
::fcntl(report[1], F_SETFD, FD_CLOEXEC);
|
|
|
|
const pid_t pid = ::fork();
|
|
if (pid < 0) {
|
|
Transport::WireLogError("MG_Remote spawn: fork failed: %s", std::strerror(errno));
|
|
::close(report[0]);
|
|
::close(report[1]);
|
|
return MOBILEGL_ERR_UNSUPPORTED;
|
|
}
|
|
|
|
if (pid == 0) {
|
|
// ---- child: async-signal-safe calls ONLY until execve -------------
|
|
// NOTHING IS HANDED OVER. No dup2, no fixed fd numbers, no
|
|
// close-on-exec dance - the child gets an endpoint name in argv and
|
|
// finds everything else by listening. That is the whole difference
|
|
// between a launcher and a fork-coupled pair, and it is why this
|
|
// function is now twenty lines shorter than the version that tried
|
|
// to be both.
|
|
::close(report[0]);
|
|
::execve(image.c_str(), argv, env.pointers.data());
|
|
const int failure = errno;
|
|
const ssize_t ignored = ::write(report[1], &failure, sizeof(failure));
|
|
(void)ignored;
|
|
::_exit(127);
|
|
}
|
|
|
|
// ---- parent ---------------------------------------------------------
|
|
::close(report[1]);
|
|
int childErrno = 0;
|
|
const ssize_t got = ::read(report[0], &childErrno, sizeof(childErrno));
|
|
::close(report[0]);
|
|
if (got == static_cast<ssize_t>(sizeof(childErrno))) {
|
|
Transport::WireLogError("MG_Remote spawn: execve(\"%s\") failed in the child: %s",
|
|
image.c_str(), std::strerror(childErrno));
|
|
int status = 0;
|
|
::waitpid(pid, &status, 0);
|
|
return MOBILEGL_ERR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
out->pid = static_cast<int>(pid);
|
|
out->endpoint = endpoint;
|
|
Transport::WireLogError("MG_Remote spawn: server pid=%d image=\"%s\" endpoint=\"%s\" "
|
|
"transport=spawn (env scrubbed: %d entries removed)",
|
|
out->pid, image.c_str(), endpoint.c_str(), env.removed);
|
|
return MOBILEGL_OK;
|
|
}
|
|
|
|
MobileGLResult ReapServer(LaunchedServer& server, std::uint32_t timeoutMs,
|
|
int* outExitCode) {
|
|
if (server.pid < 0) {
|
|
return MOBILEGL_ERR_INVALID_ARGUMENT;
|
|
}
|
|
// NOTHING TO CLOSE HERE ANY MORE. The launcher holds no descriptor
|
|
// into the child: whoever owns the CONNECTION closes it, and that EOF is
|
|
// what the child exits on. A launcher that also owned the connection is
|
|
// what made the two processes one unit.
|
|
|
|
const auto deadline = timeoutMs;
|
|
std::uint32_t waited = 0;
|
|
for (;;) {
|
|
int status = 0;
|
|
const pid_t done = ::waitpid(server.pid, &status, WNOHANG);
|
|
if (done == server.pid) {
|
|
if (outExitCode != nullptr) {
|
|
*outExitCode = WIFEXITED(status) ? WEXITSTATUS(status) : -WTERMSIG(status);
|
|
}
|
|
server.pid = -1;
|
|
return MOBILEGL_OK;
|
|
}
|
|
if (done < 0) {
|
|
return MOBILEGL_ERR_INVALID_ARGUMENT;
|
|
}
|
|
if (waited >= deadline) {
|
|
return MOBILEGL_ERR_TIMEOUT;
|
|
}
|
|
::usleep(1000);
|
|
waited += 1;
|
|
}
|
|
}
|
|
|
|
#else // !MOBILEGL_SERVER_SPAWN_POSIX
|
|
|
|
MobileGLResult LaunchServer(const std::string&, const std::string&, LaunchedServer*) {
|
|
Transport::WireLogError("MG_Remote spawn: unsupported on this platform - P6 lands POSIX only "
|
|
"(CONTRACT-P6 §2.6); there is no fork() to build a second process from");
|
|
return MOBILEGL_ERR_UNSUPPORTED;
|
|
}
|
|
MobileGLResult LaunchServerWithArgs(const std::string& imagePath, const std::string& endpoint,
|
|
const std::vector<std::string>&, LaunchedServer* out) {
|
|
return LaunchServer(imagePath, endpoint, out);
|
|
}
|
|
MobileGLResult ReapServer(LaunchedServer&, std::uint32_t, int*) {
|
|
return MOBILEGL_ERR_UNSUPPORTED;
|
|
}
|
|
int CountOwnChildren() { return -1; }
|
|
|
|
#endif
|
|
|
|
} // namespace MobileGL::MG_Remote::Server
|