Files
CoopAllTheThings/tests/ipc_server_test.cpp
BlackMark 6d96531ac7 Graceful disconnect: tell the DLL to unhook everything
On an explicit Disconnect and on graceful tool exit, the host now asks the
injected DLL to remove every subsystem so the game runs exactly as if it was
never hooked (each hook restores its original bytes). The DLL stays injected but
dormant, ready for a later reconnect -- we never eject it.

Before, both paths just dropped the IPC channel (IpcServer::stop) without telling
the DLL, leaving the hooks active with frozen forwarded state until the game
exited.

- IpcServer::request_unhook_all() sets every subsystem_disabled flag (the DLL
  reconciles to fully unhooked on its next tick); all_hooks_removed() reads the
  hook registry back so the host can confirm the game is vanilla.
- InjectionPanel::disconnect_graceful() requests the unhook, waits (bounded) for
  the registry to clear, then stops. Wired into the Disconnect button (700ms) and
  the destructor (300ms). The flags persist in the section the DLL keeps alive, so
  the unhook completes even if the host exits before confirming.

Tests (failing first):
- ipc_server_test: request_unhook_all() disables all subsystems; all_hooks_removed()
  tracks the registry. Deterministic, no game.
- mock_game_test test_graceful_disconnect: inject -> hooks installed -> request
  unhook-all -> every hook removed (game vanilla) while the DLL stays alive
  (heartbeat advancing). Full suite still passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 13:46:51 +02:00

104 lines
3.3 KiB
C++

// Unit test for the host-side IpcServer control operations that drive graceful disconnect
// (hook/src reconciles against these): request_unhook_all() must set every subsystem's
// "disabled" flag so the injected DLL removes all hooks, and all_hooks_removed() must report
// whether the DLL's hook registry currently has anything installed. No game / no DLL -- the
// test opens a second view of the same section to act as the hook would (publish a registry,
// read the control flags). Deterministic, no threads.
#include <cstdint>
#include <cstdio>
#include <windows.h>
#include "coop/protocol.hpp"
#include "coop/shared_memory.hpp"
#include "ipc/ipc_server.hpp"
using namespace coop;
namespace
{
int g_failures = 0;
void check(bool ok, const char* what)
{
std::printf("%s %s\n", ok ? " ok:" : "FAIL:", what);
if (!ok)
{
++g_failures;
}
}
bool all_disabled(const SharedBlock* b)
{
for (std::uint32_t s = 0; s < HookSubsys_Count; ++s)
{
if (b->control.subsystem_disabled[s].load(std::memory_order_acquire) != 1u)
{
return false;
}
}
return true;
}
bool none_disabled(const SharedBlock* b)
{
for (std::uint32_t s = 0; s < HookSubsys_Count; ++s)
{
if (b->control.subsystem_disabled[s].load(std::memory_order_acquire) != 0u)
{
return false;
}
}
return true;
}
} // namespace
int main()
{
const unsigned long pid = GetCurrentProcessId(); // section name is per-pid; no real game needed
IpcServer server;
if (!server.start(pid))
{
std::printf("FAIL: IpcServer::start\n");
return 1;
}
// A second view of the same section, standing in for the injected hook: it reads the control
// flags the host writes and publishes the hook registry the host reads back.
SharedMemory hook_view;
if (!hook_view.open(shared_memory_name(pid), sizeof(SharedBlock)))
{
std::printf("FAIL: open hook view\n");
return 1;
}
auto* block = hook_view.as<SharedBlock>();
// Fresh section: nothing disabled (0 = install), so the game gets every subsystem by default.
check(none_disabled(block), "fresh section requests all subsystems installed");
// Graceful disconnect step 1: ask the DLL to remove every hook.
server.request_unhook_all();
check(all_disabled(block), "request_unhook_all() disables every subsystem");
// all_hooks_removed() reflects the DLL's registry. Empty registry => nothing installed.
check(server.all_hooks_removed(), "all_hooks_removed() true when the registry is empty");
// Hook publishes one installed entry => something is still hooked.
block->status.hook_entry_count = 1;
block->status.hook_entries[0].installed = 1u;
check(!server.all_hooks_removed(), "all_hooks_removed() false while a hook is still installed");
// Hook finishes removing it => back to "nothing installed".
block->status.hook_entries[0].installed = 0u;
check(server.all_hooks_removed(), "all_hooks_removed() true once every entry is uninstalled");
// Reconnect re-enables: clearing a subsystem's disabled flag requests it back (symmetric op).
server.set_subsystem_enabled(HookSubsys_Input, true);
check(block->control.subsystem_disabled[HookSubsys_Input].load(std::memory_order_acquire) == 0u,
"re-enabling a subsystem clears its disabled flag (reconnect path)");
server.stop();
std::printf(g_failures == 0 ? "PASS ipc_server_test\n" : "FAILED ipc_server_test (%d)\n", g_failures);
return g_failures == 0 ? 0 : 1;
}