Add Re-attach for a relaunched same-name target
In the Terminated state the Injection panel now offers a Re-attach button: it refreshes the process list and finds live processes whose image name matches the original target's (case-insensitive). Exactly one match -> tear down the stale IPC channel and inject into the new pid via the normal path; several matches -> don't guess, filter the picker to the name and prompt the operator to pick one; none -> report it. Saves hunting for a relaunched game's new pid in the list. Verified: x64 build + ctest 7/7 green. Re-attach is a Terminated-state button flow, so its end-to-end behavior is best confirmed live (ImGui clicks can't be scripted); logic reviewed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -75,13 +75,6 @@ is removed from this list once done — so the top item is always next. The
|
||||
self-verifiable tooling / UI / input items come first; the game-pipeline items that
|
||||
need a real game (and Remote Play) to fully validate come last.
|
||||
|
||||
- **Re-attach to a relaunched target.** A killed-and-relaunched game gets a new pid,
|
||||
but the UI still holds the stale one. In the Terminated state, remember the
|
||||
target's image name (the panel already keeps the selected exe name) and offer a
|
||||
**Re-attach** button that injects only if a live process with that *same name*
|
||||
exists, rebinding the IPC server to the new pid via the existing `inject_dll` path.
|
||||
If several live processes share that name, don't guess — surface the matches in the
|
||||
window/process picker for a manual choice.
|
||||
- **Select targets by window, not just process.** A flat process list is fine as an
|
||||
advanced/debug view, but the default should be a **window list** — there are far
|
||||
fewer top-level windows than processes, and a window directly yields the HWND the
|
||||
|
||||
@@ -28,6 +28,24 @@ std::string narrow(const std::wstring& w)
|
||||
return out;
|
||||
}
|
||||
|
||||
// Case-insensitive equality of two image names (e.g. "snb.exe").
|
||||
bool iequals_name(const std::wstring& a, const std::wstring& b)
|
||||
{
|
||||
const std::string x = narrow(a), y = narrow(b);
|
||||
if (x.size() != y.size())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (std::size_t i = 0; i < x.size(); ++i)
|
||||
{
|
||||
if (::tolower(static_cast<unsigned char>(x[i])) != ::tolower(static_cast<unsigned char>(y[i])))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool contains_ci(const std::wstring& haystack, const char* needle_utf8)
|
||||
{
|
||||
if (needle_utf8 == nullptr || needle_utf8[0] == '\0')
|
||||
@@ -183,6 +201,50 @@ void InjectionPanel::inject_selected()
|
||||
}
|
||||
}
|
||||
|
||||
void InjectionPanel::reattach()
|
||||
{
|
||||
if (selected_name_.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Find live processes that share the original target's image name.
|
||||
refresh_processes();
|
||||
std::vector<unsigned long> matches;
|
||||
for (const ProcessEntry& e : processes_)
|
||||
{
|
||||
if (iequals_name(e.exe_name, selected_name_))
|
||||
{
|
||||
matches.push_back(e.pid);
|
||||
}
|
||||
}
|
||||
|
||||
const std::string name = narrow(selected_name_);
|
||||
if (matches.empty())
|
||||
{
|
||||
status_ = "No running \"" + name + "\" to re-attach to.";
|
||||
status_color_ = kRed;
|
||||
return;
|
||||
}
|
||||
if (matches.size() > 1)
|
||||
{
|
||||
// Don't guess which instance: filter the picker to the matches so the operator
|
||||
// chooses, then injects via the normal button.
|
||||
snprintf(filter_, sizeof(filter_), "%s", name.c_str());
|
||||
selected_pid_ = 0;
|
||||
status_ = "Multiple \"" + name + "\" running -- pick one below, then Inject & Connect.";
|
||||
status_color_ = kGrey;
|
||||
return;
|
||||
}
|
||||
|
||||
// Exactly one: tear down the stale channel and inject into the new pid.
|
||||
server_.stop();
|
||||
close_target_handle();
|
||||
injected_ = false;
|
||||
selected_pid_ = matches.front();
|
||||
inject_selected();
|
||||
}
|
||||
|
||||
void InjectionPanel::publish(const std::array<PadInfo, kMaxPads>& pads)
|
||||
{
|
||||
if (!test_input_)
|
||||
@@ -430,6 +492,18 @@ void InjectionPanel::draw(bool debug_details)
|
||||
status_ = "Stopped.";
|
||||
status_color_ = kGrey;
|
||||
}
|
||||
// A relaunched game has a new pid; re-attach by image name without hunting for
|
||||
// it in the list. Only offered once the old target is gone.
|
||||
if (target_state_ == TargetState::Terminated && !selected_name_.empty())
|
||||
{
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Re-attach"))
|
||||
{
|
||||
reattach();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("(relaunched %s)", narrow(selected_name_).c_str());
|
||||
}
|
||||
ImGui::Separator();
|
||||
}
|
||||
|
||||
|
||||
@@ -108,6 +108,7 @@ public:
|
||||
private:
|
||||
void refresh_processes();
|
||||
void inject_selected();
|
||||
void reattach(); // re-inject a relaunched same-name target (Terminated state)
|
||||
void update_liveness(); // recompute target_state_ from process + heartbeat
|
||||
void close_target_handle(); // close target_process_ and reset liveness state
|
||||
void draw_subsystem_controls(const HookStatusView& status);
|
||||
|
||||
Reference in New Issue
Block a user