// 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; // A stand-in for the GBM loader's query, found by the implementation through the process's global // symbols (the target exports them): a "device" here is just the backend name it reports. struct FakeGbmDevice { const char* backend; }; extern "C" __attribute__((visibility("default"))) const char* gbm_device_get_backend_name(void* device) { return static_cast(device)->backend; } 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, bool software = false) { 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); if (software) ::setenv("LIBGL_ALWAYS_SOFTWARE", "1", 1); else ::unsetenv("LIBGL_ALWAYS_SOFTWARE"); 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); } // A GBM device another backend created is declined before MobileGL brings anything up (so a // loader with a second vendor gives it to its owner); one of MobileGL's own is served. TEST_F(ServerProbeTest, AForeignGbmDeviceIsDeclinedAndOneOfOursIsServed) { P12::ServerProcess server; if (!P12::LaunchSupervisor("probe-gbm", &server)) GTEST_SKIP() << "no `--serve` supervisor on this host"; const std::string logBase = "/tmp/" + UniqueName("gbm") + ".log"; int fds[2] = {-1, -1}; ASSERT_EQ(::pipe(fds), 0); 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", server.endpoint.c_str(), 1); ::setenv("MOBILEGL_BACKEND_TYPE", "DirectGLES", 1); constexpr EGLenum kPlatformGbm = 0x31D7; FakeGbmDevice foreign{"drm"}; FakeGbmDevice ours{"mobilegl"}; Int32 result[3] = {-1, -1, -1}; result[0] = EGL::GetPlatformDisplay(kPlatformGbm, &foreign, nullptr) != EGL_NO_DISPLAY; result[1] = MobileGL::MG_Remote::Client::ClientSession::Active() != nullptr; // nothing brought up for it result[2] = EGL::GetPlatformDisplay(kPlatformGbm, &ours, nullptr) != EGL_NO_DISPLAY; const ssize_t wrote = ::write(fds[1], result, sizeof(result)); (void)wrote; std::fflush(nullptr); ::_exit(0); } ::close(fds[1]); Int32 result[3] = {-1, -1, -1}; pollfd pfd{fds[0], POLLIN, 0}; if (::poll(&pfd, 1, 60000) > 0) { const ssize_t got = ::read(fds[0], result, sizeof(result)); (void)got; } ::close(fds[0]); ::kill(pid, SIGKILL); ::waitpid(pid, nullptr, 0); EXPECT_EQ(result[0], 0) << "a device from another GBM backend must be declined"; EXPECT_EQ(result[1], 0) << "and declined before the session was brought up"; EXPECT_EQ(result[2], 1) << "a device of MobileGL's own GBM backend is served"; 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); // A process that asked for software rendering is left to the system's other GL, server or not. const EntryReport software = RunEntryPoints(server.endpoint, logBase, true); EXPECT_EQ(software.vendorDisplay, 0); EXPECT_EQ(software.getDisplay, 0); EXPECT_EQ(software.vendorDevices, 0); EXPECT_EQ(software.glxLoaded, 0); std::error_code ec; std::filesystem::remove(Debug::RoleLogPath(logBase.c_str(), Debug::LogRole::Client), ec); } } // namespace