remove src recomp
This commit is contained in:
parent
af36074fed
commit
31ff800b2a
19 changed files with 1398 additions and 644 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -8,5 +8,3 @@ ghidra
|
|||
# Licence-restricted vendor material, local reference only (see third-party/README.md)
|
||||
third-party/_local/
|
||||
|
||||
# Meson build output
|
||||
src/build/
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ 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. |
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ per class (e.g. `MagiAgentNetwork`, `MGAgentManagerSession`) indicate multiple
|
|||
inheritance. Statically linked C++ libraries: ProudNet, TinyXML (`TiXml*`), ATL.
|
||||
|
||||
## Outputs
|
||||
|
||||
Porting spec (Win32 → Linux substitutions, behaviour to reproduce, original bugs): `porting.md`.
|
||||
| Path | Content |
|
||||
|---|---|
|
||||
| `full/functions/<addr>.c` | Ghidra pseudocode for all 6,423 non-thunk functions (1 failed; see `full/index.tsv`). Includes library code (ProudNet, TinyXML, CRT) — not yet labelled. |
|
||||
|
|
|
|||
55
decomp/Agent_Release_x64/porting.md
Normal file
55
decomp/Agent_Release_x64/porting.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# Agent_Release_x64.exe — porting spec (Win32 → Linux)
|
||||
|
||||
This was kept from the removed Linux reconstruction `src/agent_release/`
|
||||
(removed 2026-10-15). It records the facts about the original binary that a
|
||||
re-implementation needs, and the nearest Linux equivalent for each Win32 call.
|
||||
|
||||
Sources: `evidence.md`, `cpp/AgentProcessControl.cpp`. Names are inferred;
|
||||
the behaviour is static-analysis evidence only.
|
||||
|
||||
## Behaviour to reproduce
|
||||
|
||||
The servers being managed are read from config (`option\*.xml`, mapping not
|
||||
yet recovered).
|
||||
|
||||
| Item | Behaviour |
|
||||
|---|---|
|
||||
| Single instance (`0x1400100b0`) | Named mutex `Global\RustyHearts_Agent` |
|
||||
| Launch (`0x1400094e0`) | `CreateProcessW(NULL, "<exe> <arg>", …, CREATE_NEW_CONSOLE, …, workDir)`, console 700×110, fill attribute 0x17. Command line is limited to 260 characters (`MAX_PATH`). Returns the pid, or 0 on failure (logs `=======Server Fail=======`) |
|
||||
| After each launch attempt | `Sleep(1000)` |
|
||||
| Command line (`0x140008ea0`) | `<exe for server type> <code>`. An alternative argument builder `FUN_140027540` exists, but its output is unknown |
|
||||
| Watcher (`0x140008c90`) | Polls `GetExitCodeProcess` every 10 ms. On exit it sends two RMIs to AgentManager, then relaunches if the per-server revive flag (+0xd5) or the global flag (+0x171) is set |
|
||||
| Kill (`0x140009780`) | Toolhelp32 snapshot, case-insensitive image-name match, terminates the **first** match only (`TerminateProcess(h,0)`) |
|
||||
| Main loop (`0x1400037a0`) | 100 ms tick, 1 s FPS counter. Optionally auto-starts servers after a configured delay (+0x178). At startup it kills a stale process of type 0x18 (identity unknown) |
|
||||
|
||||
## 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 or crash |
|
||||
| Launch (`0x1400094e0`) | `CreateProcessW(…, CREATE_NEW_CONSOLE, …, workDir)` + `STARTUPINFOW` console geometry | `posix_spawnp` + `POSIX_SPAWN_SETSID` + `posix_spawn_file_actions_addchdir_np` (glibc ≥ 2.29) | No console window; the command line is split on whitespace (no Windows quoting rules) |
|
||||
| Watcher (`0x140008c90`) | `GetExitCodeProcess` polled every 10 ms | blocking `waitpid` on a detached thread | No polling. Reaping fixes the original handle leak; passing the pid by value fixes the original stack-pointer race |
|
||||
| Kill (`0x140009780`) | Toolhelp32 + `OpenProcess` + `TerminateProcess(h,0)` | scan `/proc/*/cmdline` for the argv[0] basename with `strcasecmp`, then `kill(SIGKILL)` | Same rule: first match only, case-insensitive |
|
||||
| Main-loop timer | `QueryPerformanceCounter` (`TimeSourceHRWin32`) | `std::chrono::steady_clock` | — |
|
||||
| Thread start | `_beginthreadex` | `std::thread` | — |
|
||||
| `Sleep` | `Sleep` | `std::this_thread::sleep_for` | — |
|
||||
| Shutdown | console menu (`FUN_140001dc0`, not analysed) | `SIGINT` / `SIGTERM` | Different trigger |
|
||||
|
||||
## Original bugs (O)
|
||||
|
||||
- **Process handle leak:** the watcher never closes the process handle.
|
||||
- **Stack-pointer race:** `LaunchProcess` hands the watcher a pointer to a
|
||||
stack `PROCESS_INFORMATION`. It is only safe because of the
|
||||
`Sleep(1000)` that follows.
|
||||
- **Snapshot handle leak:** `KillProcessByName` leaks the snapshot handle on
|
||||
the success path.
|
||||
|
||||
## Unknown / not recovered
|
||||
|
||||
- **ProudNet RMI to AgentManager and the game servers:** proprietary
|
||||
transport. Wire-incompatible with ProudNet 1.8 (see the ProudNet section of
|
||||
`third-party/README.md`). The proxy vtable slots used are `+0x40` and `+0x50`.
|
||||
- **XML config field mapping** for the Agent.
|
||||
- **`FUN_140027540` argument builder; the `type == 1` exit follow-up
|
||||
(`FUN_140009f20`); the type 0x18 process killed at startup.**
|
||||
- **Locking around the server table:** not visible in the analysed code.
|
||||
|
|
@ -13,6 +13,8 @@ Shared conclusions are in `docs/DATABASE.md` and `docs/CONFIG.md`. This ledger
|
|||
holds the binary-specific evidence.
|
||||
|
||||
## Outputs
|
||||
|
||||
Porting spec (Win32 → Linux substitutions, behaviour to reproduce, original bugs): `porting.md`.
|
||||
| Path | Content |
|
||||
|---|---|
|
||||
| `full/functions/*.c`, `full/index.tsv` | 20,334 functions. 2 failed: `0x14000e4d0` (SERVERS2DBC stub dispatcher, timeout) and `0x1400a8190` (worker command dispatcher, flow limit) |
|
||||
|
|
|
|||
91
decomp/DBCServer_Release_x64/porting.md
Normal file
91
decomp/DBCServer_Release_x64/porting.md
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
# DBCServer_Release_x64.exe — porting spec (Win32 → Linux)
|
||||
|
||||
This was kept from the removed Linux reconstruction `src/dbc_server/`
|
||||
(removed 2026-10-15). It records the facts about the original binary that a
|
||||
re-implementation needs, and the nearest Linux equivalent for each Win32 call.
|
||||
|
||||
Sources: `evidence.md`, `docs/DATABASE.md`, `docs/CONFIG.md`. Static analysis
|
||||
only.
|
||||
|
||||
## Structure to reproduce
|
||||
|
||||
```
|
||||
peer ──SERVERS2DBC RMI {rmi_id, …}──▶ stub handler (e.g. 0x140050d20: ++per-RMI counter)
|
||||
│ push {size, cmd 1001–1317, args}
|
||||
▼
|
||||
write queue [idx < 1000] (0x140144730; else "Invalid Queue Index")
|
||||
│ critical section + semaphore, 1 worker per queue
|
||||
▼
|
||||
worker switch (cmd - 1001) (0x1400a8190, jump table 0x1400a9dd4)
|
||||
▼
|
||||
CDBLobbyWriteQuery::DBReq* (e.g. 0x1400acbb0) — ADODB.Command "dbo.up_xxx"
|
||||
▼
|
||||
reply via DBC2SERVERS proxy (76 RMIs, 0x2af9–0x2b46)
|
||||
```
|
||||
|
||||
- **Data tables:**
|
||||
- The RMI → command → procedure mapping is in `rmi_to_proc.tsv`, and the
|
||||
command dispatch in `cmd_dispatch.tsv`.
|
||||
- Procedure parameters (names, SQL types, OUTPUT flags) are in
|
||||
`decomp/schema/<db>/routines.tsv`.
|
||||
- `decomp/scripts/gen_dbc_tables.py` joins them into `procs_used.tsv`.
|
||||
- **Queue array:** a 0x1f48-byte block holding 1000 pointers. How queue slots
|
||||
are created has not been decoded.
|
||||
- **Connections (`0x1400a7aa0`):**
|
||||
- Type 1 WEB = `db_web`, 2 AUTH = `db_account`, 3 GAME = `db_game`,
|
||||
4 GAME LOG = `db_game_log`.
|
||||
- A timeout of 0 becomes 500.
|
||||
- A string longer than 255 characters gives `Error … String Max.!`.
|
||||
- **AUTH uses the WEB timeout** (+0x134); this is an original bug.
|
||||
- **Startup (`0x140042cf0`):** opens AUTH and GAME connections, then
|
||||
`up_read_world_server` reads the world list.
|
||||
- **Connect failure (`0x1400a80c0`):**
|
||||
`DB Connect failed\n Connection String = %s` is appended to
|
||||
`Error_ADO DB Connect.Log`. The original writes the password verbatim.
|
||||
- **Procedure error:** `ADO_ON_ERROR : [ %s ] Proc [ %s ]` with source file
|
||||
and line (catch funclets, `dbreq_methods.tsv`).
|
||||
- **Single instance:** mutex `Global\RustyHearts_DBC`.
|
||||
- **Startup modes:** a numeric `argv[1]` selects agent mode (`total_option.xml`
|
||||
`<db><game>`); otherwise `argv[1]` is the path of the per-server XML
|
||||
(`docs/CONFIG.md`).
|
||||
|
||||
## Win32 → Linux substitutions
|
||||
|
||||
| Original (address) | Win32 / library | Nearest Linux equivalent | Notes |
|
||||
|---|---|---|---|
|
||||
| Single instance | `CreateMutexW("Global\\RustyHearts_DBC")` | `flock(LOCK_EX\|LOCK_NB)` on `$TMPDIR/RustyHearts_DBC.lock` | Lock file instead of a kernel object |
|
||||
| DB access (`0x1400a7aa0`, `0x1400a80c0`, `DBReq*`) | ADO `_Connection` / `ADODB.Command` over `SQLOLEDB`, `CoInitialize`, `_com_issue_errorex` | ODBC: unixODBC + Microsoft ODBC Driver 18, or FreeTDS `libtdsodbc` | Call as `{? = call dbo.proc(?,…)}` with positional parameters (ADO uses named ones); GUIDs in SQL Server byte order. ODBC 18 needs `TrustServerCertificate=yes`, because SQLOLEDB never validated TLS. FreeTDS needs `Port=` (not `host,port`) and `TDS_Version=7.4` |
|
||||
| ADO connection string | `Provider=SQLOLEDB;Data Source;Initial Catalog;User ID;Password` | `Driver={…};Server;Database;UID;PWD` | Keys map 1:1. Treating the XML `timeout` as milliseconds (rounded to seconds) is an inference |
|
||||
| Transport | ProudNet `CNetServer` over Winsock2/MSWSOCK/IOCP | BSD sockets / epoll | ProudNet internal net version is **196978**; 1.8 is 197144, so the protocols are incompatible. Interop would need a reimplementation of the 2013 protocol (`third-party/README.md`) |
|
||||
| Write queue (`0x140144730`) | `CRITICAL_SECTION` + semaphore (`ReleaseSemaphore`) | `std::mutex` + `std::condition_variable` | — |
|
||||
| Threads | `_beginthreadex` | `std::thread` | — |
|
||||
| XML (`MagiServerOption`, `MagiCustomOption`) | TinyXML 2.5.x | pugixml with `parse_fragment` (the files have several top-level elements) | — |
|
||||
| Console / stop | `wprintf`, console menu | stdout, `SIGINT`/`SIGTERM`; ignore `SIGPIPE` | — |
|
||||
| `timeGetTime` | WINMM | `std::chrono::steady_clock` | — |
|
||||
| Crash reporting | BugTrap, `MiniDumpWriteDump` | core dumps | — |
|
||||
|
||||
## Prior validation of the DB mapping (2026-10-14)
|
||||
|
||||
Run with the removed port against copies of the shipped `.mdf` files, on
|
||||
SQL Server 2022 through FreeTDS 1.5.19:
|
||||
- `up_read_world_server` returned 1 row.
|
||||
- `REQ_READ_ALL_GUILD(10101)` returned the guild row.
|
||||
- `REQ_UPDATE_CHARACTER_HEARTS(guid, n)` updated
|
||||
`CharacterTable.Hearts` (then reverted).
|
||||
- `REQ_INVEN_SLOT` failed with "Could not find stored procedure", confirming
|
||||
that procedure is missing from the shipped schema.
|
||||
|
||||
This validates the mapping and the parameter types against the schema. It
|
||||
says nothing about the original binary's runtime behaviour.
|
||||
|
||||
## Unknown / not recovered
|
||||
|
||||
- **RMI argument marshalling** (ProudNet) and the per-`DBReq` post-processing
|
||||
of recordsets (e.g. `FIRST_CHARACTER` in `0x1400acbb0`).
|
||||
- **52 RMIs without a procedure mapping**, mostly log, logout and auth.
|
||||
- **Some mappings may be wrong:** those found two call levels deep, e.g.
|
||||
`REQ_CHARACTER_READ_ALL_DATA` → `up_read_Inquiry_character`.
|
||||
- **Which connection each queue/`DBReq` uses:** inferred from the database
|
||||
that holds the procedure.
|
||||
- **Queue-slot creation; agent-mode address/port** (pushed by the Agent over
|
||||
RMI); the `guildlobby`, `channel` and `Option` elements.
|
||||
1215
decomp/DBCServer_Release_x64/procs_used.tsv
Normal file
1215
decomp/DBCServer_Release_x64/procs_used.tsv
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -26,7 +26,7 @@ hypotheses to confirm or revise during analysis.
|
|||
| `ManagerServer_Release_USA_x64.exe` | 6666752 | 6510.5 | c239477294c199f8df4ef70138cc1583c87838dd5e8cebed2ff0fae2bc6c5be2 | management/admin | pending |
|
||||
| `AuctionServer_Release_USA_x64.exe` | 6477824 | 6326.0 | ebca9cc047aeca125ac0621e3652339096c74862746157adc6c72057422f6dcf | auction house | pending |
|
||||
| `MatchServer_Release_USA_x64.exe` | 6134784 | 5991.0 | 341dd4a38c9e1abb590f60325ef52bf24b60a48a70e36e2f1651bbf00591fcf2 | matchmaking | pending |
|
||||
| `DBCServer_Release_x64.exe` | 3542016 | 3459.0 | d9e4fccdf3482e50247686d4452c0b13c75a53efc4bd7320668eece8d1ce828c | DB access server (observed: SERVERS2DBC RMI → write queues → ADO stored procs; 4 connection types; Linux port `src/dbc_server/`) | open |
|
||||
| `DBCServer_Release_x64.exe` | 3542016 | 3459.0 | d9e4fccdf3482e50247686d4452c0b13c75a53efc4bd7320668eece8d1ce828c | DB access server (observed: SERVERS2DBC RMI → write queues → ADO stored procs; 4 connection types) | open |
|
||||
| `DBCServer_LOG_Release_x64.exe` | 3394560 | 3315.0 | 2636d51679abb7703a6386a398f2afb985704aa0ce00d53e75d0ff3b093f5836 | logging DB variant | pending |
|
||||
| `GameGatewayServer_Release_x64.exe` | 3330560 | 3252.5 | 4cbec55dc3dfe5a0eeba3170722e6f1d33e55d6a0d007774266fe9db96f25d1a | gateway/proxy | pending |
|
||||
| `GMServer_Release_x64.exe` | 2004480 | 1957.5 | 8fba9dbb8809419d6e2edaee31d9ad60501341d97dca0fc82a1c1e7cc32199d1 | game-master tools | pending |
|
||||
|
|
|
|||
|
|
@ -1,81 +1,49 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Generate the DBCServer port's static tables from decomp evidence.
|
||||
"""Join the DBCServer RMI -> procedure map with procedure parameters.
|
||||
|
||||
Inputs (all derived, regenerable):
|
||||
Inputs (derived, regenerable):
|
||||
decomp/DBCServer_Release_x64/rmi_to_proc.tsv RMI -> cmd -> procedures
|
||||
decomp/schema/<db>/routines.tsv procedure parameters
|
||||
Output:
|
||||
src/dbc_server/generated/tables.inc
|
||||
decomp/DBCServer_Release_x64/procs_used.tsv
|
||||
one row per (procedure, parameter) for every procedure reachable from an RMI;
|
||||
db = database that holds it (NOT_IN_SCHEMA if called but absent).
|
||||
|
||||
Run from the repo root: python3 decomp/scripts/gen_dbc_tables.py
|
||||
"""
|
||||
import csv, os
|
||||
|
||||
ROOT = os.getcwd()
|
||||
OUT = os.path.join(ROOT, 'src/dbc_server/generated/tables.inc')
|
||||
# Database -> DBC connection type (DBC:0x1400a7aa0). The DB for a procedure is
|
||||
# where it lives in the schema dump (inference, see docs/DATABASE.md).
|
||||
DBTYPE = {'RustyHearts_Account': 'Web', 'RustyHearts_Auth': 'Auth',
|
||||
'RustyHearts': 'Game', 'RustyHearts_Log': 'GameLog'}
|
||||
SQLTYPE = {'int': 'Int', 'smallint': 'SmallInt', 'tinyint': 'TinyInt', 'bigint': 'BigInt',
|
||||
'bit': 'Bit', 'float': 'Float', 'uniqueidentifier': 'Guid', 'nvarchar': 'NVarChar',
|
||||
'nchar': 'NVarChar', 'varchar': 'VarChar', 'char': 'VarChar', 'datetime': 'DateTime'}
|
||||
OUT = os.path.join(ROOT, 'decomp/DBCServer_Release_x64/procs_used.tsv')
|
||||
DBS = ['RustyHearts_Account', 'RustyHearts_Auth', 'RustyHearts', 'RustyHearts_Log']
|
||||
|
||||
def rows(path):
|
||||
with open(path, newline='') as f:
|
||||
return list(csv.reader(f, delimiter='\t'))
|
||||
|
||||
procs = {} # lower name -> (name, db, [(param, type, maxlen, out)])
|
||||
for db in DBTYPE:
|
||||
for db in DBS:
|
||||
for r in rows(f'{ROOT}/decomp/schema/{db}/routines.tsv')[1:]:
|
||||
if len(r) < 7 or r[0] != 'SQL_STORED_PROCEDURE':
|
||||
continue
|
||||
p = procs.setdefault(r[1].lower(), (r[1], db, []))
|
||||
if r[2] != 'NULL':
|
||||
p[2].append((r[3], SQLTYPE.get(r[4], 'VarChar'), int(r[5]), r[6] == '1'))
|
||||
p[2].append((r[3], r[4], r[5], r[6]))
|
||||
|
||||
rmi = rows(f'{ROOT}/decomp/DBCServer_Release_x64/rmi_to_proc.tsv')
|
||||
hdr = rmi[0]; rmi = [dict(zip(hdr, r)) for r in rmi[1:]]
|
||||
|
||||
used = []
|
||||
for r in rmi:
|
||||
hdr = rmi[0]
|
||||
callers = {}
|
||||
for r in (dict(zip(hdr, x)) for x in rmi[1:]):
|
||||
for p in filter(None, r['procs'].split(',')):
|
||||
if p.lower() not in procs:
|
||||
# Called by DBCServer but absent from the shipped schema: keep it so
|
||||
# the port fails like the original would (ADO error). DB guessed from name.
|
||||
procs[p.lower()] = (p, 'RustyHearts_Log' if '_log_' in p else 'RustyHearts', None)
|
||||
if p.lower() not in used:
|
||||
used.append(p.lower())
|
||||
callers.setdefault(p.lower(), (p, []))[1].append(r['rmi_name'])
|
||||
|
||||
def cstr(s):
|
||||
return '"' + s.replace('\\', '\\\\').replace('"', '\\"') + '"'
|
||||
|
||||
out = ['// GENERATED by decomp/scripts/gen_dbc_tables.py - do not edit.',
|
||||
'// Sources: decomp/DBCServer_Release_x64/rmi_to_proc.tsv, decomp/schema/*/routines.tsv', '']
|
||||
for i, key in enumerate(used):
|
||||
name, db, ps = procs[key]
|
||||
ps = ps or []
|
||||
out.append(f'static const ProcParam kParams{i}[] = {{' +
|
||||
''.join(f'{{{cstr(n)}, SqlType::{t}, {m}, {str(o).lower()}}},' for n, t, m, o in ps) +
|
||||
('{nullptr, SqlType::Int, 0, false}' if not ps else '') + '};')
|
||||
out.append('')
|
||||
out.append('static const ProcDef kProcs[] = {')
|
||||
for i, key in enumerate(used):
|
||||
name, db, ps = procs[key]
|
||||
known = ps is not None
|
||||
out.append(f' {{{cstr(name)}, DbType::{DBTYPE[db]}, kParams{i}, {len(ps or [])}, {str(known).lower()}}},')
|
||||
out.append('};')
|
||||
out.append('')
|
||||
out.append('// rmi_id, name, cmd_id (0 = not queued / unknown), proc indexes (-1 = none)')
|
||||
out.append('static const RmiDef kRmis[] = {')
|
||||
for r in rmi:
|
||||
idx = [used.index(p.lower()) for p in filter(None, r['procs'].split(',')) if p.lower() in used]
|
||||
idx = (idx + [-1, -1, -1])[:3]
|
||||
missing = [p for p in filter(None, r['procs'].split(',')) if procs[p.lower()][2] is None]
|
||||
note = f' // missing in schema: {",".join(missing)}' if missing else ''
|
||||
out.append(f' {{{r["rmi_id"]}, {cstr(r["rmi_name"])}, {r["cmd_id"] or 0}, '
|
||||
f'{{{idx[0]}, {idx[1]}, {idx[2]}}}}},{note}')
|
||||
out.append('};')
|
||||
os.makedirs(os.path.dirname(OUT), exist_ok=True)
|
||||
open(OUT, 'w').write('\n'.join(out) + '\n')
|
||||
print(f'{len(used)} procedures, {len(rmi)} RMIs -> {OUT}')
|
||||
out = [['proc', 'db', 'rmis', 'param_no', 'param', 'sql_type', 'max_length', 'is_output']]
|
||||
for key, (name, rmis) in sorted(callers.items()):
|
||||
pname, db, params = procs.get(key, (name, 'NOT_IN_SCHEMA', []))
|
||||
if not params:
|
||||
out.append([pname, db, ','.join(rmis), '', '', '', '', ''])
|
||||
for i, (n, t, m, o) in enumerate(params, 1):
|
||||
out.append([pname, db, ','.join(rmis), str(i), n, t, m, o])
|
||||
with open(OUT, 'w', newline='') as f:
|
||||
csv.writer(f, delimiter='\t', lineterminator='\n').writerows(out)
|
||||
print(f'{len(callers)} procedures -> {OUT}')
|
||||
|
|
|
|||
|
|
@ -169,19 +169,15 @@ In `decomp/<X>/`:
|
|||
- Mark every name as inferred.
|
||||
- Record the original bugs you notice (leaks, races).
|
||||
|
||||
## 7. Port to Linux
|
||||
## 7. Porting spec
|
||||
|
||||
`src/<name>/` builds the lifted code with Meson (see `src/README.md`). Keep a
|
||||
`platform.{hpp,cpp}` layer that maps each Win32 call to its nearest POSIX
|
||||
equivalent.
|
||||
|
||||
The per-binary `README.md` must contain:
|
||||
- a substitution table: original address → Win32 API → Linux replacement →
|
||||
behaviour difference;
|
||||
- what is stubbed or not ported, and why.
|
||||
|
||||
Build with `ninja -C build` and smoke-test it. A passing test proves only that
|
||||
the port works, not how the original behaves.
|
||||
There is no source tree in this repo. The earlier `src/` reconstructions were
|
||||
removed on 2026-10-15. Instead, record the facts a re-implementation needs in
|
||||
`decomp/<X>/porting.md`:
|
||||
- the behaviour to reproduce, with decomp addresses;
|
||||
- a substitution table: original address → Win32 API → nearest Linux
|
||||
equivalent → behaviour difference;
|
||||
- the original bugs, and what is unknown or unrecovered.
|
||||
|
||||
## 8. Close
|
||||
|
||||
|
|
@ -199,5 +195,5 @@ the Evidence bundle, because session annotations are temporary.
|
|||
`third-party/README.md`**
|
||||
- [ ] Key functions decompiled; `full/` export done
|
||||
- [ ] `evidence.md`, `INVENTORY.md`, `docs/ARCHITECTURE.md` updated
|
||||
- [ ] Lift and Linux port updated, if in scope
|
||||
- [ ] `cpp/` lift and `porting.md` updated, if in scope
|
||||
- [ ] Evidence bundle exported; REA session closed
|
||||
|
|
|
|||
|
|
@ -1,52 +0,0 @@
|
|||
# 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 `<name>/` with its sources, a `meson.build` and a `README.md`. The
|
||||
README must cite decomp addresses and list substitutions and gaps.
|
||||
2. Add `subdir('<name>')` to `meson.build` in this directory.
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
# 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.
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
// agent.cpp — logic follows the Ghidra pseudocode cited per function.
|
||||
#include "agent.hpp"
|
||||
#include "platform.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
|
||||
namespace agent {
|
||||
|
||||
void LoggingManagerSession::NotifyServerState(const ServerEntry& e)
|
||||
{
|
||||
std::printf("[RMI->AgentManager] ServerState code=%d pid=%d running=%d\n",
|
||||
e.code, static_cast<int>(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<uint8_t, ServerTypeInfo> types,
|
||||
std::unique_ptr<IManagerSession> session)
|
||||
: opts_(std::move(opts)), types_(std::move(types)), session_(std::move(session)) {}
|
||||
|
||||
void Agent::AddServer(const ServerEntry& e)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> 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<std::recursive_mutex> 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<std::recursive_mutex> 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<std::recursive_mutex> 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<double>(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
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
// 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 <sys/types.h>
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
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<uint8_t, ServerTypeInfo> types,
|
||||
std::unique_ptr<IManagerSession> 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<uint8_t, ServerTypeInfo> types_;
|
||||
std::unique_ptr<IManagerSession> session_;
|
||||
std::map<int32_t, ServerEntry> servers_;
|
||||
std::recursive_mutex mu_; // original has no visible lock here (UNKNOWN)
|
||||
std::atomic<bool> running_{true};
|
||||
};
|
||||
|
||||
} // namespace agent
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
// 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 <csignal>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
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<uint8_t, agent::ServerTypeInfo> types;
|
||||
std::vector<agent::ServerEntry> 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<uint8_t>(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<uint8_t>(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<agent::LoggingManagerSession>());
|
||||
for (auto& e : servers) ag.AddServer(e);
|
||||
g_agent = &ag;
|
||||
std::signal(SIGINT, OnSignal);
|
||||
std::signal(SIGTERM, OnSignal);
|
||||
return ag.Run();
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
threads = dependency('threads')
|
||||
|
||||
executable('agent_release',
|
||||
['main.cpp', 'agent.cpp', 'platform.cpp'],
|
||||
dependencies : threads,
|
||||
install : false)
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
// platform.cpp — Linux implementations; Win32 originals cited per function.
|
||||
#include "platform.hpp"
|
||||
|
||||
#include <dirent.h>
|
||||
#include <fcntl.h>
|
||||
#include <signal.h>
|
||||
#include <spawn.h>
|
||||
#include <strings.h>
|
||||
#include <sys/file.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
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<std::string> args;
|
||||
for (std::string a; in >> a;) args.push_back(a);
|
||||
if (args.empty()) return 0;
|
||||
std::vector<char*> 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<void(pid_t)> 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_t>(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
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
// 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 <sys/types.h>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
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<void(pid_t)> 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
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
# 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')
|
||||
Loading…
Add table
Add a link
Reference in a new issue