From b8f96fc411fc074f1f00a9e8235e62508341e5fc Mon Sep 17 00:00:00 2001 From: Benjamin Palko Date: Wed, 14 Oct 2026 16:58:56 -0400 Subject: [PATCH] implement agent_release --- .gitignore | 3 + AGENTS.md | 1 + src/README.md | 52 ++++++++++++++ src/agent_release/README.md | 65 +++++++++++++++++ src/agent_release/agent.cpp | 125 ++++++++++++++++++++++++++++++++ src/agent_release/agent.hpp | 82 +++++++++++++++++++++ src/agent_release/main.cpp | 70 ++++++++++++++++++ src/agent_release/meson.build | 6 ++ src/agent_release/platform.cpp | 126 +++++++++++++++++++++++++++++++++ src/agent_release/platform.hpp | 39 ++++++++++ src/meson.build | 7 ++ 11 files changed, 576 insertions(+) create mode 100644 src/README.md create mode 100644 src/agent_release/README.md create mode 100644 src/agent_release/agent.cpp create mode 100644 src/agent_release/agent.hpp create mode 100644 src/agent_release/main.cpp create mode 100644 src/agent_release/meson.build create mode 100644 src/agent_release/platform.cpp create mode 100644 src/agent_release/platform.hpp create mode 100644 src/meson.build diff --git a/.gitignore b/.gitignore index bc803a8c..4e7b52b5 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ ghidra # Licence-restricted vendor material, local reference only (see third-party/README.md) third-party/_local/ + +# Meson build output +src/build/ diff --git a/AGENTS.md b/AGENTS.md index 7fc59ff2..b2a3cccc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,6 +36,7 @@ code. | `schema/` | SQL Server database schema/log files (`.mdf` / `.ldf`) for `RustyHearts`, `RustyHearts_Account`, `RustyHearts_Auth`, `RustyHearts_Log`, `GMRustyHearts` | **Read-only.** Never modify. Gitignored. | | `decomp/` | Raw decompilation objects exported from Ghidra/REA (pseudocode, listings, function metadata) | Derived output. Regenerable; created on demand. | | `.agents/skills/` | Installed agent skills, notably `reverse-engineer-anything` (REA) | Agent configuration. | +| `src/` | Linux C++ reconstructions of analysed binaries (Meson; one sub-folder per binary, e.g. `agent_release/`). Build: `cd src && meson setup build && ninja -C build` | Hand-written; cite decomp addresses; `src/build/` is gitignored. | | `docs/` | Project documentation, e.g. `ARCHITECTURE.md` (language/toolchain, libraries, code conventions) | Hand-maintained; update as findings are confirmed. | | `third-party/` | Upstream sources/headers matched to the shipped support DLLs (OpenSSL 1.0.0d, curl 7.21.4, BugTrap ~1.3, mingw-w64 `dbghelp.h`, TinyXML 2.5.x) | Reference only, not build inputs. Do not edit upstream files; record provenance changes in the doc below. | diff --git a/src/README.md b/src/README.md new file mode 100644 index 00000000..19fd4c27 --- /dev/null +++ b/src/README.md @@ -0,0 +1,52 @@ +# src — Linux reconstructions + +Buildable Linux C++ ports of analysed Rusty Hearts server binaries. Each +sub-folder ports one binary and has its own README. That README covers +provenance, the Win32 → Linux substitutions, and what is not yet ported. + +| Folder | Original binary | Output | +|---|---|---| +| `agent_release/` | `Agent_Release_x64.exe` | `build/agent_release/agent_release` | + +These are reconstructions from decompiled code, not original source. + +## Requirements + +- Linux with glibc ≥ 2.29 (for `posix_spawn_file_actions_addchdir_np`) +- A C++17 compiler (tested with GCC 16.2) +- Meson ≥ 1.0 and Ninja. Run both from this repo's `mise` environment or the + system packages. + +## Build + +From this directory: + +```sh +meson setup build # first time only +ninja -C build +``` + +## Common tasks + +- **Rebuild after changes:** `ninja -C build`. +- **Release build:** `meson setup build-release --buildtype=release && ninja -C build-release`. +- **Clean rebuild:** `rm -rf build && meson setup build && ninja -C build`. + +## Run + +```sh +./build/agent_release/agent_release --help +./build/agent_release/agent_release --auto-on 1 \ + --type 1:/path/to/server:/path/to/workdir --server 1:1:1 +``` + +Stop it with Ctrl-C or SIGTERM. See `agent_release/README.md` for every +option. + +`build/` is gitignored. + +## Adding another binary + +1. Create `/` with its sources, a `meson.build` and a `README.md`. The + README must cite decomp addresses and list substitutions and gaps. +2. Add `subdir('')` to `meson.build` in this directory. diff --git a/src/agent_release/README.md b/src/agent_release/README.md new file mode 100644 index 00000000..8b1b2e30 --- /dev/null +++ b/src/agent_release/README.md @@ -0,0 +1,65 @@ +# agent_release — Linux port of `Agent_Release_x64.exe` (process-supervisor core) + +A buildable Linux reconstruction of the parts of the Agent that have been +decompiled. It is **not** the original program: names are inferred (there is no +PDB), and only the analysed functions are ported. + +Source analysis: +- `decomp/Agent_Release_x64/evidence.md` +- `decomp/Agent_Release_x64/cpp/AgentProcessControl.cpp` + +## Build and run + +```sh +cd src +meson setup build +ninja -C build +./build/agent_release/agent_release --auto-on 1 \ + --type 1:/path/to/server:/path/to/workdir --server 1:1:1 +``` + +`--type TYPE:EXE[:WORKDIR]` maps a server type to its executable. +`--server CODE:TYPE:REVIVE` registers one managed server. By default the +server's code is passed as its argument, so the command line is `EXE CODE`. + +## Win32 → Linux substitutions + +| Original (address) | Win32 | Linux replacement | Behaviour difference | +|---|---|---|---| +| Single instance (`0x1400100b0`) | `CreateMutexW("Global\\RustyHearts_Agent")` | `flock(LOCK_EX\|LOCK_NB)` on `/tmp/RustyHearts_Agent.lock` | Lock file instead of a kernel object; still released on exit/crash | +| Launch (`0x1400094e0`) | `CreateProcessW(…, CREATE_NEW_CONSOLE, …, workDir)` + `STARTUPINFOW` console geometry | `posix_spawnp` + `POSIX_SPAWN_SETSID` + `addchdir_np` | No console window; command line split on whitespace (no Windows quoting rules) | +| Watcher (`0x140008c90`) | `GetExitCodeProcess` polled every 10 ms | blocking `waitpid` on a detached `std::thread` | No polling; child reaped (fixes original handle leak); PID passed by value (fixes original stack-pointer race) | +| Kill (`0x140009780`) | Toolhelp32 snapshot + `OpenProcess` + `TerminateProcess(h,0)` | `/proc/*/cmdline` argv[0] basename, `strcasecmp`, `kill(SIGKILL)` | Same: first match only, case-insensitive; no snapshot leak | +| Main loop timer (`0x1400037a0`) | `QueryPerformanceCounter` (`TimeSourceHRWin32`) | `std::chrono::steady_clock` | — | +| Thread start | `_beginthreadex` | `std::thread` | — | +| `Sleep` | `Sleep` | `std::this_thread::sleep_for` | — | +| Shutdown flag | console menu (`FUN_140001dc0`, not analysed) | `SIGINT` / `SIGTERM` | Different trigger | + +Kept from the original: +- the 1-second sleep after each launch attempt; +- the 260-character (`MAX_PATH`) command-line limit; +- the log strings. + +## Not ported (unknown or proprietary) + +- **ProudNet networking / RMI to AgentManager and game servers.** ProudNet is + proprietary and the protocol is undecoded. `IManagerSession` is a logging + stub at the points where the original sends RMI messages (proxy vtable + slots `+0x40` and `+0x50`). +- **XML config (`option\*.xml`, TinyXML).** Field mapping is not yet recovered, + so command-line flags stand in for it. +- **Launch-argument builder `FUN_140027540`.** Its output is unknown, so only + the "server code as argument" mode is implemented. +- **Exit follow-ups.** + - The `type == 1` exit follow-up (`FUN_140009f20`) is not ported. + - The identity of the type `0x18` process killed at startup is unknown. + `--kill-on-start` stands in for it. +- **Locking.** The original's locking around the server table is not visible + in the analysed code. This port uses a recursive mutex. + +## Verified (2026-10-14, Linux) + +Smoke test, using `sleep` as the managed server: auto-start after the delay, +revive on exit, second-instance refusal, case-insensitive kill by name, +clean SIGTERM shutdown. This tests the port only. It says nothing about the +original binary's runtime behaviour. diff --git a/src/agent_release/agent.cpp b/src/agent_release/agent.cpp new file mode 100644 index 00000000..8a394863 --- /dev/null +++ b/src/agent_release/agent.cpp @@ -0,0 +1,125 @@ +// agent.cpp — logic follows the Ghidra pseudocode cited per function. +#include "agent.hpp" +#include "platform.hpp" + +#include +#include + +namespace agent { + +void LoggingManagerSession::NotifyServerState(const ServerEntry& e) +{ + std::printf("[RMI->AgentManager] ServerState code=%d pid=%d running=%d\n", + e.code, static_cast(e.pid), e.running ? 1 : 0); +} + +void LoggingManagerSession::NotifyServerStopped(const ServerEntry& e) +{ + std::printf("[RMI->AgentManager] ServerStopped code=%d\n", e.code); +} + +void LoggingManagerSession::SendLog(const std::string& text) +{ + std::printf("[RMI->AgentManager] Log: %s\n", text.c_str()); +} + +Agent::Agent(Options opts, std::map types, + std::unique_ptr session) + : opts_(std::move(opts)), types_(std::move(types)), session_(std::move(session)) {} + +void Agent::AddServer(const ServerEntry& e) +{ + std::lock_guard g(mu_); + servers_[e.code] = e; +} + +std::string Agent::BuildCommandLine(const ServerEntry& e) const +{ + auto t = types_.find(e.type); + std::string cmd = (t == types_.end() ? std::string() : t->second.exePath) + " "; + if (opts_.useCodeAsArgument) + cmd += std::to_string(e.code); + // else: original appends FUN_140027540's output — contents UNKNOWN, not ported. + return cmd; +} + +// 0x140008ea0 +void Agent::ServerOn(int32_t code) +{ + std::lock_guard g(mu_); + auto it = servers_.find(code); + if (it == servers_.end()) { + std::printf("Error - NOT EXIST SERVER : %d \n", code); + return; + } + ServerEntry& e = it->second; + if (e.running) return; + e.running = true; + + std::string cmd = BuildCommandLine(e); + auto t = types_.find(e.type); + std::string workDir = t == types_.end() ? std::string() : t->second.workDir; + + pid_t pid = 0; + if (cmd.size() < 260 && (pid = platform::LaunchProcess(cmd, workDir)) > 0) { // MAX_PATH check kept + e.pid = pid; + e.running = true; + session_->NotifyServerState(e); + platform::WatchProcess(pid, [this](pid_t p) { OnProcessExit(p); }); + } else { + e.running = false; + session_->SendLog("FAILED SERVER ON " + opts_.agentName + " "); + } +} + +void Agent::AllServerOn() +{ + std::lock_guard g(mu_); + std::printf("All Server On. \n"); + for (auto& kv : servers_) ServerOn(kv.first); +} + +// 0x140008c90 (after the wait loop) +void Agent::OnProcessExit(pid_t pid) +{ + std::lock_guard g(mu_); + ServerEntry* e = nullptr; + for (auto& kv : servers_) + if (kv.second.pid == pid) { e = &kv.second; break; } + if (!e) { std::printf("NOT FIND PROCESS ID \n"); return; } + + e->running = false; + session_->NotifyServerState(*e); + session_->NotifyServerStopped(*e); + if (running_ && (e->revive || opts_.reviveAll)) + ServerOn(e->code); + // type==1 follow-up (FUN_140009f20) — purpose UNKNOWN, not ported. +} + +// 0x1400037a0 MagiAgentServer::Run — network setup omitted (ProudNet). +int Agent::Run() +{ + using clock = std::chrono::steady_clock; + std::printf("# Agent Startup #\n\n+ Prepare Startup\n"); + std::printf("\n # Agent Running... #\n"); + if (!opts_.killOnStart.empty()) platform::KillProcessByName(opts_.killOnStart); + + const auto t0 = clock::now(); + auto seconds = [&] { return std::chrono::duration(clock::now() - t0).count(); }; + bool autoOnPending = true; + double lastTick = 0; + while (running_) { + double now = seconds(); + if (opts_.autoServerOnDelay > 0 && now > opts_.autoServerOnDelay && autoOnPending) { + autoOnPending = false; + std::printf("Start Auto Server On. \n"); + AllServerOn(); + } + if (now - lastTick >= 0.1) lastTick = now; // 10 Hz tick: network update (not ported) + platform::SleepMs(1); + } + std::printf("+ Prepare Stop\n# Agent Stoped... #\n"); + return 0; +} + +} // namespace agent diff --git a/src/agent_release/agent.hpp b/src/agent_release/agent.hpp new file mode 100644 index 00000000..d359b7b5 --- /dev/null +++ b/src/agent_release/agent.hpp @@ -0,0 +1,82 @@ +// agent.hpp — Linux port of the process-supervisor core of Agent_Release_x64.exe. +// Reconstructed from decomp/Agent_Release_x64/cpp/AgentProcessControl.cpp. +// Names are inferred (no PDB). See README.md for what is and is not ported. +#pragma once +#include +#include +#include +#include +#include +#include +#include + +namespace agent { + +// Per-server record (original: hash map at 0x140162d60, ServerEntry offsets +// +0x04 code, +0x0c type, +0xa4 pid, +0xc4 running, +0xd5 revive). +struct ServerEntry { + int32_t code = 0; + uint8_t type = 0; + pid_t pid = 0; + bool running = false; + bool revive = false; +}; + +// Server-type table (original: vector at 0x140162d58, type -> exe path). +struct ServerTypeInfo { + std::string exePath; // may include fixed arguments + std::string workDir; // original derives it from the exe path (FUN_14002fe90) +}; + +// Agent options (original: singleton at 0x140162d68). +struct Options { + std::string agentName = "AGENT"; // +0x030 + bool useCodeAsArgument = true; // +0x152 + bool reviveAll = false; // +0x171 + double autoServerOnDelay = 0.0; // +0x178, seconds; <= 0 disables + std::string killOnStart; // exe of server type 0x18 (identity UNKNOWN) +}; + +// Stand-in for the ProudNet RMI proxy to AgentManager +// (AGENT2AGENTMANAGER_PROTOCOL::Proxy, vtable slots +0x40 / +0x50). +// ProudNet is proprietary and the wire protocol is not yet decoded, so this +// interface only logs. Replace with a real transport once the protocol is known. +class IManagerSession { +public: + virtual ~IManagerSession() = default; + virtual void NotifyServerState(const ServerEntry& e) = 0; // slot +0x40 + virtual void NotifyServerStopped(const ServerEntry& e) = 0; // slot +0x50 + virtual void SendLog(const std::string& text) = 0; +}; + +class LoggingManagerSession : public IManagerSession { +public: + void NotifyServerState(const ServerEntry& e) override; + void NotifyServerStopped(const ServerEntry& e) override; + void SendLog(const std::string& text) override; +}; + +class Agent { +public: + Agent(Options opts, std::map types, + std::unique_ptr session); + + void AddServer(const ServerEntry& e); + void ServerOn(int32_t code); // 0x140008ea0 + void AllServerOn(); // 0x140009bd0 (inferred: ServerOn for every entry) + int Run(); // 0x1400037a0 MagiAgentServer::Run + void Stop() { running_ = false; } + +private: + void OnProcessExit(pid_t pid); // body of watcher thread 0x140008c90 + std::string BuildCommandLine(const ServerEntry& e) const; + + Options opts_; + std::map types_; + std::unique_ptr session_; + std::map servers_; + std::recursive_mutex mu_; // original has no visible lock here (UNKNOWN) + std::atomic running_{true}; +}; + +} // namespace agent diff --git a/src/agent_release/main.cpp b/src/agent_release/main.cpp new file mode 100644 index 00000000..b7797d89 --- /dev/null +++ b/src/agent_release/main.cpp @@ -0,0 +1,70 @@ +// main.cpp — entry point. The original reads option\*.xml (format not yet +// recovered); this port takes the equivalent settings on the command line. +#include "agent.hpp" +#include "platform.hpp" + +#include +#include +#include +#include + +static agent::Agent* g_agent = nullptr; +static void OnSignal(int) { if (g_agent) g_agent->Stop(); } + +static void Usage() +{ + std::printf( + "usage: agent_release [options] --server CODE:TYPE:REVIVE --type TYPE:EXE[:WORKDIR] ...\n" + " --auto-on SEC start all servers SEC seconds after startup\n" + " --revive-all relaunch any server that exits\n" + " --no-code-arg do not pass the server code as argv[1]\n" + " --kill-on-start EXE kill the first process named EXE before running\n" + " --name NAME agent name used in log messages\n"); +} + +int main(int argc, char** argv) +{ + agent::Options opts; + std::map types; + std::vector servers; + + for (int i = 1; i < argc; ++i) { + std::string a = argv[i]; + auto next = [&]() -> std::string { + if (i + 1 >= argc) { Usage(); std::exit(2); } + return argv[++i]; + }; + if (a == "--auto-on") opts.autoServerOnDelay = std::stod(next()); + else if (a == "--revive-all") opts.reviveAll = true; + else if (a == "--no-code-arg") opts.useCodeAsArgument = false; + else if (a == "--kill-on-start") opts.killOnStart = next(); + else if (a == "--name") opts.agentName = next(); + else if (a == "--type") { + std::string v = next(); + auto p1 = v.find(':'); + if (p1 == std::string::npos) { Usage(); return 2; } + auto p2 = v.find(':', p1 + 1); + agent::ServerTypeInfo t; + t.exePath = v.substr(p1 + 1, p2 == std::string::npos ? std::string::npos : p2 - p1 - 1); + if (p2 != std::string::npos) t.workDir = v.substr(p2 + 1); + types[static_cast(std::stoi(v.substr(0, p1)))] = t; + } else if (a == "--server") { + std::string v = next(); + agent::ServerEntry e; + int code = 0, type = 0, revive = 0; + if (std::sscanf(v.c_str(), "%d:%d:%d", &code, &type, &revive) < 2) { Usage(); return 2; } + e.code = code; e.type = static_cast(type); e.revive = revive != 0; + servers.push_back(e); + } else { Usage(); return a == "-h" || a == "--help" ? 0 : 2; } + } + + platform::SingleInstanceLock lock("RustyHearts_Agent"); // Global\RustyHearts_Agent + if (!lock.acquired()) return 1; + + agent::Agent ag(opts, types, std::make_unique()); + for (auto& e : servers) ag.AddServer(e); + g_agent = &ag; + std::signal(SIGINT, OnSignal); + std::signal(SIGTERM, OnSignal); + return ag.Run(); +} diff --git a/src/agent_release/meson.build b/src/agent_release/meson.build new file mode 100644 index 00000000..11ec7d75 --- /dev/null +++ b/src/agent_release/meson.build @@ -0,0 +1,6 @@ +threads = dependency('threads') + +executable('agent_release', + ['main.cpp', 'agent.cpp', 'platform.cpp'], + dependencies : threads, + install : false) diff --git a/src/agent_release/platform.cpp b/src/agent_release/platform.cpp new file mode 100644 index 00000000..f34e71b3 --- /dev/null +++ b/src/agent_release/platform.cpp @@ -0,0 +1,126 @@ +// platform.cpp — Linux implementations; Win32 originals cited per function. +#include "platform.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +extern char** environ; + +namespace platform { + +SingleInstanceLock::SingleInstanceLock(const std::string& name) +{ + // "Global\\" kernel-object namespace -> machine-wide lock file in /tmp. + std::string path = "/tmp/" + name + ".lock"; + fd_ = ::open(path.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0644); + if (fd_ < 0) { + std::printf("ERROR_ACCESS_DENIED!!! %s \n", path.c_str()); + } else if (::flock(fd_, LOCK_EX | LOCK_NB) != 0) { + std::printf("ERROR_ALREADY_EXISTS!!! %s \n", path.c_str()); + ::close(fd_); + fd_ = -1; + } + if (fd_ < 0) SleepMs(3000); // original sleeps 3 s before failing +} + +SingleInstanceLock::~SingleInstanceLock() +{ + if (fd_ >= 0) ::close(fd_); +} + +pid_t LaunchProcess(const std::string& commandLine, const std::string& workDir) +{ + std::istringstream in(commandLine); + std::vector args; + for (std::string a; in >> a;) args.push_back(a); + if (args.empty()) return 0; + std::vector argv; + for (auto& a : args) argv.push_back(a.data()); + argv.push_back(nullptr); + + posix_spawn_file_actions_t fa; + posix_spawn_file_actions_init(&fa); + if (!workDir.empty()) posix_spawn_file_actions_addchdir_np(&fa, workDir.c_str()); + posix_spawnattr_t attr; + posix_spawnattr_init(&attr); + posix_spawnattr_setflags(&attr, POSIX_SPAWN_SETSID); // ~ CREATE_NEW_CONSOLE + + pid_t pid = 0; + int rc = posix_spawnp(&pid, argv[0], &fa, &attr, argv.data(), environ); + posix_spawnattr_destroy(&attr); + posix_spawn_file_actions_destroy(&fa); + + if (rc != 0) { + std::printf("=======Server Fail=======\n"); + std::printf("Path\t: [%s] \n", workDir.c_str()); + std::printf("File\t: [%s] (%s)\n", commandLine.c_str(), std::strerror(rc)); + pid = 0; + } else { + std::printf("=======ServerOn=======\n"); + } + std::printf("//////////////////////////////////////////////////////////////////////////\n"); + SleepMs(1000); // kept: original sleeps 1 s after every launch attempt + return pid; +} + +void WatchProcess(pid_t pid, std::function onExit) +{ + // pid passed by value: avoids the original's stack-pointer race. + std::thread([pid, onExit = std::move(onExit)] { + int status = 0; + while (::waitpid(pid, &status, 0) < 0 && errno == EINTR) {} + onExit(pid); // waitpid reaps the child: no handle/zombie leak + }).detach(); +} + +static std::string ProcArgv0Basename(const char* pidDir) +{ + std::ifstream f(std::string("/proc/") + pidDir + "/cmdline", std::ios::binary); + std::string argv0; + std::getline(f, argv0, '\0'); + auto slash = argv0.find_last_of('/'); + return slash == std::string::npos ? argv0 : argv0.substr(slash + 1); +} + +bool KillProcessByName(const std::string& exeName) +{ + if (exeName.empty()) return false; + DIR* d = ::opendir("/proc"); + if (!d) return false; + bool killed = false; + while (dirent* e = ::readdir(d)) { + char* end = nullptr; + long pid = std::strtol(e->d_name, &end, 10); + if (*end != '\0' || pid <= 0 || pid == ::getpid()) continue; + if (::strcasecmp(ProcArgv0Basename(e->d_name).c_str(), exeName.c_str()) != 0) continue; + if (::kill(static_cast(pid), SIGKILL) == 0) { + std::printf("Process Kill.!!! [%s] \n", exeName.c_str()); + killed = true; + break; // original stops at the first match + } + } + ::closedir(d); + return killed; +} + +void SleepMs(unsigned ms) +{ + std::this_thread::sleep_for(std::chrono::milliseconds(ms)); +} + +} // namespace platform diff --git a/src/agent_release/platform.hpp b/src/agent_release/platform.hpp new file mode 100644 index 00000000..891a8718 --- /dev/null +++ b/src/agent_release/platform.hpp @@ -0,0 +1,39 @@ +// platform.hpp — Linux substitutes for the Win32 calls used by +// Agent_Release_x64.exe (see decomp/Agent_Release_x64/cpp/AgentProcessControl.cpp). +#pragma once +#include +#include +#include + +namespace platform { + +// Replaces CreateMutexW(L"Global\\RustyHearts_Agent") (0x1400100b0). +// Uses an exclusive flock() on a lock file; released automatically on exit. +class SingleInstanceLock { +public: + explicit SingleInstanceLock(const std::string& name); + ~SingleInstanceLock(); + bool acquired() const { return fd_ >= 0; } +private: + int fd_ = -1; +}; + +// Replaces CreateProcessW(..., CREATE_NEW_CONSOLE, ..., workDir) (0x1400094e0). +// commandLine is split on whitespace (the original passes one string to Windows). +// The child gets its own session (setsid) as the nearest analogue of a new console. +// Returns pid > 0, or 0 on failure (same contract as the original). +pid_t LaunchProcess(const std::string& commandLine, const std::string& workDir); + +// Replaces the watcher's GetExitCodeProcess polling loop (0x140008c90) with a +// blocking waitpid(). Calls onExit(pid) on a detached thread once the child exits. +void WatchProcess(pid_t pid, std::function onExit); + +// Replaces Toolhelp32 + OpenProcess + TerminateProcess (0x140009780). +// Scans /proc, matches basename of argv[0] case-insensitively, kills the FIRST +// match with SIGKILL (TerminateProcess is unconditional), returns true if killed. +bool KillProcessByName(const std::string& exeName); + +// Replaces Sleep(ms). +void SleepMs(unsigned ms); + +} // namespace platform diff --git a/src/meson.build b/src/meson.build new file mode 100644 index 00000000..bced57da --- /dev/null +++ b/src/meson.build @@ -0,0 +1,7 @@ +# Linux reconstructions of the Rusty Hearts server binaries. +# Each sub-folder ports one binary; see its README.md for provenance and gaps. +project('rusty-hearts-recon', 'cpp', + version : '0.1.0', + default_options : ['cpp_std=c++17', 'warning_level=3', 'buildtype=debugoptimized']) + +subdir('agent_release')