From 03890b0ad7b1b86a9d3f707c392dbf66cb6ffd23 Mon Sep 17 00:00:00 2001 From: HarcoChen <37766874+HarcoChen@users.noreply.github.com> Date: Mon, 9 Nov 2026 20:27:26 +0800 Subject: [PATCH] Confirm POSIX frame sharing and reject unpublished captures --- CMakeLists.txt | 7 +++ include/modavd_extras.h | 7 ++- src/core.c | 17 ++++++- tests/test_core.c | 50 ++++++++++++++++++++ tests/test_open_posix.c | 102 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 181 insertions(+), 2 deletions(-) create mode 100644 tests/test_open_posix.c diff --git a/CMakeLists.txt b/CMakeLists.txt index eed67ad..180313a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,6 +55,13 @@ target_link_libraries(test_core PRIVATE mx_core) mx_link_system(test_core) add_test(NAME test_core COMMAND test_core) +if(NOT WIN32) + add_executable(test_open_posix tests/test_open_posix.c) + target_link_libraries(test_open_posix PRIVATE mx_core) + mx_link_system(test_open_posix) + add_test(NAME test_open_posix COMMAND test_open_posix) +endif() + if(WIN32) # external_renderer_ipc.dll: the compatibility shell for MaaCore's MuMu path. add_library(external_renderer_ipc SHARED shim/mumu_shim.c) diff --git a/include/modavd_extras.h b/include/modavd_extras.h index 80c5f49..735e1ac 100644 --- a/include/modavd_extras.h +++ b/include/modavd_extras.h @@ -71,7 +71,8 @@ enum { /* modavd_open_params.flags */ enum { - /* Do not ask the emulator console to start frame sharing when it is off. */ + /* Only open the shared mapping, without contacting the console. The caller + * must ensure it belongs to the current emulator and sharing is active. */ MODAVD_OPEN_NO_START = 1u << 0, }; @@ -129,6 +130,10 @@ MODAVD_API void modavd_set_log(modavd_log_fn fn, void* user); MODAVD_API int modavd_parse_console_port(const char* adb_address); /* Connects to an instance. On success *out is a handle for modavd_close(). + * On POSIX, unless NO_START is set, confirms sharing through the console before + * opening the mapping: named regions can survive an emulator restart. Console + * auth/command failures return MODAVD_E_IO, even if an old mapping exists. + * The first frame may not have arrived yet; capture then returns NOT_READY. * MODAVD_E_NOT_FOUND: frame sharing is off and nothing answers on the console port. * MODAVD_E_NOT_READY: the console answered (or MODAVD_OPEN_NO_START is set) but * frame sharing did not come up in time. */ diff --git a/src/core.c b/src/core.c index 27db02a..5fd297c 100644 --- a/src/core.c +++ b/src/core.c @@ -102,13 +102,26 @@ int modavd_open(const modavd_open_params* params, modavd_handle* out) } int timeout_ms = params->timeout_ms > 0 ? params->timeout_ms : MX_DEFAULT_TIMEOUT_MS; +#ifndef _WIN32 + /* POSIX names can outlive their producer. Confirm sharing before opening + * the name, since the new emulator may replace the stale object on start. + * Repeated starts reuse the active stream. NO_START trusts the mapping. */ + if (!(params->flags & MODAVD_OPEN_NO_START)) { + int console = mx_console_command(port, "screenrecord webrtc start", 3000); + if (console != MODAVD_OK) { + return console; + } + } +#endif mx_shm shm = { 0 }; int rc = mx_shm_open(port, &shm); if (rc == MODAVD_E_NOT_READY && !(params->flags & MODAVD_OPEN_NO_START)) { +#ifdef _WIN32 int console = mx_console_command(port, "screenrecord webrtc start", 3000); if (console == MODAVD_E_NOT_FOUND) { return MODAVD_E_NOT_FOUND; } +#endif for (int waited = 0; rc == MODAVD_E_NOT_READY && waited < timeout_ms; waited += 50) { sleep_ms(50); rc = mx_shm_open(port, &shm); @@ -296,7 +309,9 @@ int mx_copy_frame(const mx_shm* shm, uint32_t format, uint32_t flags, int simd, info->format = format; info->frame_number = vi->frame_number; info->timestamp_us = vi->ts_us; - if (w == 0 || h == 0) { + /* Dimensions are set before the first frame is published. The counter + * can wrap to zero, and a published static frame need not advance. */ + if (w == 0 || h == 0 || info->timestamp_us == 0) { return MODAVD_E_NOT_READY; } if (MX_HEADER_SIZE + (uint64_t)w * h * 4 > shm->size) { diff --git a/tests/test_core.c b/tests/test_core.c index c90013e..dbe5075 100644 --- a/tests/test_core.c +++ b/tests/test_core.c @@ -204,6 +204,55 @@ static void test_capture_from_fake_frame(void) fake_frame_destroy(&f); } +static void test_capture_before_first_frame(void) +{ + fake_frame f; + if (fake_frame_create(&f, 65012, 8, 4, 8, 4) != 0) { + ++g_failures; + return; + } + modavd_open_params p = { 0 }; + p.size = sizeof(p); + p.console_port = f.port; + p.flags = MODAVD_OPEN_NO_START; + modavd_handle h = NULL; + CHECK_EQ(modavd_open(&p, &h), MODAVD_OK); + if (h) { + mx_video_info* vi = (mx_video_info*)f.view; + vi->frame_number = 0; + vi->ts_us = 0; + uint8_t buf[8 * 4 * 4]; + modavd_frame_info info = { 0 }; + info.size = sizeof(info); + for (uint32_t format = MODAVD_FMT_BGRA; format <= MODAVD_FMT_RGBA; ++format) { + CHECK_EQ(modavd_capture(h, 0, format, 0, NULL, 0, &info), MODAVD_E_NOT_READY); + memset(buf, 0xA5, sizeof(buf)); + CHECK_EQ(modavd_capture(h, 0, format, 0, buf, sizeof(buf), &info), MODAVD_E_NOT_READY); + for (size_t i = 0; i < sizeof(buf); ++i) { + CHECK_EQ(buf[i], 0xA5); + } + } + + /* A counter may wrap to zero. A published, unchanged black frame is valid. */ + vi->ts_us = 123456789ull; + memset(f.view + MX_HEADER_SIZE, 0, 8 * 4 * 4); + for (int i = 0; i < 2; ++i) { + CHECK_EQ(modavd_capture(h, 0, MODAVD_FMT_BGRA, 0, buf, sizeof(buf), &info), MODAVD_OK); + CHECK_EQ(info.frame_number, 0); + CHECK_EQ(info.timestamp_us, 123456789ull); + for (size_t j = 0; j < sizeof(buf); ++j) { + CHECK_EQ(buf[j], 0); + } + } + /* The counter alone does not mean the timestamp was published yet. */ + vi->frame_number = 1; + vi->ts_us = 0; + CHECK_EQ(modavd_capture(h, 0, MODAVD_FMT_BGRA, 0, buf, sizeof(buf), &info), MODAVD_E_NOT_READY); + modavd_close(h); + } + fake_frame_destroy(&f); +} + /* The emulator writes without a lock. The hook publishes a frame after a copy, * as if the emulator wrote one meanwhile. */ typedef struct publish_state { @@ -416,6 +465,7 @@ int main(void) test_version_and_errors(); test_open_errors(); test_capture_from_fake_frame(); + test_capture_before_first_frame(); test_capture_new_frame_mid_copy(); test_capture_odd_sizes(); test_convert_row(); diff --git a/tests/test_open_posix.c b/tests/test_open_posix.c new file mode 100644 index 0000000..85da4ee --- /dev/null +++ b/tests/test_open_posix.c @@ -0,0 +1,102 @@ +/* + * POSIX open tests with real shared memory and a controlled console response. + * + * Copyright 2026 HarcoChen + * SPDX-License-Identifier: LGPL-3.0-or-later + */ +#include + +#include "fake_frame.h" + +static int failures; +static int console_calls; +static int console_result; +static fake_frame* replacement; + +#define CHECK(condition) \ + do { \ + if (!(condition)) { \ + fprintf(stderr, "%s:%d: %s\n", __FILE__, __LINE__, #condition); \ + ++failures; \ + } \ + } while (0) + +/* This definition replaces console.c from the static archive. No network or + * auth token is needed; core.c and shm.c run through the public entry points. */ +int mx_console_command(int port, const char* command, int timeout_ms) +{ + ++console_calls; + CHECK(port == 65013); + CHECK(strcmp(command, "screenrecord webrtc start") == 0); + CHECK(timeout_ms > 0); + if (replacement) { + CHECK(fake_frame_create(replacement, port, 8, 4, 8, 4) == 0); + } + return console_result; +} + +int main(void) +{ + fake_frame stale; + if (fake_frame_create(&stale, 65013, 8, 4, 8, 4) != 0) { + return 1; + } + fake_frame_set_size(&stale, 4, 2); + modavd_open_params p = { 0 }; + p.size = sizeof(p); + p.console_port = stale.port; + p.timeout_ms = 100; + modavd_handle h = NULL; + + /* A valid old frame must not hide an absent or inaccessible producer. */ + console_result = MODAVD_E_NOT_FOUND; + CHECK(modavd_open(&p, &h) == MODAVD_E_NOT_FOUND); + CHECK(h == NULL); + modavd_close(h); + console_result = MODAVD_E_IO; + CHECK(modavd_open(&p, &h) == MODAVD_E_IO); + CHECK(h == NULL); + modavd_close(h); + CHECK(console_calls == 2); + + /* NO_START explicitly trusts the supplied mapping, without a console. */ + p.flags = MODAVD_OPEN_NO_START; + CHECK(modavd_open(&p, &h) == MODAVD_OK); + CHECK(console_calls == 2); + modavd_close(h); + + /* The new producer replaces the old name when starting. Open after that + * command so that the handle sees the new 8x4 frame, not the old 4x2 one. */ + fake_frame current = { 0 }; + replacement = ¤t; + console_result = MODAVD_OK; + p.flags = 0; + CHECK(modavd_open(&p, &h) == MODAVD_OK); + CHECK(console_calls == 3); + CHECK(h != NULL); + replacement = NULL; + modavd_handle second = NULL; + CHECK(modavd_open(&p, &second) == MODAVD_OK); + CHECK(console_calls == 4); + CHECK(second != NULL); + modavd_frame_info info = { 0 }; + info.size = sizeof(info); + uint8_t buf[8 * 4 * 4]; + if (h && second) { + CHECK(modavd_capture(h, 0, MODAVD_FMT_BGRA, 0, buf, sizeof(buf), &info) == MODAVD_OK); + CHECK(info.width == 8 && info.height == 4); + CHECK(modavd_capture(second, 0, MODAVD_FMT_BGRA, 0, buf, sizeof(buf), &info) == MODAVD_OK); + CHECK(info.width == 8 && info.height == 4); + modavd_close(second); + second = NULL; + /* Closing another reader must not stop a static, already published frame. */ + CHECK(modavd_capture(h, 0, MODAVD_FMT_BGRA, 0, buf, sizeof(buf), &info) == MODAVD_OK); + } + modavd_close(second); + modavd_close(h); + if (current.view) { + fake_frame_destroy(¤t); + } + fake_frame_destroy(&stale); + return failures ? 1 : 0; +}