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

@@ -6,6 +6,7 @@
#include <windows.h>
#include "inject/dll_probe.hpp"
#include "inject/injector.hpp"
#include "ui/app_chrome.hpp"
@@ -217,6 +218,48 @@ void InjectionPanel::refresh_processes()
processes_ = list_processes();
}
void InjectionPanel::publish_subsystem_state()
{
// Publish the desired subsystem state before the hook's next reconcile tick.
server_.set_subsystem_enabled(HookSubsys_Input, want_input_);
server_.set_subsystem_enabled(HookSubsys_Focus, want_focus_);
server_.set_subsystem_enabled(HookSubsys_Audio, want_audio_);
server_.set_subsystem_enabled(HookSubsys_Video, want_video_);
server_.set_subsystem_enabled(HookSubsys_Mkb, want_mkb_);
server_.set_cursor_clip_allowed(!release_cursor_);
}
void InjectionPanel::begin_liveness_tracking()
{
injected_ = true;
// Track liveness: a SYNCHRONIZE|QUERY handle lets us notice the game exiting,
// and seeding the heartbeat clock avoids a spurious "hung" before the first beat.
close_target_handle(); // drop any handle from a previous target
target_process_ = OpenProcess(SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION, FALSE, selected_pid_);
target_state_ = TargetState::Alive;
last_heartbeat_ = 0;
last_heartbeat_time_ = ImGui::GetTime();
dll_alive_ = true;
}
void InjectionPanel::reconnect_selected()
{
// Re-attach to a DLL that's already injected and alive (a prior session left it dormant after a
// graceful disconnect, or the tool restarted): bring the channel back up on the SAME per-pid
// section the DLL still holds and re-publish the desired subsystem state -- no re-injection.
if (!server_.start(selected_pid_))
{
status_ = "Failed to re-attach shared memory.";
status_color_ = kRed;
return;
}
publish_subsystem_state();
begin_liveness_tracking();
status_ = "Reconnected to " + narrow(selected_name_) + " (pid " + std::to_string(selected_pid_) +
") -- reused the injected DLL.";
status_color_ = kGreen;
}
void InjectionPanel::inject_selected()
{
if (selected_pid_ == 0)
@@ -226,6 +269,15 @@ void InjectionPanel::inject_selected()
return;
}
// If our DLL is already injected and alive in this target (left dormant by a graceful disconnect,
// or surviving a tool restart -- it keeps the section alive), reconnect to it instead of
// injecting a second time.
if (hook_dll_alive(selected_pid_))
{
reconnect_selected();
return;
}
// Bring up the shared-memory channel before injecting so the hook finds it
// immediately on load.
if (!server_.start(selected_pid_))
@@ -235,26 +287,12 @@ void InjectionPanel::inject_selected()
return;
}
// Publish the desired subsystem state before the hook's first reconcile tick.
server_.set_subsystem_enabled(HookSubsys_Input, want_input_);
server_.set_subsystem_enabled(HookSubsys_Focus, want_focus_);
server_.set_subsystem_enabled(HookSubsys_Audio, want_audio_);
server_.set_subsystem_enabled(HookSubsys_Video, want_video_);
server_.set_subsystem_enabled(HookSubsys_Mkb, want_mkb_);
server_.set_cursor_clip_allowed(!release_cursor_);
publish_subsystem_state();
const InjectResult result = inject_dll(selected_pid_, hook_dll_path());
if (result.status == InjectStatus::Ok)
{
injected_ = true;
// Track liveness: a SYNCHRONIZE|QUERY handle lets us notice the game exiting,
// and seeding the heartbeat clock avoids a spurious "hung" before the first beat.
close_target_handle(); // drop any handle from a previous target
target_process_ = OpenProcess(SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION, FALSE, selected_pid_);
target_state_ = TargetState::Alive;
last_heartbeat_ = 0;
last_heartbeat_time_ = ImGui::GetTime();
dll_alive_ = true;
begin_liveness_tracking();
status_ = "Injected into " + narrow(selected_name_) + " (pid " + std::to_string(selected_pid_) + ").";
status_color_ = kGreen;
}