diff --git a/MobileGL/MG_Test/Wire/CMakeLists.txt b/MobileGL/MG_Test/Wire/CMakeLists.txt index af043317..e96cd749 100644 --- a/MobileGL/MG_Test/Wire/CMakeLists.txt +++ b/MobileGL/MG_Test/Wire/CMakeLists.txt @@ -36,6 +36,12 @@ if (NOT WIN32) # P7 F2 latch (PH-1 (3)(4), Ph fuzz arm 2): one raw-peer row per latched site against a real # `--serve` supervisor, each followed by a healthy next session. Needs the server image too. list(APPEND MOBILEGL_WIRE_TESTS PeerLatchTest) + # P12 (on-screen server window). InProcessServerTest: the in-process display server entry run on a + # thread (sequential sessions, the latch reset, Busy, the backend pin) and D8's parent-death signal + # for a forked session child. ServerOwnedSurfaceTest: a real MobileGL client with + # MOBILEGL_IPC_SURFACE=server against a display-less `--serve` supervisor. Both drive real peers + # and (the second, and D8's case) the real server image, as PeerLatchTest does. + list(APPEND MOBILEGL_WIRE_TESTS InProcessServerTest ServerOwnedSurfaceTest) endif() include(GoogleTest) @@ -53,9 +59,10 @@ foreach (test IN LISTS MOBILEGL_WIRE_TESTS) ${MGL_ROOT}/MobileGL ${MGL_ROOT}/3rdparty/flatbuffers/include ) - if (test STREQUAL "PeerLatchTest") + if (test STREQUAL "PeerLatchTest" OR test STREQUAL "InProcessServerTest" OR test STREQUAL "ServerOwnedSurfaceTest") # It builds records and a program archive by hand, so it reaches MG_Pipe and MG_State - # the way the codec suites do. + # the way the codec suites do. (P12's two server suites share PeerLatchTest's healthy + # session, P12ServerRig.h, and so its include paths.) target_include_directories(${test} PRIVATE ${MGL_ROOT}/MobileGL/MG_Pipe ${MGL_ROOT}/3rdparty/xxHash @@ -69,7 +76,8 @@ foreach (test IN LISTS MOBILEGL_WIRE_TESTS) ${LINK_LIBRARIES} ) - if (test STREQUAL "ServerSpawnTest" OR test STREQUAL "PeerLatchTest") + if (test STREQUAL "ServerSpawnTest" OR test STREQUAL "PeerLatchTest" OR test STREQUAL "InProcessServerTest" OR + test STREQUAL "ServerOwnedSurfaceTest") # 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/InProcessServerTest.cpp b/MobileGL/MG_Test/Wire/InProcessServerTest.cpp new file mode 100644 index 00000000..f1a0721c --- /dev/null +++ b/MobileGL/MG_Test/Wire/InProcessServerTest.cpp @@ -0,0 +1,255 @@ +// MobileGL - MobileGL/MG_Test/Wire/InProcessServerTest.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 + +// P12 (on-screen server window), D5 and D8, on the Linux host. +// +// D5: THE IN-PROCESS DISPLAY SERVER, RUN ON A THREAD. The on-screen server is the display Activity's +// own process: mobilegl_server_serve_inprocess serves tcp:// on a thread and runs every session on a +// thread of the same process (an ANativeWindow survives neither fork nor exec). Nothing about that +// needs Android, so it is proven here: a process forked from this test runs the entry on a +// std::thread - its main thread only waits for SIGTERM to call mobilegl_server_stop_inprocess - and +// forked peers talk to it over loopback TCP. What is proven: +// * sessions run one after another IN ONE PROCESS (every Welcome names the server process's own +// pid) and each is a real one (a record applied, a pbuffer made current, a green clear read back +// where headless EGL exists) - so RunSession returned and cleaned up after each; +// * a session that LATCHED does not decline the next (ResetSessionLatch between sessions); +// * an authenticated second Hello while a session is live is Refuse{Busy}; +// * the first session's backend is pinned for the process lifetime; +// * stop returns the serving thread with 0 and the process exits cleanly. +// +// D8: A FORKED SESSION CHILD DIES WITH ITS SUPERVISOR (PR_SET_PDEATHSIG). The exec'd supervisor is +// SIGKILLed with a session live and its client holding the connection; this test is the child +// subreaper, so the orphaned session child is reaped HERE - killed by SIGKILL, not still running. + +#include "P12ServerRig.h" + +#include + +#include +#include +#include +#include + +using namespace MobileGL; +using namespace P12; + +namespace { + + // THE PROCESS THAT IS THE IN-PROCESS DISPLAY SERVER. The environment is the one the display + // Activity sets before loading the library; the entry runs on a thread; SIGTERM (blocked in every + // thread, taken synchronously here) is the stop. + [[noreturn]] void RunInProcessServerProcess(const std::string& endpoint, const std::string& logBase) { + ::setenv("MOBILEGL_LOG_FILE_PATH", logBase.c_str(), 1); + ::setenv("MOBILEGL_IPC_ROLE", "server", 1); + ::setenv("MOBILEGL_IPC_DIAL", "no", 1); + for (const char* name : {"MOBILEGL_TRANSPORT", "MOBILEGL_IPC_SERVER_PATH", "MOBILEGL_IPC_RING_MB", + "MOBILEGL_IPC_STAGE_MB", "MOBILEGL_IPC_CONTROL", "MOBILEGL_IPC_SURFACE", + "MOBILEGL_BACKEND_TYPE"}) + ::unsetenv(name); + sigset_t stop; + sigemptyset(&stop); + sigaddset(&stop, SIGTERM); + pthread_sigmask(SIG_BLOCK, &stop, nullptr); + std::atomic returned{false}; + std::atomic served{-1}; + std::thread server([&] { + served.store(mobilegl_server_serve_inprocess(endpoint.c_str())); + returned.store(true); + }); + bool stopping = false; + while (!returned.load()) { + timespec slice{0, 100 * 1000 * 1000}; + if (sigtimedwait(&stop, nullptr, &slice) == SIGTERM && !stopping) { + stopping = true; + mobilegl_server_stop_inprocess(); + } + } + server.join(); + std::fflush(nullptr); + // 0 only for the orderly shape: stopped, and the serving thread returned 0. + ::_exit(stopping && served.load() == 0 ? 0 : 100 + (served.load() & 0x7f)); + } + + bool LaunchInProcessServer(const std::string& label, ServerProcess* server) { + server->logBase = "/tmp/mgl-p12-inproc-" + label + "-" + std::to_string(::getpid()) + ".log"; + PinHeadlessEgl(); + for (int attempt = 0; attempt < 5; ++attempt) { + const int port = FreeLoopbackPort(); + if (port <= 0) return false; + server->endpoint = "tcp://127.0.0.1:" + std::to_string(port); + Debug::TruncateRoleLogs(server->logBase.c_str()); + std::fflush(nullptr); + const pid_t pid = ::fork(); + if (pid < 0) return false; + if (pid == 0) RunInProcessServerProcess(server->endpoint, server->logBase); + server->pid = pid; + bool listening = false; + (void)WaitFor( + [&] { + if (server->Log().find("listening on") != std::string::npos) return listening = true; + return server->WaitExit(0) && server->pid <= 0; + }, + 10000); + if (listening) return true; + if (server->pid > 0) return false; + } + return false; + } + + // The in-process supervisor's reap line for its n-th session, or "" while it has not been reaped. + std::string InProcessReapLine(const ServerProcess& server, unsigned ordinal) { + const std::string log = server.Log(); + const std::string needle = "in-process session #" + std::to_string(ordinal) + " pid="; + for (auto at = log.find(needle); at != std::string::npos; at = log.find(needle, at + 1)) { + const auto end = log.find('\n', at); + const std::string line = log.substr(at, end == std::string::npos ? std::string::npos : end - at); + if (line.find(" reaped ") != std::string::npos) return line; + } + return {}; + } + + void ApplyOneRecord(Remote::Client::ClientSession& client, PeerReport& r) { + P::MGPMemoryBarrier barrier{}; + barrier.Bits = 0x2000u; + r.applied = Emit(client, P::MGPWireOp::MemoryBarrier, &barrier, sizeof(barrier)) ? 1 : 0; + } + + void LatchTheSession(Remote::Client::ClientSession& client, PeerReport& r) { + // A CAMetalLayer* on the wire: Fatal{UnmigratedSurface, "MetalLayer@P12"}, latched. + (void)SendSurfaceOp(client, r, static_cast(::MobileGL::Wire::SurfaceOpKind::CreateWindowSurface), + static_cast(::MobileGL::Wire::WindowKind::MetalLayer), 0x1234); + } + +} // namespace + +// D5, THE HEADLINE. Three sessions, one process: a healthy one, one that latches, and a healthy one +// after it. Red with ResetSessionLatch removed from the supervisor's reap: session C's Welcome arrives +// but the latch still stands, so its apply thread declines the ring (applied 0) and its EGL ops are +// answered PROTOCOL_MISMATCH. +TEST(InProcessServer, SequentialSessionsShareOneProcessAndALatchedSessionDoesNotDeclineTheNext) { + ServerProcess server; + ASSERT_TRUE(LaunchInProcessServer("seq", &server)) << "the in-process server did not start:\n" << server.Log(); + const auto serverPid = static_cast(server.pid); + + const PeerReport a = RunPeer(server.endpoint, HealthySession, /*stopCleanly=*/true); + ASSERT_EQ(a.started, 1) << "session A got no Welcome (" << a.note << ")\n" << server.Log(); + EXPECT_EQ(a.serverPid, serverPid) << "the session is not the in-process server's own process"; + EXPECT_EQ(a.applied, 1) << a.note; + std::string reapA; + ASSERT_TRUE(WaitFor([&] { return !(reapA = InProcessReapLine(server, 1)).empty(); }, 15000)) << server.Log(); + EXPECT_NE(reapA.find("exit=0 "), std::string::npos) << reapA; + + const PeerReport b = RunPeer(server.endpoint, LatchTheSession, /*stopCleanly=*/false); + ASSERT_EQ(b.started, 1) << "session B got no Welcome (" << b.note << ")"; + EXPECT_EQ(b.serverPid, serverPid); + std::string reapB; + ASSERT_TRUE(WaitFor([&] { return !(reapB = InProcessReapLine(server, 2)).empty(); }, 15000)) << server.Log(); + EXPECT_NE(reapB.find("exit=75 (latched fault)"), std::string::npos) << "session B did not end latched: " << reapB; + + const PeerReport c = RunPeer(server.endpoint, HealthySession, /*stopCleanly=*/true); + ASSERT_EQ(c.started, 1) << "session C got no Welcome after a latched session (" << c.note << ")\n" << server.Log(); + EXPECT_EQ(c.serverPid, serverPid); + EXPECT_EQ(c.applied, 1) << "session C did not apply a record: the latch was not reset (" << c.note << ")"; + std::string reapC; + ASSERT_TRUE(WaitFor([&] { return !(reapC = InProcessReapLine(server, 3)).empty(); }, 15000)) << server.Log(); + EXPECT_NE(reapC.find("exit=0 "), std::string::npos) << reapC; + EXPECT_EQ(Count(server.Log(), "SessionLatch{"), 1u) << "only session B may latch"; + + // STOP: the serving thread returns 0 and the process exits cleanly (exit 0 is only produced for a + // stop that the serving thread answered with 0). + server.Stop(); + EXPECT_TRUE(WIFEXITED(server.exitStatus) && WEXITSTATUS(server.exitStatus) == 0) + << "the in-process server did not stop cleanly: status 0x" << std::hex << server.exitStatus; + EXPECT_NE(server.Log().find("in-process display server stopped"), std::string::npos); + + // Where headless EGL exists, both healthy sessions rendered - the backend came up, went away with + // session A and came up again in the same process. + if (a.egl == 0 || c.egl == 0) { + GTEST_SKIP() << "sessions, latch reset and stop proven; the render half needs headless EGL (" << a.note + << " / " << c.note << ")"; + } + EXPECT_EQ(a.status, Remote::Wire::ReplySink::kStatusOk) << a.note; + EXPECT_EQ(a.pixel, 0xFF00FF00u) << "session A's clear did not read back green"; + EXPECT_EQ(c.status, Remote::Wire::ReplySink::kStatusOk) << c.note; + EXPECT_EQ(c.pixel, 0xFF00FF00u) << "session C's clear did not read back green"; +} + +// D5: ONE SESSION AT A TIME. An authenticated second Hello while a session is live is refused Busy +// by the supervisor thread - and served once the first has gone. Red with the thread hand-off leaving +// `m_active` unset: the second Hello is handed a second session thread. +TEST(InProcessServer, AnAuthenticatedSecondHelloWhileASessionIsLiveIsRefusedBusy) { + ServerProcess server; + ASSERT_TRUE(LaunchInProcessServer("busy", &server)) << server.Log(); + pid_t held = -1; + const PeerReport a = RunPeer(server.endpoint, ApplyOneRecord, /*stopCleanly=*/false, 30000, /*holdMs=*/5000, &held); + ASSERT_EQ(a.started, 1) << a.note << "\n" << server.Log(); + ASSERT_EQ(a.applied, 1) << a.note; + + const PeerReport b = RunPeer(server.endpoint, ApplyOneRecord, /*stopCleanly=*/true, 15000); + EXPECT_EQ(b.started, 0) << "a second session was admitted while one was live"; + EXPECT_NE(server.Log().find("Refuse{Busy} one session is already active"), std::string::npos) << server.Log(); + + ReleaseHeldPeer(held); + ASSERT_TRUE(WaitFor([&] { return !InProcessReapLine(server, 1).empty(); }, 15000)) << server.Log(); + const PeerReport c = RunPeer(server.endpoint, ApplyOneRecord, /*stopCleanly=*/true); + EXPECT_EQ(c.started, 1) << "the next session after the busy one was not served (" << c.note << ")"; + EXPECT_EQ(c.applied, 1); +} + +// D5: THE BACKEND IS PINNED FOR THE PROCESS LIFETIME. With no MOBILEGL_BACKEND_TYPE the first +// session's backend (Espryt here) is the pin, and a later Hello asking for Magma is refused by the +// pinned-backend refusal - Espryt and Magma never share a process. Red with the in-process pin +// deleted: session B is admitted. +TEST(InProcessServer, TheFirstSessionsBackendIsPinnedForTheProcessLifetime) { + ServerProcess server; + ASSERT_TRUE(LaunchInProcessServer("pin", &server)) << server.Log(); + const PeerReport a = RunPeer(server.endpoint, ApplyOneRecord, /*stopCleanly=*/true); + ASSERT_EQ(a.started, 1) << a.note << "\n" << server.Log(); + ASSERT_TRUE(WaitFor([&] { return !InProcessReapLine(server, 1).empty(); }, 15000)) << server.Log(); + + const PeerReport b = RunPeer(server.endpoint, ApplyOneRecord, /*stopCleanly=*/true, 30000, 0, nullptr, [] { + MobileGL::MG_Config::ActiveBackendType = MobileGL::BackendType::DirectVulkan; + }); + EXPECT_EQ(b.started, 0) << "a Magma session was admitted into a process whose first session was Espryt"; + const std::string log = server.Log(); + EXPECT_NE(log.find("pinned backend type"), std::string::npos) << log; + EXPECT_NE(log.find("Hello.backendType disagrees with pinned backend"), std::string::npos) << log; +} + +// D8. Red with DieWithSupervisor's prctl deleted: the session child is still running (its client is +// holding the connection) when the wait for it runs out. +TEST(SupervisorChildren, AForkedSessionChildDiesWithItsSupervisor) { + // Orphans of this test's descendants are reparented HERE, so the session child can be reaped and + // its cause of death read. + ASSERT_EQ(::prctl(PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0), 0); + ServerProcess supervisor; + ASSERT_TRUE(LaunchSupervisor("pdeathsig", &supervisor)) << supervisor.Log(); + pid_t held = -1; + const PeerReport a = + RunPeer(supervisor.endpoint, ApplyOneRecord, /*stopCleanly=*/false, 30000, /*holdMs=*/8000, &held); + ASSERT_EQ(a.started, 1) << a.note << "\n" << supervisor.Log(); + const auto sessionChild = static_cast(a.serverPid); + ASSERT_GT(sessionChild, 0); + ASSERT_NE(sessionChild, supervisor.pid) << "not a forked session child"; + ASSERT_EQ(::kill(sessionChild, 0), 0) << "the session child is not running before the supervisor dies"; + + ::kill(supervisor.pid, SIGKILL); + ASSERT_TRUE(supervisor.WaitExit(5000)); + int status = 0; + const bool reaped = WaitFor( + [&] { return ::waitpid(sessionChild, &status, WNOHANG) == sessionChild; }, 5000); + EXPECT_TRUE(reaped) << "the session child outlived its supervisor by 5 s with its client still connected"; + if (reaped) { + EXPECT_TRUE(WIFSIGNALED(status) && WTERMSIG(status) == SIGKILL) + << "the session child ended, but not by the parent-death SIGKILL (status 0x" << std::hex << status << ")"; + } else { + ::kill(sessionChild, SIGKILL); + (void)::waitpid(sessionChild, &status, 0); + } + ReleaseHeldPeer(held); +} diff --git a/MobileGL/MG_Test/Wire/P12ServerRig.h b/MobileGL/MG_Test/Wire/P12ServerRig.h new file mode 100644 index 00000000..d75ba9f1 --- /dev/null +++ b/MobileGL/MG_Test/Wire/P12ServerRig.h @@ -0,0 +1,407 @@ +// MobileGL - MobileGL/MG_Test/Wire/P12ServerRig.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 + +// P12 (on-screen server window): the two-process rig InProcessServerTest and ServerOwnedSurfaceTest +// share - a server (the exec'd `--serve` supervisor, or a forked process running the IN-PROCESS +// server entry on a thread), forked peers that talk to it over loopback TCP, and the healthy +// session PeerLatchTest's second session runs (a record applied and, with headless EGL, a green clear +// read back). The shapes are PeerLatchTest's, trimmed to what these two suites need; see there for +// the reasons behind each (why peers are forks, why a failing case keeps its logs). +// +// Header-only and everything in an anonymous namespace: each suite is one translation unit. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace { + + namespace P12 { + namespace P = MobileGL::MG_Pipe; + namespace Remote = MobileGL::MG_Remote; + namespace Debug = MobileGL::MG_Util::Debug; + using MobileGL::Int32; + using MobileGL::Uint32; + using MobileGL::Uint64; + using MobileGL::Uint8; + + inline std::string ServerImage() { + if (const char* explicitPath = std::getenv("MOBILEGL_TEST_SERVER_PATH")) return explicitPath; + return "libMobileGLServer.so"; + } + + // The server (and a server process forked from this one) inherits this; its make-current + // needs a headless EGL the way ServerLoopEglTest's does. + inline void PinHeadlessEgl() { + if (std::getenv("EGL_PLATFORM") == nullptr) ::setenv("EGL_PLATFORM", "surfaceless", 1); + if (std::getenv("__EGL_VENDOR_LIBRARY_FILENAMES") == nullptr) { + const char* mesa = "/usr/share/glvnd/egl_vendor.d/50_mesa.json"; + std::error_code ec; + if (std::filesystem::exists(mesa, ec)) ::setenv("__EGL_VENDOR_LIBRARY_FILENAMES", mesa, 1); + } + ::unsetenv("DISPLAY"); + ::unsetenv("WAYLAND_DISPLAY"); + } + + inline std::string ReadFile(const std::string& path) { + std::ifstream in(path, std::ios::binary); + std::stringstream all; + all << in.rdbuf(); + return all.str(); + } + + inline int FreeLoopbackPort() { + const int fd = ::socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) return -1; + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = 0; + socklen_t len = sizeof(addr); + int port = -1; + if (::bind(fd, reinterpret_cast(&addr), sizeof(addr)) == 0 && + ::getsockname(fd, reinterpret_cast(&addr), &len) == 0) { + port = ntohs(addr.sin_port); + } + ::close(fd); + return port; + } + + inline bool WaitFor(const std::function& predicate, int timeoutMs) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs); + while (std::chrono::steady_clock::now() < deadline) { + if (predicate()) return true; + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + } + return predicate(); + } + + inline Uint32 Count(const std::string& haystack, const std::string& needle) { + Uint32 n = 0; + for (auto at = haystack.find(needle); at != std::string::npos; at = haystack.find(needle, at + 1)) ++n; + return n; + } + + // A server process this test owns: its pid, its tcp:// endpoint, and the log base its roles + // write under. Stopped with SIGTERM (then SIGKILL) and reaped; the logs are removed unless the + // case failed - then they are the evidence the messages quote. + struct ServerProcess { + pid_t pid = -1; + std::string endpoint; + std::string logBase; + int exitStatus = -1; // waitpid status once reaped + + std::string Log() const { return ReadFile(Debug::RoleLogPath(logBase.c_str(), Debug::LogRole::Server)); } + + // Waits for the process to end on its own, up to `timeoutMs`. + bool WaitExit(int timeoutMs) { + if (pid <= 0) return true; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs); + for (;;) { + int status = 0; + const pid_t got = ::waitpid(pid, &status, WNOHANG); + if (got == pid) { + exitStatus = status; + pid = -1; + return true; + } + if (got < 0) { + pid = -1; + return true; + } + if (std::chrono::steady_clock::now() >= deadline) return false; + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + } + void Stop() { + if (pid <= 0) return; + ::kill(pid, SIGTERM); + if (!WaitExit(10000)) { + ::kill(pid, SIGKILL); + (void)WaitExit(5000); + } + } + ~ServerProcess() { + Stop(); + if (logBase.empty() || ::testing::Test::HasFailure()) return; + std::error_code ec; + std::filesystem::remove(Debug::RoleLogPath(logBase.c_str(), Debug::LogRole::Server), ec); + std::filesystem::remove(Debug::RoleLogPath(logBase.c_str(), Debug::LogRole::Client), ec); + std::filesystem::remove(logBase, ec); + } + }; + + // The exec'd `--serve` supervisor (the fork-per-session shape), on a fresh loopback port. + inline bool LaunchSupervisor(const std::string& label, ServerProcess* server) { + server->logBase = "/tmp/mgl-p12-" + label + "-" + std::to_string(::getpid()) + ".log"; + ::setenv("MOBILEGL_LOG_FILE_PATH", server->logBase.c_str(), 1); + PinHeadlessEgl(); + for (int attempt = 0; attempt < 5; ++attempt) { + const int port = FreeLoopbackPort(); + if (port <= 0) return false; + server->endpoint = "tcp://127.0.0.1:" + std::to_string(port); + Debug::TruncateRoleLogs(server->logBase.c_str()); + Remote::Server::LaunchedServer launched; + if (Remote::Server::LaunchServerWithArgs(ServerImage(), server->endpoint, {"--serve"}, &launched) != + MOBILEGL_OK) + return false; + server->pid = launched.pid; + bool listening = false; + (void)WaitFor( + [&] { + if (server->Log().find("listening on") != std::string::npos) return listening = true; + return server->WaitExit(0) && server->pid <= 0; + }, + 10000); + if (listening) return true; + if (server->pid > 0) return false; // alive and silent: not a port race + } + return false; + } + + // ------------------------------------------------------------------------------------ + // Forked peers (PeerLatchTest's RunPeer, trimmed) + // ------------------------------------------------------------------------------------ + + struct PeerReport { + Int32 started = 0; // handshake answered Welcome + Uint32 serverPid = 0; // Welcome::serverPid + Int32 forged = 0; // the peer's bad bytes went out + Int32 applied = 0; // a record was applied + Int32 egl = 0; // headless EGL came up in the session + Int32 status = -99; // the read-back's reply status + Uint32 pixel = 0; // the read-back texel + char note[256] = {}; + }; + + inline void Note(PeerReport& r, const char* text) { std::snprintf(r.note, sizeof(r.note), "%s", text); } + + using PeerBody = std::function; + + // Runs `body` in a forked peer connected over TCP to `endpoint`. `holdMs` > 0: the peer reports + // once `body` returns and then holds its connections open and silent that long; the caller + // gets its pid in `heldPeer` and owes it ReleaseHeldPeer. `before` runs in the peer first (a + // config it must differ in). + inline PeerReport RunPeer(const std::string& endpoint, const PeerBody& body, bool stopCleanly, + int timeoutMs = 30000, int holdMs = 0, pid_t* heldPeer = nullptr, + const std::function& before = {}) { + PeerReport report{}; + int fds[2] = {-1, -1}; + if (::pipe(fds) != 0) { + Note(report, "pipe failed"); + return report; + } + std::fflush(nullptr); + const pid_t pid = ::fork(); + if (pid == 0) { + ::close(fds[0]); + if (before) before(); + PeerReport r{}; + auto& client = Remote::Client::ClientSessionInstance(); + MobileGL::MG_Config::Ipc.Control = endpoint.c_str(); + MobileGL::MG_Config::Ipc.Data = "auto"; + const MobileGLResult started = client.StartSpawned(); + r.started = started == MOBILEGL_OK ? 1 : 0; + r.serverPid = client.PeerServerPid(); + if (r.started) body(client, r); + else std::snprintf(r.note, sizeof(r.note), "start rc=%d", static_cast(started)); + if (stopCleanly && r.started) client.Stop(); + const ssize_t wrote = ::write(fds[1], &r, sizeof(r)); + (void)wrote; + std::fflush(nullptr); + if (holdMs > 0) std::this_thread::sleep_for(std::chrono::milliseconds(holdMs)); + ::_exit(0); + } + ::close(fds[1]); + if (pid < 0) { + ::close(fds[0]); + Note(report, "fork failed"); + return report; + } + pollfd pfd{fds[0], POLLIN, 0}; + const int ready = ::poll(&pfd, 1, timeoutMs); + if (ready > 0) { + const ssize_t got = ::read(fds[0], &report, sizeof(report)); + if (got != static_cast(sizeof(report))) { + report = PeerReport{}; + Note(report, "peer died before reporting"); + } + } else { + Note(report, "peer timed out"); + ::kill(pid, SIGKILL); + } + ::close(fds[0]); + if (holdMs > 0 && heldPeer != nullptr && ready > 0) { + *heldPeer = pid; + return report; + } + int status = 0; + ::waitpid(pid, &status, 0); + return report; + } + + inline void ReleaseHeldPeer(pid_t peer) { + if (peer <= 0) return; + ::kill(peer, SIGKILL); + int status = 0; + ::waitpid(peer, &status, 0); + } + + // A control frame straight onto the control connection: the SurfaceOp latch rows' carrier. + inline bool SendSurfaceOp(Remote::Client::ClientSession& client, PeerReport& r, Uint8 kind, Uint8 windowKind, + Uint64 token) { + flatbuffers::FlatBufferBuilder builder(256); + const auto op = ::MobileGL::Wire::CreateSurfaceOp( + builder, /*seq=*/4242, static_cast<::MobileGL::Wire::SurfaceOpKind>(kind), /*display=*/1, + /*surface=*/1, static_cast<::MobileGL::Wire::WindowKind>(windowKind), token, 16, 16, 0, 1, 1); + const auto envelope = + ::MobileGL::Wire::CreateCtrlEnvelope(builder, ::MobileGL::Wire::CtrlMsg::SurfaceOp, op.Union()); + ::MobileGL::Wire::FinishCtrlEnvelopeBuffer(builder, envelope); + Remote::Transport::ITransport* control = client.Control_Plane(); + if (control == nullptr || + control->SendFrame(MobileGLByteSpan{builder.GetBufferPointer(), builder.GetSize()}) != MOBILEGL_OK) { + Note(r, "the control frame could not be sent"); + return false; + } + r.forged = 1; + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + return true; + } + + inline bool Emit(Remote::Client::ClientSession& client, P::MGPWireOp op, const void* payload, Uint64 bytes, + const void* tail = nullptr, Uint64 tailBytes = 0) { + Int32 status = Remote::Wire::ReplySink::kStatusError; + const Uint64 seq = client.EmitAndWait(op, payload, bytes, tail, tailBytes, nullptr, 0, &status); + return seq != Remote::Wire::kInvalidSeq && + client.WaitForApplied(seq, 5000) == Remote::Transport::SessionWait::Reached; + } + + // The client's virtual EGL handles (EGLImpl mints small integers; ServerLoopEglTest's shape). + inline EGLDisplay Dpy() { return reinterpret_cast(static_cast(0x1)); } + inline EGLSurface Surf() { return reinterpret_cast(static_cast(0x1)); } + inline EGLContext Ctx() { return reinterpret_cast(static_cast(0x1)); } + + inline bool BringUpEgl(PeerReport& r) { + EGLint major = -1; + EGLint minor = -1; + if (!Remote::Server::ServerInitializeEGLDisplay(Dpy(), &major, &minor)) return Note(r, "no EGL display"), false; + if (!Remote::Server::ServerCreateEGLPbufferSurface(Surf(), 8, 8)) return Note(r, "no pbuffer"), false; + if (!Remote::Server::ServerMakeEGLCurrent(Dpy(), Surf(), Surf(), Ctx())) return Note(r, "no make-current"), false; + r.egl = 1; + return true; + } + + // A HEALTHY SESSION: one record applied and, with headless EGL, a green clear read back - + // PeerLatchTest's session B, verbatim in what it sends. + inline void HealthySession(Remote::Client::ClientSession& client, PeerReport& r) { + P::MGPMemoryBarrier barrier{}; + barrier.Bits = 0x2000u; + r.applied = Emit(client, P::MGPWireOp::MemoryBarrier, &barrier, sizeof(barrier)) ? 1 : 0; + if (!BringUpEgl(r)) return; + P::MGPFramebufferState framebuffer{}; + framebuffer.Fbo = P::kMGPipeDefaultFramebuffer; + framebuffer.Target = static_cast(P::MGPipeFramebufferTarget::Both); + framebuffer.IsDefault = 1; + framebuffer.Complete = 1; + framebuffer.Width = framebuffer.Height = 8; + framebuffer.Layers = framebuffer.Samples = 1; + framebuffer.FixedSampleLocations = 1; + for (auto& drawBuffer : framebuffer.DrawBuffers) drawBuffer = -1; + framebuffer.DrawBuffers[0] = 0; + framebuffer.ContentHash = 1; + P::MGPPixelPackState pack{}; + pack.Pack.Alignment = 4; + P::MGPSamplerViews views{}; + views.Count = 1; + const P::MGPBoundView unboundView{}; + P::MGPSamplerStates samplers{}; + samplers.Count = 1; + const P::MGPipeHandle unboundSampler = P::kMGPipeNullHandle; + const P::MGPContextValues context{}; + MobileGL::RenderStateParameters params{}; + for (auto& mask : params.ColorMasks) mask = MobileGL::BoolVec4(true, true, true, true); + std::vector pipelineBytes(P::kMGPipePipelineChunkBytes, 0); + P::MGPipeGatherPipelineBytes(params, pipelineBytes.data()); + P::MGPRenderStateDesc cso{}; + cso.Cso = P::MGPipeHandle{P::kMGPipeFirstAllocatableSlot, 1u}; + cso.BaseCso = P::kMGPipeNullHandle; + cso.ChunkMask = P::MGPipeRenderStateChunkDetail::kAllPipelineHalfBits; + cso.Blob = client.Encoder().StageBytes(pipelineBytes.data(), pipelineBytes.size()); + P::MGPBindRenderState bindCso{}; + bindCso.Cso = cso.Cso; + bindCso.Version = 1; + bindCso.PipelineVersion = 1; + if (!Emit(client, P::MGPWireOp::CreateRenderState, &cso, sizeof(cso)) || + !Emit(client, P::MGPWireOp::BindRenderState, &bindCso, sizeof(bindCso))) + return Note(r, "the render state was not applied"); + if (!Emit(client, P::MGPWireOp::SetFramebufferState, &framebuffer, sizeof(framebuffer)) || + !Emit(client, P::MGPWireOp::SetPixelPackState, &pack, sizeof(pack)) || + !Emit(client, P::MGPWireOp::SetSamplerViews, &views, sizeof(views), &unboundView, sizeof(unboundView)) || + !Emit(client, P::MGPWireOp::BindSamplerStates, &samplers, sizeof(samplers), &unboundSampler, + sizeof(unboundSampler)) || + !Emit(client, P::MGPWireOp::SetContextValues, &context, sizeof(context))) + return Note(r, "the default framebuffer's inputs were not applied"); + P::MGPClear clear{}; + clear.Fbo = P::kMGPipeNullHandle; + clear.Kind = P::kMGPipeClearKindColor; + clear.DrawBufferIndex = 0; + clear.BufferMask = GL_COLOR_BUFFER_BIT; + clear.ValueClass = P::kMGPipeClearValueClassFloat; + const float green[4] = {0.0f, 1.0f, 0.0f, 1.0f}; + std::memcpy(clear.ColorValue, green, sizeof(green)); + if (!Emit(client, P::MGPWireOp::Clear, &clear, sizeof(clear))) return Note(r, "clear not applied"); + P::MGPReadbackInfo read{}; + read.Res = P::kMGPipeNullHandle; + read.Box = P::MGPBox{0, 0, 0, 1, 1, 1}; + read.Format = GL_RGBA; + read.Type = GL_UNSIGNED_BYTE; + read.DstSize = 4; + Uint8 texel[4] = {}; + Int32 status = Remote::Wire::ReplySink::kStatusError; + Uint64 replySize = 0; + const Uint64 seq = client.EmitAndWait(P::MGPWireOp::ReadPixels, &read, sizeof(read), nullptr, 0, texel, + sizeof(texel), &status, &replySize); + r.status = status; + if (seq == Remote::Wire::kInvalidSeq || replySize != sizeof(texel)) return Note(r, "no read-back"); + std::memcpy(&r.pixel, texel, sizeof(r.pixel)); + } + } // namespace P12 + +} // namespace diff --git a/MobileGL/MG_Test/Wire/ServerOwnedSurfaceTest.cpp b/MobileGL/MG_Test/Wire/ServerOwnedSurfaceTest.cpp new file mode 100644 index 00000000..b6821b97 --- /dev/null +++ b/MobileGL/MG_Test/Wire/ServerOwnedSurfaceTest.cpp @@ -0,0 +1,245 @@ +// MobileGL - MobileGL/MG_Test/Wire/ServerOwnedSurfaceTest.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 + +// P12 (on-screen server window), END TO END ON THE HOST: a real MobileGL client - the EGL entry +// points, MobileGL's own initialisation, the spawn/tcp session - against a real `--serve` supervisor +// that, like every host server, owns NO display. +// +// What is proven, each on both sides of the wire: +// * with MOBILEGL_IPC_SURFACE=server a headless client's eglCreateWindowSurface(NULL) reaches the +// server as a ServerOwned request, the server REFUSES it BY NAME (it owns no display), the client +// fails it BY NAME (EGL_BAD_NATIVE_WINDOW, and its own log line), and the session is NOT latched: +// it goes on to create a pbuffer, make it current, and end exit=0; +// * without the knob a NULL window is the client's own EGL_BAD_NATIVE_WINDOW and the server never +// hears of it; +// * D4: a session whose first surface was a pbuffer (offscreen) is refused a ServerOwned surface as +// SurfaceModeMismatch - named by the server and by the client - and carries on. + +#include "P12ServerRig.h" + +#include + +using namespace MobileGL; +using namespace P12; + +namespace { + + namespace EGL = MobileGL::MG_Impl::EGLImpl; + + enum class Scenario { + ServerOwnedFirst, // the ServerOwned create, then a pbuffer + NullWindowWithoutKnob, // the knob unset: a NULL window, then a pbuffer + PbufferThenServerOwned, // a pbuffer first (offscreen), then the ServerOwned create + }; + + struct EglPeerReport { + Int32 initialized = 0; + Int32 windowCreated = 0; // eglCreateWindowSurface returned a surface + Int32 windowError = 0; // eglGetError after it + Int32 pbufferCreated = 0; + Int32 madeCurrent = 0; + Int32 madeCurrentAfter = 0; // the pbuffer still current after the refused window (mismatch case) + Uint32 serverPid = 0; + char note[256] = {}; + }; + + // A forked peer that is a whole MobileGL client: the environment a deployment sets, then the EGL + // entry points. Where its lines land is ClientLog's note below. + EglPeerReport RunEglPeer(const ServerProcess& server, Scenario scenario, bool surfaceKnob, + const std::string& clientLogBase) { + EglPeerReport 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", clientLogBase.c_str(), 1); + ::setenv("MOBILEGL_TRANSPORT", "spawn", 1); + ::setenv("MOBILEGL_IPC_CONTROL", server.endpoint.c_str(), 1); + ::setenv("MOBILEGL_BACKEND_TYPE", "DirectGLES", 1); + if (surfaceKnob) ::setenv("MOBILEGL_IPC_SURFACE", "server", 1); + else ::unsetenv("MOBILEGL_IPC_SURFACE"); + EglPeerReport r{}; + const auto note = [&r](const char* text) { std::snprintf(r.note, sizeof(r.note), "%s", text); }; + const EGLDisplay dpy = EGL::GetDisplay(EGL_DEFAULT_DISPLAY); + EGLint major = 0; + EGLint minor = 0; + if (dpy == EGL_NO_DISPLAY || EGL::Initialize(dpy, &major, &minor) != EGL_TRUE) { + note("eglInitialize failed (no spawn session?)"); + } else { + r.initialized = 1; + if (auto* session = Remote::Client::ClientSession::Active()) r.serverPid = session->PeerServerPid(); + (void)EGL::BindAPI(EGL_OPENGL_API); + const EGLint configAttribs[] = {EGL_SURFACE_TYPE, EGL_WINDOW_BIT | EGL_PBUFFER_BIT, EGL_NONE}; + EGLConfig config = nullptr; + EGLint count = 0; + (void)EGL::ChooseConfig(dpy, configAttribs, &config, 1, &count); + const EGLint contextAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 3, EGL_NONE}; + const EGLContext context = EGL::CreateContext(dpy, config, EGL_NO_CONTEXT, contextAttribs); + const EGLint windowAttribs[] = {EGL_WIDTH, 64, EGL_HEIGHT, 48, EGL_NONE}; + const EGLint pbufferAttribs[] = {EGL_WIDTH, 8, EGL_HEIGHT, 8, EGL_NONE}; + EGLSurface pbuffer = EGL_NO_SURFACE; + const auto tryWindow = [&] { + const EGLSurface window = EGL::CreateWindowSurface(dpy, config, NativeWindowType{}, windowAttribs); + r.windowCreated = window != EGL_NO_SURFACE ? 1 : 0; + r.windowError = EGL::GetError(); + }; + const auto tryPbuffer = [&] { + pbuffer = EGL::CreatePbufferSurface(dpy, config, pbufferAttribs); + r.pbufferCreated = pbuffer != EGL_NO_SURFACE ? 1 : 0; + if (r.pbufferCreated) r.madeCurrent = EGL::MakeCurrent(dpy, pbuffer, pbuffer, context) == EGL_TRUE; + }; + if (scenario == Scenario::PbufferThenServerOwned) { + tryPbuffer(); + tryWindow(); + if (r.pbufferCreated) { + // The session carries on on its own path after the refusal. + (void)EGL::MakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + r.madeCurrentAfter = EGL::MakeCurrent(dpy, pbuffer, pbuffer, context) == EGL_TRUE; + } + } else { + tryWindow(); + tryPbuffer(); + } + (void)EGL::MakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + if (pbuffer != EGL_NO_SURFACE) (void)EGL::DestroySurface(dpy, pbuffer); + if (context != EGL_NO_CONTEXT) (void)EGL::DestroyContext(dpy, context); + (void)EGL::Terminate(dpy); + } + const ssize_t wrote = ::write(fds[1], &r, sizeof(r)); + (void)wrote; + std::fflush(nullptr); + ::_exit(0); + } + ::close(fds[1]); + if (pid < 0) { + ::close(fds[0]); + return report; + } + pollfd pfd{fds[0], POLLIN, 0}; + if (::poll(&pfd, 1, 60000) > 0) { + if (::read(fds[0], &report, sizeof(report)) != static_cast(sizeof(report))) report = EglPeerReport{}; + } else { + ::kill(pid, SIGKILL); + } + ::close(fds[0]); + int status = 0; + ::waitpid(pid, &status, 0); + return report; + } + + // The supervisor's reap line for one session child, or "" while it has not been reaped. + std::string ReapLine(const ServerProcess& server, Uint32 sessionPid) { + const std::string log = server.Log(); + const std::string needle = "session pid=" + std::to_string(sessionPid) + " reaped "; + const auto at = log.find(needle); + if (at == std::string::npos) return {}; + const auto end = log.find('\n', at); + return log.substr(at, end == std::string::npos ? std::string::npos : end - at); + } + + // WHERE THE PEER'S LINES LAND. The peer's forwarded SERVER lines are written under its own base + // (WritePeerLog reads MOBILEGL_LOG_FILE_PATH at each write), so they never truncate the + // supervisor's log. Its own CLIENT lines, though, go through the client-role FILE* this test + // process already opened when it launched the supervisor (ServerSpawn logs), which the fork + // inherits - so they are read from the supervisor base's client file. Nothing else in this + // process writes a ServerOwned line there. + struct ClientLog { + std::string base; + std::string inheritedBase; + ClientLog(const std::string& label, const ServerProcess& server) + : base("/tmp/mgl-p12-client-" + label + "-" + std::to_string(::getpid()) + ".log"), + inheritedBase(server.logBase) { + Debug::TruncateRoleLogs(base.c_str()); + } + // This client's OWN lines (and the test process's own spawn lines, which name no surface). + std::string Own() const { + return ReadFile(Debug::RoleLogPath(base.c_str(), Debug::LogRole::Client)) + + ReadFile(Debug::RoleLogPath(inheritedBase.c_str(), Debug::LogRole::Client)); + } + ~ClientLog() { + if (::testing::Test::HasFailure()) return; + std::error_code ec; + std::filesystem::remove(Debug::RoleLogPath(base.c_str(), Debug::LogRole::Client), ec); + std::filesystem::remove(Debug::RoleLogPath(base.c_str(), Debug::LogRole::Server), ec); + std::filesystem::remove(base, ec); + } + }; + +} // namespace + +// THE HEADLINE (D1 + D3 on the host). Red with the server's no-display refusal made a latch: the +// session ends exit=75 and the pbuffer after it is never created; red with the client's refusal +// naming removed: the client log has no line of its own. +TEST(ServerOwnedSurface, ADisplaylessServerRefusesItByNameOnBothSidesAndTheSessionCarriesOn) { + ServerProcess server; + ASSERT_TRUE(LaunchSupervisor("serverowned", &server)) << server.Log(); + ClientLog client("serverowned", server); + const EglPeerReport r = RunEglPeer(server, Scenario::ServerOwnedFirst, /*surfaceKnob=*/true, client.base); + ASSERT_EQ(r.initialized, 1) << r.note << "\n" << client.Own(); + EXPECT_EQ(r.windowCreated, 0) << "a server with no display created a server-owned window surface"; + EXPECT_EQ(r.windowError, EGL_BAD_NATIVE_WINDOW); + + std::string reap; + ASSERT_TRUE(WaitFor([&] { return !(reap = ReapLine(server, r.serverPid)).empty(); }, 15000)) << server.Log(); + const std::string serverLog = server.Log(); + EXPECT_NE(serverLog.find("Refuse ServerOwned: this server owns no display"), std::string::npos) + << "the server did not name its refusal:\n" << serverLog; + EXPECT_EQ(serverLog.find("Fatal{"), std::string::npos) << "the refusal latched or killed the session:\n" << serverLog; + EXPECT_NE(reap.find("exit=0 "), std::string::npos) << "the session did not end cleanly: " << reap; + const std::string clientLog = client.Own(); + EXPECT_NE(clientLog.find("Refuse ServerOwned (NoServerDisplay)"), std::string::npos) + << "the client did not name the refusal in its own log:\n" << clientLog; + + // Not latched: the same session went on to its pbuffer. Its make-current needs headless EGL. + EXPECT_EQ(r.pbufferCreated, 1) << "the session did not carry on after the refusal"; + EXPECT_NE(serverLog.find("surface=pbuffer 8x8"), std::string::npos) << "no pbuffer arm proof:\n" << serverLog; + if (r.madeCurrent == 0) GTEST_SKIP() << "refusal proven; the make-current half needs headless EGL"; +} + +// D1: without the knob a NULL window never leaves the client. Red with the client's null check made +// unconditional in the other direction (the knob ignored): the server logs a ServerOwned refusal. +TEST(ServerOwnedSurface, WithoutTheKnobANullWindowIsTheClientsOwnRefusal) { + ServerProcess server; + ASSERT_TRUE(LaunchSupervisor("nullwindow", &server)) << server.Log(); + ClientLog client("nullwindow", server); + const EglPeerReport r = RunEglPeer(server, Scenario::NullWindowWithoutKnob, /*surfaceKnob=*/false, client.base); + ASSERT_EQ(r.initialized, 1) << r.note; + EXPECT_EQ(r.windowCreated, 0); + EXPECT_EQ(r.windowError, EGL_BAD_NATIVE_WINDOW); + ASSERT_TRUE(WaitFor([&] { return !ReapLine(server, r.serverPid).empty(); }, 15000)) << server.Log(); + EXPECT_EQ(server.Log().find("ServerOwned"), std::string::npos) << "a NULL window without the knob reached the server"; + EXPECT_EQ(r.pbufferCreated, 1); +} + +// D4. The first surface was a pbuffer, so the session is offscreen; the ServerOwned create after it is +// SurfaceModeMismatch - checked BEFORE the display (this server has none, and the answer is still +// the mode) - named on both sides, and the session keeps its pbuffer. Red with the mode check deleted: +// the server answers NoServerDisplay instead. +TEST(ServerOwnedSurface, AnOffscreenSessionRefusesAServerOwnedSurfaceAsSurfaceModeMismatch) { + ServerProcess server; + ASSERT_TRUE(LaunchSupervisor("modemismatch", &server)) << server.Log(); + ClientLog client("modemismatch", server); + const EglPeerReport r = RunEglPeer(server, Scenario::PbufferThenServerOwned, /*surfaceKnob=*/true, client.base); + ASSERT_EQ(r.initialized, 1) << r.note; + if (r.pbufferCreated == 0) GTEST_SKIP() << "the offscreen mode needs a pbuffer, and this host has no headless EGL"; + EXPECT_EQ(r.windowCreated, 0) << "an offscreen session went on-screen"; + EXPECT_EQ(r.windowError, EGL_BAD_NATIVE_WINDOW); + std::string reap; + ASSERT_TRUE(WaitFor([&] { return !(reap = ReapLine(server, r.serverPid)).empty(); }, 15000)) << server.Log(); + const std::string serverLog = server.Log(); + EXPECT_NE(serverLog.find("SurfaceModeMismatch - a ServerOwned CreateWindowSurface"), std::string::npos) + << serverLog; + EXPECT_EQ(serverLog.find("Refuse ServerOwned: this server owns no display"), std::string::npos) + << "the mode was not asked first"; + EXPECT_EQ(serverLog.find("Fatal{"), std::string::npos) << serverLog; + EXPECT_NE(reap.find("exit=0 "), std::string::npos) << reap; + EXPECT_NE(client.Own().find("SurfaceModeMismatch - eglCreateWindowSurface"), std::string::npos) << client.Own(); + EXPECT_EQ(r.madeCurrentAfter, r.madeCurrent) << "the session did not keep its pbuffer after the refusal"; +}