diff --git a/CMakeLists.txt b/CMakeLists.txt index 3bd3781c..78be3a55 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/MobileGL/ConfigLoader.cpp b/MobileGL/ConfigLoader.cpp index 2bf4c92e..fd554e3a 100644 --- a/MobileGL/ConfigLoader.cpp +++ b/MobileGL/ConfigLoader.cpp @@ -18,12 +18,10 @@ #endif #include +#include #include -#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 -#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& 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& 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& 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 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>(); @@ -82,6 +189,15 @@ namespace MobileGL::MG_ConfigLoader { } } } + + // Below the environment: a key the environment set is never replaced. + UnorderedMap 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) { diff --git a/MobileGL/Init.cpp b/MobileGL/Init.cpp index c68a2621..43204961 100644 --- a/MobileGL/Init.cpp +++ b/MobileGL/Init.cpp @@ -24,6 +24,9 @@ #if MOBILEGL_PIPE_VERIFY #include // MGPipeVerifyFlushSummary, before Debug::Close in DestroyImpl #endif +#if MOBILEGL_BUILD_DISAGGREGATED +#include +#endif #include #include @@ -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 diff --git a/MobileGL/Init.h b/MobileGL/Init.h index c2cf887c..de11c23f 100644 --- a/MobileGL/Init.h +++ b/MobileGL/Init.h @@ -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 diff --git a/MobileGL/MG_Gbm/MobileGLGbm.cpp b/MobileGL/MG_Gbm/MobileGLGbm.cpp index 9580cbb0..871aeada 100644 --- a/MobileGL/MG_Gbm/MobileGLGbm.cpp +++ b/MobileGL/MG_Gbm/MobileGLGbm.cpp @@ -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(dlsym(lib, "mobilegl_shared_image_allocate")); api.import = reinterpret_cast(dlsym(lib, "mobilegl_shared_image_import")); api.release = reinterpret_cast(dlsym(lib, "mobilegl_shared_image_release")); + api.serverAvailable = reinterpret_cast(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; diff --git a/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp b/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp index 9a200338..5b5336c5 100644 --- a/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp +++ b/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp @@ -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; diff --git a/MobileGL/MG_Impl/EGLImpl/Exporting/GlvndVendor.cpp b/MobileGL/MG_Impl/EGLImpl/Exporting/GlvndVendor.cpp index 8c474135..8c1be751 100644 --- a/MobileGL/MG_Impl/EGLImpl/Exporting/GlvndVendor.cpp +++ b/MobileGL/MG_Impl/EGLImpl/Exporting/GlvndVendor.cpp @@ -31,6 +31,9 @@ #include "../EGLImpl.h" #include "../EGLPlatformExtensions.h" +#include + +#include // 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(&g_deviceToken); diff --git a/MobileGL/MG_Impl/GLXImpl/Exporting/GlvndVendor.cpp b/MobileGL/MG_Impl/GLXImpl/Exporting/GlvndVendor.cpp index 16501431..3a2b75aa 100644 --- a/MobileGL/MG_Impl/GLXImpl/Exporting/GlvndVendor.cpp +++ b/MobileGL/MG_Impl/GLXImpl/Exporting/GlvndVendor.cpp @@ -25,6 +25,7 @@ #if defined(__linux__) && !defined(__ANDROID__) #include "../GLXImpl.h" #include "../../GetProcAddress.h" +#include 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(exportsTable); auto* imports = static_cast(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(vendor); imports->isScreenSupported = &VendorIsScreenSupported; diff --git a/MobileGL/MG_Impl/SharedImageApi.cpp b/MobileGL/MG_Impl/SharedImageApi.cpp index b36de6cd..8a22cd9a 100644 --- a/MobileGL/MG_Impl/SharedImageApi.cpp +++ b/MobileGL/MG_Impl/SharedImageApi.cpp @@ -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 lock(g_apiMutex); BackendObject* backend = ActiveBackend(false); diff --git a/MobileGL/MG_Impl/SharedImageApi.h b/MobileGL/MG_Impl/SharedImageApi.h index b6b479e6..765682ad 100644 --- a/MobileGL/MG_Impl/SharedImageApi.h +++ b/MobileGL/MG_Impl/SharedImageApi.h @@ -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*); diff --git a/MobileGL/MG_Remote/Client/ServerProbe.cpp b/MobileGL/MG_Remote/Client/ServerProbe.cpp new file mode 100644 index 00000000..02ca469f --- /dev/null +++ b/MobileGL/MG_Remote/Client/ServerProbe.cpp @@ -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 +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace MobileGL::MG_Remote::Client { + + namespace { + constexpr Uint32 kDefaultProbeTimeoutMs = 250; + constexpr Uint32 kMaxProbeTimeoutMs = 60000; + + std::atomic g_available{false}; + std::atomic g_declineLogged{false}; + + std::string Lowered(std::string text) { + std::transform(text.begin(), text.end(), text.begin(), + [](unsigned char c) { return static_cast(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(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:,: 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::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(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 diff --git a/MobileGL/MG_Remote/Client/ServerProbe.h b/MobileGL/MG_Remote/Client/ServerProbe.h new file mode 100644 index 00000000..dc7ec2e9 --- /dev/null +++ b/MobileGL/MG_Remote/Client/ServerProbe.h @@ -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:`, `unix:@` 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 + +#include + +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 diff --git a/MobileGL/MG_Remote/Transport/SocketTransport.cpp b/MobileGL/MG_Remote/Transport/SocketTransport.cpp index 06f672a0..6737e532 100644 --- a/MobileGL/MG_Remote/Transport/SocketTransport.cpp +++ b/MobileGL/MG_Remote/Transport/SocketTransport.cpp @@ -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( + deadline - std::chrono::steady_clock::now()).count(); + rc = ::poll(&pfd, 1, static_cast(std::max(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(&address), length, deadline); + } + MobileGLResult SocketTransport::ConnectControl(const std::string& path, std::uint32_t timeoutMs, std::unique_ptr& 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; } diff --git a/MobileGL/MG_Remote/Transport/SocketTransport.h b/MobileGL/MG_Remote/Transport/SocketTransport.h index 96e85729..8cc364c1 100644 --- a/MobileGL/MG_Remote/Transport/SocketTransport.h +++ b/MobileGL/MG_Remote/Transport/SocketTransport.h @@ -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& outClient); diff --git a/MobileGL/MG_Test/Util/CMakeLists.txt b/MobileGL/MG_Test/Util/CMakeLists.txt index 4b059483..d4ccc8fa 100644 --- a/MobileGL/MG_Test/Util/CMakeLists.txt +++ b/MobileGL/MG_Test/Util/CMakeLists.txt @@ -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) diff --git a/MobileGL/MG_Test/Util/ConfigFileTest.cpp b/MobileGL/MG_Test/Util/ConfigFileTest.cpp new file mode 100644 index 00000000..221fdd73 --- /dev/null +++ b/MobileGL/MG_Test/Util/ConfigFileTest.cpp @@ -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 + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +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(""); + } + + std::vector> 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"), ""); +#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"), ""); +#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"), ""); + } +} // namespace diff --git a/MobileGL/MG_Test/Wire/CMakeLists.txt b/MobileGL/MG_Test/Wire/CMakeLists.txt index e2f94043..222c39fa 100644 --- a/MobileGL/MG_Test/Wire/CMakeLists.txt +++ b/MobileGL/MG_Test/Wire/CMakeLists.txt @@ -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 diff --git a/MobileGL/MG_Test/Wire/ServerProbeTest.cpp b/MobileGL/MG_Test/Wire/ServerProbeTest.cpp new file mode 100644 index 00000000..259c71da --- /dev/null +++ b/MobileGL/MG_Test/Wire/ServerProbeTest.cpp @@ -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 +#include +#include +#include + +#include + +#include + +#include + +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(offsetof(sockaddr_un, sun_path) + endpoint.size()); + } else { + std::memcpy(address.sun_path, endpoint.c_str(), endpoint.size()); + length = static_cast(offsetof(sockaddr_un, sun_path) + endpoint.size() + 1); + } + return fd >= 0 && ::bind(fd, reinterpret_cast(&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(&address), sizeof(address)) != 0 || + ::listen(fd, 4) != 0 || ::getsockname(fd, reinterpret_cast(&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::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(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