[Feature] (Client): read a system config file and decline EGL/GLX/GBM when no server answers

This commit is contained in:
2026-10-02 19:36:38 -04:00
parent d24abe9287
commit 69e96f5b8d
18 changed files with 932 additions and 13 deletions
+3
View File
@@ -646,6 +646,9 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
MobileGL/MG_Remote/FatalFunnel.cpp
MobileGL/MG_Remote/Wire/PipeWireCodec.cpp
MobileGL/MG_Remote/Client/ClientSession.cpp
# Whether the configured server is there at all, asked before a display is offered so the
# system's EGL/GLX/GBM loaders can move on to another implementation when it is not.
MobileGL/MG_Remote/Client/ServerProbe.cpp
MobileGL/MG_Remote/Client/EmitTables.cpp
MobileGL/MG_Remote/Client/CapsMirror.cpp
# P5 c1: the client role's BackendObject. pActiveBackendObject holds one of these
+120 -4
View File
@@ -18,12 +18,10 @@
#endif
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#if MOBILEGL_BUILD_DISAGGREGATED
// std::strcmp, for InitIpc's server-role check (P12). Split-only, so the pull build's TU is
// unchanged (G1).
// std::strcmp: InitIpc's server-role check (P12) and the system configuration's.
#include <cstring>
#endif
#ifndef _WIN32
extern char** environ;
@@ -52,6 +50,115 @@ namespace MobileGL::MG_ConfigLoader {
return (key.compare(0, 6, "LIBGL_") == 0 || key.compare(0, 9, "MOBILEGL_") == 0);
}
// ---- the system configuration -----------------------------------------------------------
//
// A library that is every process's GL implementation (installed as the system's EGL/GLX
// vendor) cannot rely on each process having been started with MOBILEGL_* in its environment:
// most are started by something that has never heard of it. So the settings a whole machine
// shares live in files, and the environment only overrides them:
//
// environment > /etc/mobilegl/backend (MOBILEGL_BACKEND_TYPE only) > client.conf > built-in
//
// client.conf is `KEY=value` lines; blank lines and lines starting with '#' are skipped, the
// key and the value are trimmed, and one pair of matching quotes around the value is removed.
// Only MOBILEGL_* / LIBGL_* keys are taken, exactly as from the environment. The backend file
// holds just the backend's name (the file the backend switch rewrites), and names it over a
// MOBILEGL_BACKEND_TYPE line in client.conf so that switch cannot be silently shadowed.
//
// MOBILEGL_CONFIG_FILE / MOBILEGL_BACKEND_FILE name other files; set to empty they turn the
// file off. The defaults exist only on desktop Linux: an Android app configures its process
// itself, and a server process (MOBILEGL_IPC_ROLE=server) never reads the clients' file.
namespace {
#if defined(__linux__) && !defined(__ANDROID__)
constexpr const char* kDefaultConfigFile = "/etc/mobilegl/client.conf";
constexpr const char* kDefaultBackendFile = "/etc/mobilegl/backend";
#else
constexpr const char* kDefaultConfigFile = "";
constexpr const char* kDefaultBackendFile = "";
#endif
String FilePathSetting(const char* variable, const char* fallback) {
const char* value = std::getenv(variable);
return value != nullptr ? String(value) : String(fallback);
}
String Trimmed(const String& text) {
const auto first = text.find_first_not_of(" \t\r\n");
if (first == String::npos) return String();
const auto last = text.find_last_not_of(" \t\r\n");
return text.substr(first, last - first + 1);
}
Bool IsServerProcess() {
const char* role = std::getenv("MOBILEGL_IPC_ROLE");
return role != nullptr && std::strcmp(role, "server") == 0;
}
// Reads `path` into `out` without replacing what `out` already holds. A missing file is
// not an error: the defaults are the absence of one.
void ReadConfigFile(const String& path, UnorderedMap<String, String>& out, Bool log) {
if (path.empty()) return;
std::FILE* file = std::fopen(path.c_str(), "r");
if (file == nullptr) return;
char buffer[4096];
Uint32 lineNumber = 0;
while (std::fgets(buffer, sizeof(buffer), file) != nullptr) {
++lineNumber;
const String line = Trimmed(buffer);
if (line.empty() || line[0] == '#') continue;
const auto equals = line.find('=');
const String key = equals == String::npos ? String() : Trimmed(line.substr(0, equals));
if (key.empty() || !IsAcceptedPrefix(key)) {
if (log) {
MGLOG_W("Config: %s:%u is not a MOBILEGL_*/LIBGL_* KEY=value line; ignored", path.c_str(),
lineNumber);
}
continue;
}
String value = Trimmed(line.substr(equals + 1));
if (value.size() >= 2 && (value.front() == '"' || value.front() == '\'') && value.back() == value.front()) {
value = value.substr(1, value.size() - 2);
}
out.emplace(key, value);
}
std::fclose(file);
}
void ReadBackendFile(const String& path, UnorderedMap<String, String>& out) {
if (path.empty()) return;
std::FILE* file = std::fopen(path.c_str(), "r");
if (file == nullptr) return;
char buffer[256];
const Bool read = std::fgets(buffer, sizeof(buffer), file) != nullptr;
std::fclose(file);
if (!read) return;
const String name = Trimmed(buffer);
if (!name.empty()) out.emplace("MOBILEGL_BACKEND_TYPE", name);
}
// Everything the system files say, the backend file ahead of client.conf (first emplace
// wins). Re-read on every call: callers are few (Init, and a handful of lookups before it),
// and a file that changed between two processes' starts must not be served stale.
void ReadSystemConfig(UnorderedMap<String, String>& out, Bool log) {
if (IsServerProcess()) return;
ReadBackendFile(FilePathSetting("MOBILEGL_BACKEND_FILE", kDefaultBackendFile), out);
ReadConfigFile(FilePathSetting("MOBILEGL_CONFIG_FILE", kDefaultConfigFile), out, log);
}
} // namespace
Bool LookupSetting(const char* key, String& outValue) {
if (const char* value = std::getenv(key)) {
outValue = value;
return true;
}
UnorderedMap<String, String> system;
ReadSystemConfig(system, false);
const auto it = system.find(key);
if (it == system.end()) return false;
outValue = it->second;
return true;
}
inline void InitializeAcceptedEnvVariables() {
if (!acceptedEnvVariablesMap) {
acceptedEnvVariablesMap = MakeUnique<UnorderedMap<String, String>>();
@@ -82,6 +189,15 @@ namespace MobileGL::MG_ConfigLoader {
}
}
}
// Below the environment: a key the environment set is never replaced.
UnorderedMap<String, String> system;
ReadSystemConfig(system, true);
for (const auto& [key, value] : system) {
if (acceptedEnvVariablesMap->emplace(key, value).second) {
MGLOG_D("Config: Accepted system setting: %s=%s", key.c_str(), value.c_str());
}
}
}
inline void QueryEnvVariable(const String& key, String& outValue, const String& defaultValue) {
+26
View File
@@ -24,6 +24,9 @@
#if MOBILEGL_PIPE_VERIFY
#include <MG_Backend/MGPipe/PipeInputs.h> // MGPipeVerifyFlushSummary, before Debug::Close in DestroyImpl
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Remote/Client/ServerProbe.h>
#endif
#include <atomic>
#include <mutex>
@@ -102,6 +105,10 @@ namespace MobileGL {
MG_State::GLState::ClearProgramTranslationCache();
MG_Backend::gBackendFunctionsTable = {};
g_isInitialized = false;
#if MOBILEGL_BUILD_DISAGGREGATED
// A later re-initialization asks whether the server is there again.
MG_Remote::Client::ResetServerProbeCache();
#endif
#if MOBILEGL_PIPE_VERIFY
// BEFORE Close, and from here rather than from a static destructor (V1 fix round 2):
// Close() nulls the role's sink and Log.cpp's next write reopens it with "w", so the
@@ -220,6 +227,25 @@ namespace MobileGL {
DestroyImpl(true);
}
Bool ImplementationAvailable() {
if (g_isInitialized.load(std::memory_order_acquire)) {
#if MOBILEGL_BUILD_DISAGGREGATED
// Initialized: under a split transport the session either came up (a backend object
// exists) or it did not, and a display on a session that never came up cannot draw.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
return MG_Backend::pActiveBackendObject != nullptr;
}
#endif
return true;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// Not yet: the bring-up would dial the configured server and wait for it. Ask first.
return MG_Remote::Client::ConfiguredServerAvailable();
#else
return true;
#endif
}
// MobileGL's lifecycle is owned entirely by the host-API layers
// (EGL/WGL/CGL): initialization happens lazily on the first entry point
// via EnsureInitialized(), and full teardown happens deterministically
+11
View File
@@ -26,8 +26,19 @@ namespace MobileGL {
namespace MG_ConfigLoader {
void Init();
// One setting resolved the way Init() resolves it - the environment, then the system
// configuration files (ConfigLoader.cpp) - for the few readers that must decide something
// before MobileGL initializes. False when neither names it.
Bool LookupSetting(const char* key, String& outValue);
} // namespace MG_ConfigLoader
// Whether this process can be served right now. False only when MobileGL is configured to
// reach a server that is not there (or a split session already failed to come up), so a
// caller that can choose another implementation - the system's EGL/GLX/GBM loaders - should
// decline rather than hand out something that cannot draw. Cheap: once true it stays true
// until the library is torn down, and a probe is one non-blocking connect with a short bound.
Bool ImplementationAvailable();
namespace MG_Backend {
void Init();
} // namespace MG_Backend
+17 -2
View File
@@ -73,11 +73,14 @@ namespace {
PFN_mobilegl_shared_image_allocate allocate = nullptr;
PFN_mobilegl_shared_image_import import = nullptr;
PFN_mobilegl_shared_image_release release = nullptr;
PFN_mobilegl_server_available serverAvailable = nullptr; // optional (older libraries lack it)
};
// The library the process already has (by its soname: glvnd loads it by path, which records
// the soname too), else MOBILEGL_GBM_LIBRARY, else the loader's search path. Never closed:
// libMobileGL leaks its globals at exit by design and must not be unloaded under them.
// the soname too), else MOBILEGL_GBM_LIBRARY, else the loader's search path, else the GLX vendor
// name it is installed under in the system library directory (the same file, so the same
// instance if glvnd loads it later). Never closed: libMobileGL leaks its globals at exit by
// design and must not be unloaded under them.
const SharedImageApi* Api() {
static std::once_flag once;
static SharedImageApi api;
@@ -88,6 +91,7 @@ namespace {
const char* path = std::getenv("MOBILEGL_GBM_LIBRARY");
lib = dlopen(path != nullptr && path[0] != '\0' ? path : "libMobileGL.so", RTLD_NOW | RTLD_LOCAL);
}
if (lib == nullptr) lib = dlopen("libGLX_mobilegl.so.0", RTLD_NOW | RTLD_LOCAL);
if (lib == nullptr) {
std::fprintf(stderr, "mobilegl_gbm: libMobileGL.so could not be loaded: %s\n", dlerror());
return;
@@ -97,6 +101,7 @@ namespace {
api.allocate = reinterpret_cast<PFN_mobilegl_shared_image_allocate>(dlsym(lib, "mobilegl_shared_image_allocate"));
api.import = reinterpret_cast<PFN_mobilegl_shared_image_import>(dlsym(lib, "mobilegl_shared_image_import"));
api.release = reinterpret_cast<PFN_mobilegl_shared_image_release>(dlsym(lib, "mobilegl_shared_image_release"));
api.serverAvailable = reinterpret_cast<PFN_mobilegl_server_available>(dlsym(lib, "mobilegl_server_available"));
if (version == nullptr || version() < 1 || api.allocate == nullptr || api.import == nullptr ||
api.release == nullptr) {
std::fprintf(stderr, "mobilegl_gbm: this libMobileGL.so has no shared-image ABI\n");
@@ -341,7 +346,17 @@ namespace {
void SurfaceDestroy(gbm_surface*) {}
// THE DEVICE IS DECLINED WHEN NOTHING COULD SERVE ITS BUFFERS. The loader takes this backend
// because GBM_BACKEND names it, and when its create_device fails it goes on to the backend the
// device's own driver names - so declining is what lets one system-wide GBM_BACKEND=mobilegl
// coexist with processes that run while no MobileGL server is reachable. Asked before any
// buffer exists, with one bounded connect (mobilegl_server_available), never a session bring-up.
gbm_device* CreateDevice(int fd, uint32_t backendVersion) {
const SharedImageApi* api = Api();
if (api == nullptr || (api->serverAvailable != nullptr && api->serverAvailable() == 0)) {
errno = ENODEV;
return nullptr;
}
auto* device = new (std::nothrow) Device{};
if (device == nullptr) return nullptr;
gbm_device_v0& v0 = device->base.v0;
+9
View File
@@ -530,6 +530,11 @@ namespace MobileGL::MG_Impl::EGLImpl {
}
EGLDisplay GetDisplay(NativeDisplayType display) {
// Before the bring-up, which would wait for a configured server that is not there: no
// display is better than one that cannot draw, and the caller may have another EGL.
if (!MobileGL::ImplementationAvailable()) {
return EGL_NO_DISPLAY;
}
auto* state = GetStateEnsureInitialized();
if (!state) {
return EGL_NO_DISPLAY;
@@ -1187,6 +1192,10 @@ namespace MobileGL::MG_Impl::EGLImpl {
EGLDisplay GetPlatformDisplay(EGLenum platform, void* native_display, const EGLAttrib* attrib_list) {
(void)attrib_list;
// As GetDisplay.
if (!MobileGL::ImplementationAvailable()) {
return EGL_NO_DISPLAY;
}
auto* state = GetStateEnsureInitialized();
if (!state) {
return EGL_NO_DISPLAY;
@@ -31,6 +31,9 @@
#include "../EGLImpl.h"
#include "../EGLPlatformExtensions.h"
#include <Init.h>
#include <mutex>
// Defined in Definitions.cpp; eglext.h declares them only under EGL_EGLEXT_PROTOTYPES.
extern "C" EGLImage eglCreateImageKHR(EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer,
@@ -65,14 +68,27 @@ namespace {
return MG_Impl::EGLImpl::QueryString(dpy, name);
}
// DECLINING IS HOW THIS VENDOR STEPS ASIDE. glvnd asks its vendors for a display in order and
// takes the first that answers, so a vendor installed ahead of the system's own one must answer
// EGL_NO_DISPLAY whenever it cannot actually serve: no MobileGL server reachable (asked before
// the bring-up, so an absent server costs one refused connect rather than the bring-up's wait),
// or a split session that was tried and did not come up. The application then gets the next
// vendor's display instead of one of ours that fails at eglInitialize.
EGLDisplay EGLAPIENTRY VendorGetPlatformDisplay(EGLenum platform, void* native_display,
const EGLAttrib* attrib_list) {
if (!MobileGL::ImplementationAvailable()) {
return EGL_NO_DISPLAY;
}
// EGL_NONE means the application called eglGetDisplay(EGL_DEFAULT_DISPLAY): the vendor is
// being asked to name its own default display.
if (platform == EGL_NONE) {
return MG_Impl::EGLImpl::GetDisplay(EGL_DEFAULT_DISPLAY);
const EGLDisplay display = platform == EGL_NONE
? MG_Impl::EGLImpl::GetDisplay(EGL_DEFAULT_DISPLAY)
: MG_Impl::EGLImpl::GetPlatformDisplay(platform, native_display, attrib_list);
// The bring-up above may itself have failed (a server that refused this client).
if (display != EGL_NO_DISPLAY && !MobileGL::ImplementationAvailable()) {
return EGL_NO_DISPLAY;
}
return MG_Impl::EGLImpl::GetPlatformDisplay(platform, native_display, attrib_list);
return display;
}
EGLBoolean EGLAPIENTRY VendorGetSupportsAPI(EGLenum api) {
@@ -111,8 +127,16 @@ namespace {
const char* DrmRenderNode() {
// MOBILEGL_DEVICE_DRM_NODE names another node: a machine with more than one GPU, or a test
// that wants the mismatch refused rather than papered over.
const char* configured = std::getenv("MOBILEGL_DEVICE_DRM_NODE");
return (configured != nullptr && configured[0] != '\0') ? configured : "/dev/dri/renderD128";
// Read like every other setting (environment over the system configuration), once: the
// answer is handed out as a pointer.
static std::once_flag once;
static String node;
std::call_once(once, [] {
if (!MG_ConfigLoader::LookupSetting("MOBILEGL_DEVICE_DRM_NODE", node) || node.empty()) {
node = "/dev/dri/renderD128";
}
});
return node.c_str();
}
// One device, one identity. A pointer to a private object rather than a number, so that a
@@ -124,6 +148,12 @@ namespace {
if (num_devices == nullptr) {
return EGL_FALSE;
}
// No server, no device: a device this vendor could never open a display on would only lead
// the caller to it (glvnd merges every vendor's list; an error here would fail the merge).
if (!MobileGL::ImplementationAvailable()) {
*num_devices = 0;
return EGL_TRUE;
}
*num_devices = 1;
if (devices != nullptr && max_devices >= 1) {
devices[0] = reinterpret_cast<EGLDeviceEXT>(&g_deviceToken);
@@ -25,6 +25,7 @@
#if defined(__linux__) && !defined(__ANDROID__)
#include "../GLXImpl.h"
#include "../../GetProcAddress.h"
#include <Init.h>
namespace {
using namespace MobileGL;
@@ -111,6 +112,13 @@ extern "C" MOBILEGL_API int __glx_Main(Uint32 version, const void* exportsTable,
const auto* exports = static_cast<const GLXApiExports*>(exportsTable);
auto* imports = static_cast<GLXApiImports*>(importsTable);
if ((version >> 16) != kGLXVendorABIMajor || exports == nullptr || imports == nullptr) return 0;
// Declining here is the only way this vendor can step aside: libGLX takes the vendor that
// __GLX_VENDOR_LIBRARY_NAME names without asking it about the screen, and only a vendor that
// fails to load sends it on to the one the X server names. So with no MobileGL server
// reachable (or a split session that did not come up) this answers 0, and X clients get the
// system's GLX instead of contexts that cannot draw. libGLX loads a vendor on the first GLX
// call that needs one, so this runs once per process, not at library load.
if (!MobileGL::ImplementationAvailable()) return 0;
g_glxExports = exports;
g_glxVendor = static_cast<GLXVendorInfo*>(vendor);
imports->isScreenSupported = &VendorIsScreenSupported;
+4
View File
@@ -100,6 +100,10 @@ MOBILEGL_EXPORT int mobilegl_shared_image_import(int fd, uint32_t width, uint32_
return 0;
}
MOBILEGL_EXPORT int mobilegl_server_available(void) {
return MobileGL::ImplementationAvailable() ? 1 : 0;
}
MOBILEGL_EXPORT int mobilegl_shared_image_release(uint64_t id) {
const std::lock_guard<std::mutex> lock(g_apiMutex);
BackendObject* backend = ActiveBackend(false);
+8
View File
@@ -81,7 +81,15 @@ int mobilegl_shared_image_import(int fd, uint32_t width, uint32_t height, uint32
/* Drops this process's reference (an allocation's or an import's). */
int mobilegl_shared_image_release(uint64_t id);
/* 1 when this process can be served: its session is up, or the server it is configured to dial
* answers (one bounded non-blocking connect, cached once true); 0 when not. Never brings the
* session up. A component that a system loader picked on MobileGL's behalf (the GBM backend) asks
* this first and declines, so the loader moves on to another implementation. Optional: a library
* without it is assumed available. */
int mobilegl_server_available(void);
/* dlsym-friendly signatures. */
typedef int (*PFN_mobilegl_server_available)(void);
typedef uint32_t (*PFN_mobilegl_shared_image_abi_version)(void);
typedef int (*PFN_mobilegl_shared_image_formats)(uint32_t*, uint32_t);
typedef int (*PFN_mobilegl_shared_image_allocate)(uint32_t, uint32_t, uint32_t, struct mobilegl_shared_image*);
+105
View File
@@ -0,0 +1,105 @@
// MobileGL - MobileGL/MG_Remote/Client/ServerProbe.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 "ServerProbe.h"
#include <Init.h>
#include <MG_Remote/Transport/SocketTransport.h>
#include <MG_Util/Debug/Log.h>
#include <algorithm>
#include <atomic>
#include <cctype>
#include <cerrno>
#include <chrono>
#include <cstdlib>
namespace MobileGL::MG_Remote::Client {
namespace {
constexpr Uint32 kDefaultProbeTimeoutMs = 250;
constexpr Uint32 kMaxProbeTimeoutMs = 60000;
std::atomic<Bool> g_available{false};
std::atomic<Bool> g_declineLogged{false};
std::string Lowered(std::string text) {
std::transform(text.begin(), text.end(), text.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return text;
}
Uint32 ProbeTimeoutMs() {
String text;
if (!MG_ConfigLoader::LookupSetting("MOBILEGL_IPC_PROBE_TIMEOUT_MS", text)) return kDefaultProbeTimeoutMs;
char* end = nullptr;
errno = 0;
const unsigned long value = std::strtoul(text.c_str(), &end, 10);
if (end == text.c_str() || *end != '\0' || errno == ERANGE || value > kMaxProbeTimeoutMs) {
return kDefaultProbeTimeoutMs;
}
return static_cast<Uint32>(value);
}
} // namespace
Bool ConfiguredProbeTarget(ServerProbeTarget* out) {
String transport;
String control;
if (!MG_ConfigLoader::LookupSetting("MOBILEGL_TRANSPORT", transport) || Lowered(transport) != "spawn") {
return false;
}
if (!MG_ConfigLoader::LookupSetting("MOBILEGL_IPC_CONTROL", control)) return false; // "fork": launches its own
std::string endpoint;
if (control.compare(0, 5, "unix:") == 0) {
endpoint = control.substr(5);
} else if (control.compare(0, 6, "tcp://") == 0) {
endpoint = control;
} else {
return false; // fork, fd:<control>,<aux>: nothing to dial
}
if (endpoint.empty()) return false;
if (out != nullptr) {
out->endpoint = endpoint;
out->timeoutMs = ProbeTimeoutMs();
}
return true;
}
ServerProbeResult ProbeConfiguredServer() {
ServerProbeTarget target;
if (!ConfiguredProbeTarget(&target) || target.timeoutMs == 0) return ServerProbeResult::NotApplicable;
return Transport::SocketTransport::Probe(target.endpoint, target.timeoutMs) ? ServerProbeResult::Reachable
: ServerProbeResult::Unreachable;
}
Bool ConfiguredServerAvailable() {
if (g_available.load(std::memory_order_acquire)) return true;
const auto started = std::chrono::steady_clock::now();
const ServerProbeResult result = ProbeConfiguredServer();
if (result != ServerProbeResult::Unreachable) {
g_available.store(true, std::memory_order_release);
return true;
}
if (!g_declineLogged.exchange(true)) {
ServerProbeTarget target;
(void)ConfiguredProbeTarget(&target);
const auto tookUs = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now() - started).count();
MGLOG_I("MG_Remote client: no MobileGL server answers at \"%s\" (probed in %lld us); declining, so "
"the system's next GL implementation serves this process",
target.endpoint.c_str(), static_cast<long long>(tookUs));
}
return false;
}
void ResetServerProbeCache() {
g_available.store(false, std::memory_order_release);
g_declineLogged.store(false, std::memory_order_release);
}
} // namespace MobileGL::MG_Remote::Client
+59
View File
@@ -0,0 +1,59 @@
// MobileGL - MobileGL/MG_Remote/Client/ServerProbe.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
// IS THE SERVER THIS PROCESS WOULD DIAL THERE AT ALL.
//
// Installed as the system's EGL/GLX vendor, this library is asked first by every GL process on the
// machine, and the session bring-up behind its first display waits up to 20 s for a server that
// may simply not be running. The loaders that ask (EGL and GLX dispatchers, the GBM loader) all
// move on to the next implementation when one declines - so the honest, cheap answer when nobody
// is listening is to decline before that wait starts, and this is the question that decides it.
//
// Only a CONNECT dial can be probed: MOBILEGL_TRANSPORT=spawn with MOBILEGL_IPC_CONTROL naming
// `unix:<path>`, `unix:@<abstract>` or `tcp://host:port`. Everything else - monolith, inproc, a
// fork control that launches its own server, an fd: pair somebody already connected - has no
// remote to be absent and answers NotApplicable. Both settings are read the way the configuration
// loader reads them (environment over the system configuration files), because the probe runs
// before MobileGL initializes.
//
// MOBILEGL_IPC_PROBE_TIMEOUT_MS bounds one probe (default 250, 1..60000); 0 turns probing off.
#pragma once
#include <Includes.h>
#include <string>
namespace MobileGL::MG_Remote::Client {
enum class ServerProbeResult {
NotApplicable, // nothing to dial: the configuration serves the process another way
Reachable, // something accepted a connection at the configured endpoint
Unreachable, // the configured endpoint refused, does not exist, or did not answer in time
};
struct ServerProbeTarget {
std::string endpoint; // SocketTransport form: a path, "@name", or "tcp://host:port"
Uint32 timeoutMs = 0; // 0: probing disabled
};
// What the configuration says to probe; false when it names nothing probeable (NotApplicable).
Bool ConfiguredProbeTarget(ServerProbeTarget* out);
// One probe of the configured server, uncached.
ServerProbeResult ProbeConfiguredServer();
// The cached form the entry points use: once a probe found the server (or there is nothing to
// probe) the answer stays true for the life of the process; a negative answer is re-probed on
// the next call, which is one refused connect.
Bool ConfiguredServerAvailable();
// Forgets a positive answer (tests; and the library's teardown, so a re-initialization asks again).
void ResetServerProbeCache();
} // namespace MobileGL::MG_Remote::Client
@@ -512,6 +512,54 @@ namespace MobileGL::MG_Remote::Transport {
return MOBILEGL_OK;
}
namespace {
// One non-blocking connect to `address`, waited on until `deadline`.
bool ProbeOne(int family, const sockaddr* address, socklen_t length,
std::chrono::steady_clock::time_point deadline) {
const int fd = ::socket(family, SOCK_STREAM, 0);
if (fd < 0) return false;
::fcntl(fd, F_SETFD, FD_CLOEXEC);
::fcntl(fd, F_SETFL, ::fcntl(fd, F_GETFL, 0) | O_NONBLOCK);
bool reachable = ::connect(fd, address, length) == 0;
if (!reachable && family == AF_UNIX && errno == EAGAIN) {
// A unix listener whose backlog is full: it exists and is busy, which is a server.
reachable = true;
} else if (!reachable && errno == EINPROGRESS) {
pollfd pfd{fd, POLLOUT, 0};
int rc;
do {
const auto remaining = std::chrono::duration_cast<std::chrono::milliseconds>(
deadline - std::chrono::steady_clock::now()).count();
rc = ::poll(&pfd, 1, static_cast<int>(std::max<std::int64_t>(0, remaining)));
} while (rc < 0 && errno == EINTR);
int error = 0;
socklen_t size = sizeof(error);
reachable = rc > 0 && ::getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &size) == 0 && error == 0;
}
::close(fd);
return reachable;
}
} // namespace
bool SocketTransport::Probe(const std::string& path, std::uint32_t timeoutMs) {
const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(std::max(1u, timeoutMs));
if (TcpName(path)) {
addrinfo* addresses = nullptr;
if (!TcpAddress(path, &addresses)) return false;
bool reachable = false;
for (auto* address = addresses; address != nullptr && !reachable; address = address->ai_next) {
reachable = ProbeOne(address->ai_family, address->ai_addr, address->ai_addrlen, deadline);
if (std::chrono::steady_clock::now() >= deadline) break;
}
::freeaddrinfo(addresses);
return reachable;
}
sockaddr_un address{};
socklen_t length = 0;
if (!FillAddress(path, &address, &length)) return false;
return ProbeOne(AF_UNIX, reinterpret_cast<const sockaddr*>(&address), length, deadline);
}
MobileGLResult SocketTransport::ConnectControl(const std::string& path, std::uint32_t timeoutMs,
std::unique_ptr<SocketTransport>& outClient) {
if (!TcpName(path)) return ConnectTo(path, timeoutMs, outClient);
@@ -906,6 +954,8 @@ namespace MobileGL::MG_Remote::Transport {
return MOBILEGL_ERR_UNSUPPORTED;
}
bool SocketTransport::Probe(const std::string&, std::uint32_t) { return false; }
SocketTransport::SocketTransport(int, int, TransportRole role) : m_role(role) {}
SocketTransport::~SocketTransport() = default;
int SocketTransport::TakeDataFd() { return -1; }
@@ -132,6 +132,14 @@ namespace MobileGL::MG_Remote::Transport {
// 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.
// Is anything listening at `path` (a ConnectTo endpoint: an AF_UNIX path, '@' + an abstract
// name, or tcp://host:port)? One non-blocking connect per address, bounded by `timeoutMs`
// in total and never retried, then closed without a byte written - the server's pairing
// drops a connection that closes before presenting a PairBind. For deciding whether to
// offer a display at all, so it must not stall: a refused or absent endpoint answers at
// once, and only an unresponsive TCP peer can take the whole bound.
static bool Probe(const std::string& path, std::uint32_t timeoutMs);
// Connects the control connection only on `tcp://`; identical to ConnectTo otherwise.
static MobileGLResult ConnectControl(const std::string& path, std::uint32_t timeoutMs,
std::unique_ptr<SocketTransport>& outClient);
+19
View File
@@ -71,8 +71,27 @@ target_link_libraries(
${LINK_LIBRARIES}
)
# The system configuration files under the environment: the precedence MG_ConfigLoader::Init()
# and its pre-init LookupSetting readers share. No GL context, no driver.
add_executable(
ConfigFileTest
ConfigFileTest.cpp
)
target_include_directories(ConfigFileTest PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
)
target_link_libraries(
ConfigFileTest PRIVATE
GTest::gtest_main
${LINK_LIBRARIES}
)
include(GoogleTest)
gtest_discover_tests(JobNodeTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
gtest_discover_tests(LogLevelTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
gtest_discover_tests(PipeStatsTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
gtest_discover_tests(DamageTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
gtest_discover_tests(ConfigFileTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
+176
View File
@@ -0,0 +1,176 @@
// MobileGL - MobileGL/MG_Test/Util/ConfigFileTest.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
// The system configuration files (ConfigLoader.cpp): environment > backend file > client.conf >
// built-in default, for MG_ConfigLoader::Init() and for the LookupSetting readers that run before
// it. No GL context, no driver: the loader is parsing.
#include <gtest/gtest.h>
#include <Config.h>
#include <Init.h>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <string>
#include <tuple>
#include <vector>
#include <unistd.h>
namespace {
using MobileGL::BackendType;
using MobileGL::String;
namespace Config = MobileGL::MG_Config;
namespace Loader = MobileGL::MG_ConfigLoader;
// Every variable a case touches, restored after it.
constexpr const char* kTouched[] = {
"MOBILEGL_CONFIG_FILE", "MOBILEGL_BACKEND_FILE", "MOBILEGL_BACKEND_TYPE",
"MOBILEGL_MAGMA_FRAMESINFLIGHT", "MOBILEGL_TRANSPORT", "MOBILEGL_IPC_CONTROL",
"MOBILEGL_IPC_DATA", "MOBILEGL_IPC_ROLE", "MOBILEGL_TEST_ONLY_KEY",
};
class ConfigFileTest : public ::testing::Test {
protected:
void SetUp() override {
for (const char* name : kTouched) {
const char* value = std::getenv(name);
saved_.emplace_back(name, value != nullptr, value != nullptr ? value : "");
::unsetenv(name);
}
dir_ = std::filesystem::temp_directory_path() /
("mgl-config-test-" + std::to_string(::getpid()) + "-" +
::testing::UnitTest::GetInstance()->current_test_info()->name());
std::filesystem::create_directories(dir_);
conf_ = (dir_ / "client.conf").string();
backend_ = (dir_ / "backend").string();
// Point both at this case's files, which do not exist yet: the host's own /etc is
// never read by a case.
::setenv("MOBILEGL_CONFIG_FILE", conf_.c_str(), 1);
::setenv("MOBILEGL_BACKEND_FILE", backend_.c_str(), 1);
}
void TearDown() override {
for (const auto& [name, had, value] : saved_) {
if (had) ::setenv(name.c_str(), value.c_str(), 1);
else ::unsetenv(name.c_str());
}
std::error_code ec;
std::filesystem::remove_all(dir_, ec);
Loader::Init(); // leave the process's configuration as its environment says
}
static void Write(const std::string& path, const std::string& text) {
std::ofstream out(path, std::ios::binary);
out << text;
}
static String Lookup(const char* key) {
String value;
return Loader::LookupSetting(key, value) ? value : String("<unset>");
}
std::vector<std::tuple<std::string, bool, std::string>> saved_;
std::filesystem::path dir_;
std::string conf_;
std::string backend_;
};
TEST_F(ConfigFileTest, WithoutFilesOrEnvironmentTheBuiltInDefaultsHold) {
Loader::Init();
EXPECT_EQ(Config::ActiveBackendType, BackendType::DirectGLES);
EXPECT_EQ(Config::Features.MagmaFramesInFlight, 3u);
EXPECT_EQ(Lookup("MOBILEGL_TRANSPORT"), "<unset>");
#if MOBILEGL_BUILD_DISAGGREGATED
EXPECT_EQ(Config::Transport, Config::TransportMode::Monolith);
EXPECT_EQ(Config::Ipc.Control, "fork");
#endif
}
TEST_F(ConfigFileTest, TheFileFillsWhatTheEnvironmentLeavesUnset) {
Write(conf_, "# a comment\n"
"\n"
" MOBILEGL_MAGMA_FRAMESINFLIGHT = 5 \n"
"MOBILEGL_TRANSPORT=spawn\r\n"
"MOBILEGL_IPC_CONTROL=\"unix:@test-endpoint\"\n"
"MOBILEGL_IPC_DATA='shm'\n"
"NOT_OURS=1\n"
"garbage line without equals\n"
"MOBILEGL_TEST_ONLY_KEY=a=b\n");
Loader::Init();
EXPECT_EQ(Config::Features.MagmaFramesInFlight, 5u);
EXPECT_EQ(Lookup("MOBILEGL_TRANSPORT"), "spawn");
EXPECT_EQ(Lookup("MOBILEGL_IPC_CONTROL"), "unix:@test-endpoint");
EXPECT_EQ(Lookup("MOBILEGL_IPC_DATA"), "shm");
EXPECT_EQ(Lookup("MOBILEGL_TEST_ONLY_KEY"), "a=b"); // only the first '=' splits
EXPECT_EQ(Lookup("NOT_OURS"), "<unset>");
#if MOBILEGL_BUILD_DISAGGREGATED
EXPECT_EQ(Config::Transport, Config::TransportMode::Spawn);
EXPECT_EQ(Config::Ipc.Control, "unix:@test-endpoint");
EXPECT_EQ(Config::Ipc.Data, "shm");
#endif
}
TEST_F(ConfigFileTest, TheEnvironmentWinsOverTheFile) {
Write(conf_, "MOBILEGL_MAGMA_FRAMESINFLIGHT=5\nMOBILEGL_IPC_CONTROL=unix:@from-file\n");
::setenv("MOBILEGL_MAGMA_FRAMESINFLIGHT", "7", 1);
::setenv("MOBILEGL_IPC_CONTROL", "unix:@from-env", 1);
Loader::Init();
EXPECT_EQ(Config::Features.MagmaFramesInFlight, 7u);
EXPECT_EQ(Lookup("MOBILEGL_IPC_CONTROL"), "unix:@from-env");
#if MOBILEGL_BUILD_DISAGGREGATED
EXPECT_EQ(Config::Ipc.Control, "unix:@from-env");
#endif
// Set but empty is still the environment speaking.
::setenv("MOBILEGL_IPC_CONTROL", "", 1);
EXPECT_EQ(Lookup("MOBILEGL_IPC_CONTROL"), "");
}
TEST_F(ConfigFileTest, TheBackendFileNamesTheBackendUnderTheEnvironmentAndOverClientConf) {
Write(backend_, " DirectVulkan \n");
Write(conf_, "MOBILEGL_BACKEND_TYPE=DirectGLES\n");
Loader::Init();
EXPECT_EQ(Config::ActiveBackendType, BackendType::DirectVulkan);
EXPECT_EQ(Lookup("MOBILEGL_BACKEND_TYPE"), "DirectVulkan");
::setenv("MOBILEGL_BACKEND_TYPE", "DirectGLES", 1);
Loader::Init();
EXPECT_EQ(Config::ActiveBackendType, BackendType::DirectGLES);
::unsetenv("MOBILEGL_BACKEND_TYPE");
// Without the backend file, client.conf's line is the answer.
std::filesystem::remove(backend_);
Write(conf_, "MOBILEGL_BACKEND_TYPE=DirectVulkan\n");
Loader::Init();
EXPECT_EQ(Config::ActiveBackendType, BackendType::DirectVulkan);
}
TEST_F(ConfigFileTest, AnEmptyPathTurnsAFileOffAndAMissingFileIsNoError) {
Write(conf_, "MOBILEGL_MAGMA_FRAMESINFLIGHT=5\n");
Write(backend_, "DirectVulkan\n");
::setenv("MOBILEGL_CONFIG_FILE", "", 1);
::setenv("MOBILEGL_BACKEND_FILE", "", 1);
Loader::Init();
EXPECT_EQ(Config::Features.MagmaFramesInFlight, 3u);
EXPECT_EQ(Config::ActiveBackendType, BackendType::DirectGLES);
::setenv("MOBILEGL_CONFIG_FILE", (dir_ / "absent.conf").string().c_str(), 1);
Loader::Init();
EXPECT_EQ(Config::Features.MagmaFramesInFlight, 3u);
}
TEST_F(ConfigFileTest, AServerProcessNeverReadsTheClientsFile) {
Write(conf_, "MOBILEGL_MAGMA_FRAMESINFLIGHT=5\n");
Write(backend_, "DirectVulkan\n");
::setenv("MOBILEGL_IPC_ROLE", "server", 1);
Loader::Init();
EXPECT_EQ(Config::Features.MagmaFramesInFlight, 3u);
EXPECT_EQ(Config::ActiveBackendType, BackendType::DirectGLES);
EXPECT_EQ(Lookup("MOBILEGL_MAGMA_FRAMESINFLIGHT"), "<unset>");
}
} // namespace
+11 -2
View File
@@ -77,6 +77,10 @@ if (NOT WIN32)
# supervisor over loopback TCP (P12ServerRig.h): refused by name once per session on the side
# that names it, map_persistent DECLINED, never a Fatal. Needs the server image too.
list(APPEND MOBILEGL_WIRE_TESTS AdoptTierStreamTest)
# Declining when no server is there (Client/ServerProbe.h): the probe over unix/abstract/TCP
# listeners, and the EGL/glvnd/GLX entry points in forked clients, with no server (they decline
# inside a second) and against a real `--serve` supervisor (a display). Needs the server image.
list(APPEND MOBILEGL_WIRE_TESTS ServerProbeTest)
endif()
include(GoogleTest)
@@ -95,7 +99,7 @@ foreach (test IN LISTS MOBILEGL_WIRE_TESTS)
${MGL_ROOT}/3rdparty/flatbuffers/include
)
if (test STREQUAL "PeerLatchTest" OR test STREQUAL "InProcessServerTest" OR test STREQUAL "ServerOwnedSurfaceTest" OR
test STREQUAL "AdoptTierStreamTest" OR test STREQUAL "MultiSessionTest")
test STREQUAL "AdoptTierStreamTest" OR test STREQUAL "MultiSessionTest" OR test STREQUAL "ServerProbeTest")
# It builds records and a program archive by hand, so it reaches MG_Pipe and MG_State
# the way the codec suites do. (P12's two server suites share PeerLatchTest's healthy
# session, P12ServerRig.h, and so its include paths.)
@@ -116,8 +120,13 @@ foreach (test IN LISTS MOBILEGL_WIRE_TESTS)
target_compile_definitions(${test} PRIVATE MOBILEGL_SOURCE_ROOT="${MGL_ROOT}")
endif ()
if (test STREQUAL "ServerProbeTest")
# It calls the glvnd vendor entry point the way libEGL does.
target_include_directories(${test} PRIVATE ${MGL_ROOT}/3rdparty/glvnd/include)
endif ()
if (test STREQUAL "ServerSpawnTest" OR test STREQUAL "PeerLatchTest" OR test STREQUAL "InProcessServerTest" OR
test STREQUAL "ServerOwnedSurfaceTest" OR test STREQUAL "AdoptTierStreamTest")
test STREQUAL "ServerOwnedSurfaceTest" OR test STREQUAL "AdoptTierStreamTest" OR test STREQUAL "ServerProbeTest")
# It execs the real image, so it must be built first and must be told
# where it is. MOBILEGL_TEST_SERVER_PATH is read by the test's
# ServerImage() helper; without it the test would depend on the working
+263
View File
@@ -0,0 +1,263 @@
// MobileGL - MobileGL/MG_Test/Wire/ServerProbeTest.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
// DECLINING WHEN NO SERVER IS THERE (MG_Remote/Client/ServerProbe.h).
//
// Installed as the system's EGL/GLX/GBM vendor, MobileGL is asked first by every GL process, so it
// must step aside - quickly - when the server it would dial is not running:
// * the probe itself: a listening abstract name / path / loopback port answers Reachable, an
// absent one Unreachable within milliseconds, and the configurations with nothing to dial
// (monolith, fork, fd:, probing turned off) NotApplicable;
// * the entry points, each in a fresh forked process: with the configured endpoint absent,
// eglGetDisplay, the glvnd vendor's getPlatformDisplay and its device list all decline, and
// the GLX vendor refuses to load - in well under a second, where the session bring-up they
// replace waits 20 s; with a real `--serve` supervisor listening, a display is handed out.
#include "P12ServerRig.h"
#include <Init.h>
#include <MG_Impl/EGLImpl/EGLImpl.h>
#include <MG_Remote/Client/ServerProbe.h>
#include <MG_Remote/Transport/SocketTransport.h>
#include <glvnd/libeglabi.h>
#include <sys/un.h>
#include <utility>
extern "C" EGLBoolean __egl_Main(uint32_t version, const __EGLapiExports* exports, __EGLvendorInfo* vendor,
__EGLapiImports* imports);
extern "C" int __glx_Main(uint32_t version, const void* exports, void* vendor, void* imports);
using namespace MobileGL;
namespace {
namespace EGL = MobileGL::MG_Impl::EGLImpl;
namespace Probe = MobileGL::MG_Remote::Client;
namespace Debug = MobileGL::MG_Util::Debug;
using MobileGL::MG_Remote::Transport::SocketTransport;
std::string UniqueName(const char* what) {
return std::string("mgl-probe-test-") + what + "-" + std::to_string(::getpid());
}
// A bare listener (no MobileGL server): all the probe needs is someone accepting connections.
struct Listener {
int fd = -1;
~Listener() {
if (fd >= 0) ::close(fd);
}
bool Unix(const std::string& endpoint) { // "@name" or a path
fd = ::socket(AF_UNIX, SOCK_STREAM, 0);
sockaddr_un address{};
address.sun_family = AF_UNIX;
socklen_t length = 0;
if (endpoint[0] == '@') {
std::memcpy(address.sun_path + 1, endpoint.c_str() + 1, endpoint.size() - 1);
length = static_cast<socklen_t>(offsetof(sockaddr_un, sun_path) + endpoint.size());
} else {
std::memcpy(address.sun_path, endpoint.c_str(), endpoint.size());
length = static_cast<socklen_t>(offsetof(sockaddr_un, sun_path) + endpoint.size() + 1);
}
return fd >= 0 && ::bind(fd, reinterpret_cast<sockaddr*>(&address), length) == 0 && ::listen(fd, 4) == 0;
}
int Tcp() { // returns the port
fd = ::socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in address{};
address.sin_family = AF_INET;
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
socklen_t length = sizeof(address);
if (fd < 0 || ::bind(fd, reinterpret_cast<sockaddr*>(&address), sizeof(address)) != 0 ||
::listen(fd, 4) != 0 || ::getsockname(fd, reinterpret_cast<sockaddr*>(&address), &length) != 0)
return -1;
return ntohs(address.sin_port);
}
};
long long MillisSince(std::chrono::steady_clock::time_point start) {
return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start).count();
}
// The probe reads the environment over the system configuration; no case may read the host's.
class ServerProbeTest : public ::testing::Test {
protected:
void SetUp() override {
::setenv("MOBILEGL_CONFIG_FILE", "", 1);
::setenv("MOBILEGL_BACKEND_FILE", "", 1);
::unsetenv("MOBILEGL_IPC_PROBE_TIMEOUT_MS");
Probe::ResetServerProbeCache();
}
void TearDown() override {
::unsetenv("MOBILEGL_TRANSPORT");
::unsetenv("MOBILEGL_IPC_CONTROL");
::unsetenv("MOBILEGL_IPC_PROBE_TIMEOUT_MS");
Probe::ResetServerProbeCache();
}
static void Configure(const char* transport, const std::string& control) {
::setenv("MOBILEGL_TRANSPORT", transport, 1);
::setenv("MOBILEGL_IPC_CONTROL", control.c_str(), 1);
}
};
TEST_F(ServerProbeTest, AListeningEndpointIsReachableAndAnAbsentOneIsNotQuickly) {
Listener abstract;
const std::string name = "@" + UniqueName("abstract");
ASSERT_TRUE(abstract.Unix(name));
EXPECT_TRUE(SocketTransport::Probe(name, 250));
const std::string path = "/tmp/" + UniqueName("path") + ".sock";
::unlink(path.c_str());
{
Listener filesystem;
ASSERT_TRUE(filesystem.Unix(path));
EXPECT_TRUE(SocketTransport::Probe(path, 250));
}
::unlink(path.c_str());
Listener tcp;
const int port = tcp.Tcp();
ASSERT_GT(port, 0);
EXPECT_TRUE(SocketTransport::Probe("tcp://127.0.0.1:" + std::to_string(port), 250));
const auto start = std::chrono::steady_clock::now();
EXPECT_FALSE(SocketTransport::Probe("@" + UniqueName("nobody"), 250));
EXPECT_FALSE(SocketTransport::Probe(path, 250)); // unlinked: ENOENT
::close(std::exchange(tcp.fd, -1));
EXPECT_FALSE(SocketTransport::Probe("tcp://127.0.0.1:" + std::to_string(port), 250));
EXPECT_LT(MillisSince(start), 200) << "a refused or absent endpoint must answer at once, not at the bound";
}
TEST_F(ServerProbeTest, TheConfigurationDecidesWhatIsProbed) {
Listener abstract;
const std::string name = "@" + UniqueName("configured");
ASSERT_TRUE(abstract.Unix(name));
Configure("spawn", "unix:" + name);
EXPECT_EQ(Probe::ProbeConfiguredServer(), Probe::ServerProbeResult::Reachable);
Configure("SPAWN", "unix:@" + UniqueName("absent"));
EXPECT_EQ(Probe::ProbeConfiguredServer(), Probe::ServerProbeResult::Unreachable);
// Nothing to dial: these are served some other way, never declined by a probe.
Configure("monolith", "unix:@" + UniqueName("absent"));
EXPECT_EQ(Probe::ProbeConfiguredServer(), Probe::ServerProbeResult::NotApplicable);
Configure("spawn", "fork");
EXPECT_EQ(Probe::ProbeConfiguredServer(), Probe::ServerProbeResult::NotApplicable);
Configure("spawn", "fd:3,4");
EXPECT_EQ(Probe::ProbeConfiguredServer(), Probe::ServerProbeResult::NotApplicable);
::unsetenv("MOBILEGL_TRANSPORT");
::unsetenv("MOBILEGL_IPC_CONTROL");
EXPECT_EQ(Probe::ProbeConfiguredServer(), Probe::ServerProbeResult::NotApplicable);
// MOBILEGL_IPC_PROBE_TIMEOUT_MS=0 turns probing off.
Configure("spawn", "unix:@" + UniqueName("absent"));
::setenv("MOBILEGL_IPC_PROBE_TIMEOUT_MS", "0", 1);
EXPECT_EQ(Probe::ProbeConfiguredServer(), Probe::ServerProbeResult::NotApplicable);
}
TEST_F(ServerProbeTest, APositiveAnswerIsCachedAndANegativeOneIsAskedAgain) {
const std::string name = "@" + UniqueName("cache");
Configure("spawn", "unix:" + name);
EXPECT_FALSE(Probe::ConfiguredServerAvailable());
{
Listener late;
ASSERT_TRUE(late.Unix(name));
EXPECT_TRUE(Probe::ConfiguredServerAvailable()); // the server came up: asked again
}
EXPECT_TRUE(Probe::ConfiguredServerAvailable()); // and remembered
Probe::ResetServerProbeCache();
EXPECT_FALSE(Probe::ConfiguredServerAvailable());
}
// ---- the entry points, each in a fresh process ---------------------------------------------
struct EntryReport {
Int32 getDisplay = -1; // 1: eglGetDisplay gave a display
Int32 vendorDisplay = -1; // 1: the glvnd vendor's getPlatformDisplay did
Int32 vendorDevices = -1; // the vendor's eglQueryDevicesEXT count
Int32 glxLoaded = -1; // __glx_Main's answer
Int64 elapsedMs = -1; // all of the above, end to end
};
EntryReport RunEntryPoints(const std::string& control, const std::string& logBase) {
EntryReport report{};
int fds[2] = {-1, -1};
if (::pipe(fds) != 0) return report;
std::fflush(nullptr);
const pid_t pid = ::fork();
if (pid == 0) {
::close(fds[0]);
::setenv("MOBILEGL_LOG_FILE_PATH", logBase.c_str(), 1);
::setenv("MOBILEGL_TRANSPORT", "spawn", 1);
::setenv("MOBILEGL_IPC_CONTROL", control.c_str(), 1);
::setenv("MOBILEGL_BACKEND_TYPE", "DirectGLES", 1);
EntryReport r{};
const auto start = std::chrono::steady_clock::now();
__EGLapiExports exports{};
__EGLapiImports imports{};
const bool vendor = __egl_Main(EGL_VENDOR_ABI_VERSION, &exports, nullptr, &imports) == EGL_TRUE;
r.vendorDisplay = vendor && imports.getPlatformDisplay(EGL_NONE, EGL_DEFAULT_DISPLAY, nullptr) != EGL_NO_DISPLAY;
r.getDisplay = EGL::GetDisplay(EGL_DEFAULT_DISPLAY) != EGL_NO_DISPLAY;
if (vendor) {
using QueryDevices = EGLBoolean (*)(EGLint, EGLDeviceEXT*, EGLint*);
auto* query = reinterpret_cast<QueryDevices>(imports.getProcAddress("eglQueryDevicesEXT"));
EGLint count = -1;
if (query != nullptr && query(0, nullptr, &count) == EGL_TRUE) r.vendorDevices = count;
}
void* glxExports[16] = {};
void* glxImports[16] = {};
r.glxLoaded = __glx_Main(1u << 16, glxExports, nullptr, glxImports);
r.elapsedMs = MillisSince(start);
const ssize_t wrote = ::write(fds[1], &r, sizeof(r));
(void)wrote;
std::fflush(nullptr);
::_exit(0);
}
::close(fds[1]);
pollfd pfd{fds[0], POLLIN, 0};
if (pid > 0 && ::poll(&pfd, 1, 60000) > 0) {
const ssize_t got = ::read(fds[0], &report, sizeof(report));
(void)got;
}
::close(fds[0]);
if (pid > 0) {
::kill(pid, SIGKILL);
::waitpid(pid, nullptr, 0);
}
return report;
}
TEST_F(ServerProbeTest, WithNoServerEveryEntryPointDeclinesWellInsideASecond) {
const std::string logBase = "/tmp/" + UniqueName("decline") + ".log";
const EntryReport r = RunEntryPoints("unix:@" + UniqueName("no-server"), logBase);
EXPECT_EQ(r.vendorDisplay, 0) << "the glvnd vendor must decline so the next vendor is asked";
EXPECT_EQ(r.getDisplay, 0);
EXPECT_EQ(r.vendorDevices, 0) << "no device either, and no error (glvnd fails its merge on one)";
EXPECT_EQ(r.glxLoaded, 0) << "the GLX vendor must refuse to load so libGLX falls back";
EXPECT_GE(r.elapsedMs, 0);
EXPECT_LT(r.elapsedMs, 1000) << "declining must not cost the bring-up's 20 s connect budget";
std::error_code ec;
std::filesystem::remove(Debug::RoleLogPath(logBase.c_str(), Debug::LogRole::Client), ec);
}
TEST_F(ServerProbeTest, WithAServerListeningADisplayIsHandedOut) {
P12::ServerProcess server;
if (!P12::LaunchSupervisor("probe", &server)) GTEST_SKIP() << "no `--serve` supervisor on this host";
const std::string logBase = "/tmp/" + UniqueName("serve") + ".log";
const EntryReport r = RunEntryPoints(server.endpoint, logBase);
EXPECT_EQ(r.vendorDisplay, 1) << server.Log();
EXPECT_EQ(r.getDisplay, 1);
EXPECT_EQ(r.vendorDevices, 1);
EXPECT_EQ(r.glxLoaded, 1);
std::error_code ec;
std::filesystem::remove(Debug::RoleLogPath(logBase.c_str(), Debug::LogRole::Client), ec);
}
} // namespace