mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Feature] (Client): read a system config file and decline EGL/GLX/GBM when no server answers
This commit is contained in:
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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*);
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user