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>
This commit is contained in:
@@ -98,6 +98,10 @@ void InjectionPanel::refresh_targets()
|
||||
|
||||
InjectionPanel::~InjectionPanel()
|
||||
{
|
||||
// Graceful tool exit: unhook everything so a still-running game returns to normal (the DLL
|
||||
// stays injected, dormant). Short timeout -- the flags persist in the section the DLL keeps
|
||||
// alive, so the unhook completes even if the process exits before it confirms.
|
||||
disconnect_graceful(/*timeout_ms=*/300);
|
||||
if (vk_layer_enabled_)
|
||||
{
|
||||
unregister_vk_layer(); // don't leave the implicit layer registered after the tool closes
|
||||
@@ -105,6 +109,25 @@ InjectionPanel::~InjectionPanel()
|
||||
close_target_handle();
|
||||
}
|
||||
|
||||
void InjectionPanel::disconnect_graceful(int timeout_ms)
|
||||
{
|
||||
// Tell the DLL to remove every hook so the game runs as if it was never touched, then wait
|
||||
// (bounded) for it to confirm before we drop the channel. The flags persist in the section the
|
||||
// DLL keeps alive, so it unhooks even if we time out or exit first -- the wait just lets us
|
||||
// observe a clean game. The DLL is left injected (dormant) for a later reconnect; we never eject.
|
||||
if (server_.running())
|
||||
{
|
||||
server_.request_unhook_all();
|
||||
for (int waited = 0; waited < timeout_ms && !server_.all_hooks_removed(); waited += 10)
|
||||
{
|
||||
Sleep(10);
|
||||
}
|
||||
}
|
||||
server_.stop();
|
||||
injected_ = false;
|
||||
close_target_handle();
|
||||
}
|
||||
|
||||
void InjectionPanel::close_target_handle()
|
||||
{
|
||||
if (target_process_ != nullptr)
|
||||
@@ -557,10 +580,10 @@ void InjectionPanel::draw(bool debug_details)
|
||||
}
|
||||
if (ImGui::Button("Disconnect"))
|
||||
{
|
||||
server_.stop();
|
||||
injected_ = false;
|
||||
close_target_handle();
|
||||
status_ = "Stopped.";
|
||||
// Leave the game vanilla: unhook everything before dropping the channel. The DLL stays
|
||||
// injected (dormant), so it can be reconnected later without re-injecting.
|
||||
disconnect_graceful(/*timeout_ms=*/700);
|
||||
status_ = "Disconnected (game unhooked; DLL left injected).";
|
||||
status_color_ = kGrey;
|
||||
}
|
||||
// A relaunched game has a new pid; re-attach by image name without hunting for
|
||||
|
||||
@@ -165,6 +165,10 @@ private:
|
||||
void refresh_targets(); // refresh both the window list and the process list
|
||||
void refresh_processes();
|
||||
void inject_selected();
|
||||
// Graceful disconnect: ask the DLL to remove every hook (game returns to vanilla), wait
|
||||
// (bounded) for it to take effect, then drop the channel. The DLL stays injected/dormant for a
|
||||
// later reconnect; we never eject it. Used by the Disconnect button and the destructor.
|
||||
void disconnect_graceful(int timeout_ms);
|
||||
void reattach(); // re-inject a relaunched same-name target (Terminated state)
|
||||
void auto_reattach_tick(); // poll for the same game relaunching while auto-reattach is on
|
||||
void update_liveness(); // recompute target_state_ from process + heartbeat
|
||||
|
||||
@@ -127,6 +127,42 @@ void IpcServer::set_subsystem_enabled(std::uint32_t subsystem, bool enabled)
|
||||
}
|
||||
}
|
||||
|
||||
void IpcServer::request_unhook_all()
|
||||
{
|
||||
std::scoped_lock lock(mutex_);
|
||||
if (block_ == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
for (std::uint32_t s = 0; s < HookSubsys_Count; ++s)
|
||||
{
|
||||
block_->control.subsystem_disabled[s].store(1u, std::memory_order_release); // 1 = remove
|
||||
}
|
||||
}
|
||||
|
||||
bool IpcServer::all_hooks_removed() const
|
||||
{
|
||||
std::scoped_lock lock(mutex_);
|
||||
if (block_ == nullptr)
|
||||
{
|
||||
return true; // not connected -> nothing of ours is hooked
|
||||
}
|
||||
const HookStatus& s = block_->status;
|
||||
std::uint32_t count = s.hook_entry_count;
|
||||
if (count > kMaxHookEntries)
|
||||
{
|
||||
count = kMaxHookEntries;
|
||||
}
|
||||
for (std::uint32_t i = 0; i < count; ++i)
|
||||
{
|
||||
if (s.hook_entries[i].installed != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void IpcServer::host_log(std::uint32_t level, const char* text)
|
||||
{
|
||||
std::scoped_lock lock(mutex_);
|
||||
|
||||
@@ -81,6 +81,18 @@ public:
|
||||
// reconciles on its next tick. No-op if not started.
|
||||
void set_subsystem_enabled(std::uint32_t subsystem, bool enabled);
|
||||
|
||||
// Ask the injected DLL to remove EVERY subsystem, so the game runs exactly as if it was
|
||||
// never hooked (every hook's original bytes restored). The DLL stays loaded but dormant --
|
||||
// only the hooks are disabled -- ready for a later reconnect. Used by graceful Disconnect
|
||||
// and graceful tool exit. No-op if not started.
|
||||
void request_unhook_all();
|
||||
|
||||
// True once the DLL's hook registry reports nothing is currently installed (every subsystem
|
||||
// removed). Also true when not started or before the DLL has published a registry -- i.e.
|
||||
// "there is nothing hooked right now" -- so the host can confirm a graceful disconnect took
|
||||
// effect (poll this after request_unhook_all()).
|
||||
[[nodiscard]] bool all_hooks_removed() const;
|
||||
|
||||
// Enqueue a mouse/keyboard event for the hook's MKB subsystem to forward. Drops
|
||||
// silently if not started or the ring is full.
|
||||
void push_mkb(const MkbEvent& ev)
|
||||
|
||||
Reference in New Issue
Block a user