Reconnect to an already-injected DLL (reuse it, survive a tool restart)

Disconnect -> reconnect now reuses the DLL already in the game instead of
injecting again, including across a tool restart or crash: a connected DLL keeps
its per-pid shared section (and worker) alive after the host goes away, so a
fresh host can find it and re-attach to the same section.

- hook_dll_alive(pid) (host/src/inject/dll_probe.cpp): detect a live DLL by
  opening the per-pid section and polling its heartbeat (returns as soon as a
  beat lands; a missing section or stalled worker reads as not-alive). It does
  not check magic -- a graceful disconnect zeroes magic but the DLL keeps
  beating and the worker never re-checks magic post-connect.
- InjectionPanel: the Inject and Connect button branches to reconnect_selected()
  when a live DLL is detected -- IpcServer::start() re-attaches to the SAME
  section the DLL still holds and re-publishes the subsystem state; no
  re-injection. Factored the shared post-connect setup (publish_subsystem_state
  / begin_liveness_tracking). The DLL needed no change -- it just resumes reading
  the re-attached section.
- A false not-alive is benign: the inject path still re-attaches an
  already-injected DLL (LoadLibrary no-ops), so the timeout only needs to clear
  the worker's ~250ms beat period with margin.

Test (mock_game_test test_reconnect): inject -> hooked -> graceful disconnect ->
drop the host handle (simulating a restart while the DLL keeps the section alive)
-> detect via heartbeat -> re-attach to the same section -> hooks re-install
without re-injecting -> and hook_dll_alive goes false once the game is gone.

Roadmap: both current tasks (graceful disconnect, reconnect) done -> removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 14:05:54 +02:00
parent 6d96531ac7
commit 0eb275daca
8 changed files with 213 additions and 26 deletions

View File

@@ -0,0 +1,38 @@
#include "inject/dll_probe.hpp"
#include <cstdint>
#include <windows.h>
#include "coop/protocol.hpp"
#include "coop/shared_memory.hpp"
namespace coop
{
bool hook_dll_alive(unsigned long pid, int timeout_ms)
{
// The per-pid section exists only while someone holds it; a connected DLL keeps it alive across
// a host restart. Open it (don't create), then confirm the DLL's worker is actually beating --
// a stale section with a dead worker (heartbeat frozen) must read as not-alive so we inject fresh.
// Poll rather than sample once: the worker only beats ~every 250 ms, so a single short read can
// straddle a gap and miss it; return the instant a beat lands, and give up after the timeout.
SharedMemory shm;
if (!shm.open(shared_memory_name(pid), sizeof(SharedBlock)))
{
return false;
}
auto* block = shm.as<SharedBlock>();
const std::uint32_t h0 = block->status.heartbeat.load(std::memory_order_acquire);
for (int waited = 0; waited < timeout_ms; waited += 25)
{
Sleep(25);
if (block->status.heartbeat.load(std::memory_order_acquire) != h0)
{
return true;
}
}
return false;
}
} // namespace coop

View File

@@ -0,0 +1,18 @@
// Detect whether our hook DLL is already injected and alive in a target process, so the host can
// reconnect to it (reusing the DLL) instead of injecting again -- including after a tool restart or
// crash, since a connected DLL keeps the per-pid IPC section alive.
#pragma once
namespace coop
{
// True if `pid` already hosts a live coop_hook DLL: the per-pid IPC section exists and its heartbeat
// advances within `timeout_ms` (the DLL's worker is still beating). Returns as soon as a beat lands,
// so it's fast when the DLL is healthy; the timeout must clear the worker's beat period (~250 ms), so
// the default leaves margin. A missing section, or one whose heartbeat has stalled (dead worker),
// reads as not-alive -> the caller should inject fresh (which still re-attaches an already-injected
// DLL, so a false negative is benign). The default clears several beat periods for margin; a healthy
// DLL is detected as soon as the first beat lands, so this returns quickly in the normal case.
bool hook_dll_alive(unsigned long pid, int timeout_ms = 1000);
} // namespace coop