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

@@ -110,14 +110,6 @@ default** and covers anything the hooked path doesn't.
Completed work lives in **Lessons learned** + the test suite, not here.
### Current tasks
- **Reconnect to an already-injected DLL.** Support disconnect → reconnect reusing the DLL that's
already in the game, *including across a tool restart or crash*: the host detects the live DLL
(via its advancing IPC heartbeat on the per-pid section), re-attaches to the same shared section,
and resumes control without re-injecting. (DLL self-cleanup on host *crash* is explicitly **not**
required — relaunch, reconnect, then disconnect gracefully to clean up.)
### Future work
- **Per-game profiles** — persist each game's subsystem / capture-mode / audio choices and re-apply
@@ -397,6 +389,14 @@ person/account to receive the stream.
sees the mirrored video, hears the audio, and that their controller drives the
real game.
**Disconnect / reconnect.** **Disconnect** asks the injected DLL to remove every hook so the game
behaves exactly as if it was never touched, then drops the channel — but leaves the DLL injected
(dormant). Clicking **Inject & Connect** on a game that still has a live DLL (left dormant, or
surviving a tool restart/crash — a connected DLL keeps its shared section alive) **reconnects** to it
and resumes, without injecting again. Closing the host also unhooks the game on the way out. So a
clean cycle is: connect → play → disconnect (game back to normal, DLL parked) → reconnect later. The
DLL is never force-unloaded; it goes away when the game exits.
Useful checks while developing without RPT: tick **Forward synthetic test input**
in the Injection panel to make the game move on its own (proving forwarding is the
source), and click away from the game to confirm focus spoofing keeps it running.