mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[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:
@@ -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);
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
Reference in New Issue
Block a user