[MG_Test] (P12): host proofs - the in-process server entry on a thread (sequential sessions, latch reset, Busy, backend pin, stop), a forked session child's parent-death signal, and a MOBILEGL_IPC_SURFACE=server client refused by name on both sides by a display-less server

This commit is contained in:
2026-09-23 23:55:23 -04:00
parent 4fb5aadfa2
commit 37b53152cc
4 changed files with 918 additions and 3 deletions
+11 -3
View File
@@ -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
@@ -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 <MG_Remote/Server/InProcessServer.h>
#include <atomic>
#include <csignal>
#include <pthread.h>
#include <sys/prctl.h>
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<bool> returned{false};
std::atomic<int> 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<Uint8>(::MobileGL::Wire::SurfaceOpKind::CreateWindowSurface),
static_cast<Uint8>(::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<Uint32>(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<pid_t>(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);
}
+407
View File
@@ -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 <MG_Remote/Client/ClientSession.h>
#include <MG_Remote/FatalFunnel.h>
#include <MG_Remote/Protocol/generated/protocol_generated.h>
#include <MG_Remote/Server/ServerLoop.h>
#include <MG_Remote/Server/ServerSpawn.h>
#include <MG_Remote/Transport/ILink.h>
#include <MG_Remote/Wire/PipeWireCodec.h>
#include <MG_Pipe/MGPipeRenderStateSpans.h>
#include <MG_Pipe/PipeApply.h>
#include <MG_Util/Debug/Log.h>
#include <Config.h>
#include <gtest/gtest.h>
#include <chrono>
#include <csignal>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <functional>
#include <sstream>
#include <string>
#include <thread>
#include <vector>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <poll.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>
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<sockaddr*>(&addr), sizeof(addr)) == 0 &&
::getsockname(fd, reinterpret_cast<sockaddr*>(&addr), &len) == 0) {
port = ntohs(addr.sin_port);
}
::close(fd);
return port;
}
inline bool WaitFor(const std::function<bool()>& 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<void(Remote::Client::ClientSession&, PeerReport&)>;
// 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<void()>& 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<int>(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<ssize_t>(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<EGLDisplay>(static_cast<std::uintptr_t>(0x1)); }
inline EGLSurface Surf() { return reinterpret_cast<EGLSurface>(static_cast<std::uintptr_t>(0x1)); }
inline EGLContext Ctx() { return reinterpret_cast<EGLContext>(static_cast<std::uintptr_t>(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<Uint8>(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<Uint8> 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
@@ -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 <MG_Impl/EGLImpl/EGLImpl.h>
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<ssize_t>(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";
}