GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 55.2% 116 / 0 / 210
Functions: 90.9% 10 / 0 / 11
Branches: 42.6% 58 / 0 / 136

src/bootstrap.cpp
Line Branch Exec Source
1 #include "DetourModKit/bootstrap.hpp"
2
3 #include "DetourModKit/config.hpp"
4 #include "DetourModKit/hook_manager.hpp"
5 #include "DetourModKit/input.hpp"
6 #include "DetourModKit/logger.hpp"
7 #include "DetourModKit/async_logger.hpp"
8
9 #include <DetourModKit.hpp>
10
11 #include <atomic>
12 #include <cstring>
13 #include <exception>
14
15 namespace DetourModKit::Bootstrap
16 {
17 namespace
18 {
19 HANDLE g_shutdown_event = nullptr;
20 HANDLE g_worker_thread = nullptr;
21 HANDLE g_instance_mutex = nullptr;
22 HMODULE g_module = nullptr;
23 std::atomic<bool> g_detach_called{false};
24
25 std::function<bool()> g_init_fn;
26 std::function<void()> g_shutdown_fn;
27
28 6 bool is_target_process(std::string_view expected) noexcept
29 {
30
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 5 times.
6 if (expected.empty())
31 {
32 1 return true;
33 }
34
35 5 char exe_path[MAX_PATH]{};
36 5 const DWORD len = GetModuleFileNameA(nullptr, exe_path, MAX_PATH);
37
2/4
✓ Branch 6 → 7 taken 5 times.
✗ Branch 6 → 8 not taken.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 5 times.
5 if (len == 0 || len >= MAX_PATH)
38 {
39 return false;
40 }
41
42 5 const char *exe_name = std::strrchr(exe_path, '\\');
43
1/2
✓ Branch 10 → 11 taken 5 times.
✗ Branch 10 → 12 not taken.
5 exe_name = exe_name ? exe_name + 1 : exe_path;
44
45 5 std::string expected_copy(expected);
46 5 return _stricmp(exe_name, expected_copy.c_str()) == 0;
47 5 }
48
49 4 bool acquire_instance_mutex(std::string_view prefix) noexcept
50 {
51
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 4 times.
4 if (prefix.empty())
52 {
53 return true;
54 }
55
56 4 wchar_t mutex_name[128]{};
57 4 std::wstring wprefix;
58 4 wprefix.reserve(prefix.size());
59
2/2
✓ Branch 12 → 10 taken 89 times.
✓ Branch 12 → 13 taken 4 times.
93 for (char c : prefix)
60 {
61 89 wprefix.push_back(static_cast<wchar_t>(static_cast<unsigned char>(c)));
62 }
63
64 4 const int n = wsprintfW(mutex_name, L"%s%lu", wprefix.c_str(), GetCurrentProcessId());
65
1/2
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 4 times.
4 if (n <= 0)
66 {
67 return false;
68 }
69
70 4 HANDLE h = CreateMutexW(nullptr, FALSE, mutex_name);
71
1/2
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 4 times.
4 if (!h)
72 {
73 return false;
74 }
75
2/2
✓ Branch 22 → 23 taken 2 times.
✓ Branch 22 → 25 taken 2 times.
4 if (GetLastError() == ERROR_ALREADY_EXISTS)
76 {
77 2 CloseHandle(h);
78 2 return false;
79 }
80
81 2 g_instance_mutex = h;
82 2 return true;
83 4 }
84
85 2 DWORD WINAPI lifecycle_thread(LPVOID) noexcept
86 {
87 2 Logger &logger = Logger::get_instance();
88
89 2 bool init_ok = false;
90
1/2
✓ Branch 4 → 5 taken 2 times.
✗ Branch 4 → 7 not taken.
2 if (g_init_fn)
91 {
92 try
93 {
94
2/2
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 19 taken 1 time.
2 init_ok = g_init_fn();
95 }
96
1/2
✓ Branch 19 → 20 taken 1 time.
✗ Branch 19 → 24 not taken.
1 catch (const std::exception &e)
97 {
98 1 logger.error("Bootstrap: init_fn threw: {}", e.what());
99 1 }
100 catch (...)
101 {
102 logger.error("Bootstrap: init_fn threw unknown exception.");
103 }
104 }
105 else
106 {
107 init_ok = true;
108 }
109
110
2/2
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 11 taken 1 time.
2 if (!init_ok)
111 {
112 1 logger.error("Bootstrap: init_fn returned failure; worker idling until detach.");
113 }
114
115
1/2
✓ Branch 11 → 12 taken 2 times.
✗ Branch 11 → 13 not taken.
2 if (g_shutdown_event)
116 {
117 2 WaitForSingleObject(g_shutdown_event, INFINITE);
118 }
119
120
1/2
✓ Branch 14 → 15 taken 2 times.
✗ Branch 14 → 16 not taken.
2 if (g_shutdown_fn)
121 {
122 try
123 {
124
2/2
✓ Branch 15 → 16 taken 1 time.
✓ Branch 15 → 27 taken 1 time.
2 g_shutdown_fn();
125 }
126
1/2
✓ Branch 27 → 28 taken 1 time.
✗ Branch 27 → 32 not taken.
1 catch (const std::exception &e)
127 {
128 1 logger.error("Bootstrap: shutdown_fn threw: {}", e.what());
129 1 }
130 catch (...)
131 {
132 logger.error("Bootstrap: shutdown_fn threw unknown exception.");
133 }
134 }
135
136 2 DMK_Shutdown();
137 2 return 0;
138 }
139 } // anonymous namespace
140
141 namespace
142 {
143 1 void release_instance_mutex_locked() noexcept
144 {
145
1/2
✓ Branch 2 → 3 taken 1 time.
✗ Branch 2 → 5 not taken.
1 if (g_instance_mutex)
146 {
147 1 CloseHandle(g_instance_mutex);
148 1 g_instance_mutex = nullptr;
149 }
150 1 }
151
152 // Shared teardown path for early-attach failures. Ensures singletons
153 // initialized by Logger::configure / user init_fn are torn down before
154 // on_dll_attach returns FALSE, so a subsequent DLL load starts clean.
155 void unwind_early_attach_failure() noexcept
156 {
157 if (g_shutdown_event)
158 {
159 CloseHandle(g_shutdown_event);
160 g_shutdown_event = nullptr;
161 }
162 release_instance_mutex_locked();
163 g_init_fn = nullptr;
164 g_shutdown_fn = nullptr;
165 g_module = nullptr;
166 try
167 {
168 DMK_Shutdown();
169 }
170 catch (...)
171 {
172 }
173 }
174 } // anonymous namespace
175
176 7 [[nodiscard]] BOOL on_dll_attach(HMODULE hMod,
177 const ModInfo &info,
178 std::function<bool()> init_fn,
179 std::function<void()> shutdown_fn)
180 {
181
3/4
✓ Branch 2 → 3 taken 6 times.
✓ Branch 2 → 4 taken 1 time.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 6 times.
7 if (g_shutdown_event || g_worker_thread)
182 {
183 1 return FALSE;
184 }
185
186 6 g_module = hMod;
187
2/2
✓ Branch 5 → 6 taken 5 times.
✓ Branch 5 → 7 taken 1 time.
6 if (hMod)
188 {
189 5 DisableThreadLibraryCalls(hMod);
190 }
191
192
2/2
✓ Branch 8 → 9 taken 2 times.
✓ Branch 8 → 10 taken 4 times.
6 if (!is_target_process(info.game_process_name))
193 {
194 2 g_module = nullptr;
195 2 return FALSE;
196 }
197
198
2/2
✓ Branch 11 → 12 taken 2 times.
✓ Branch 11 → 13 taken 2 times.
4 if (!acquire_instance_mutex(info.instance_mutex_prefix))
199 {
200 2 g_module = nullptr;
201 2 return FALSE;
202 }
203
204
1/2
✓ Branch 14 → 15 taken 2 times.
✗ Branch 14 → 33 not taken.
2 Logger::configure(info.prefix, info.log_file);
205 2 Logger::get_instance().enable_async_mode(info.async_cfg);
206
207 2 g_init_fn = std::move(init_fn);
208 2 g_shutdown_fn = std::move(shutdown_fn);
209
210 2 g_shutdown_event = CreateEventW(nullptr, TRUE, FALSE, nullptr);
211
1/2
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 27 taken 2 times.
2 if (!g_shutdown_event)
212 {
213 unwind_early_attach_failure();
214 return FALSE;
215 }
216
217 2 g_worker_thread = CreateThread(nullptr, 0, lifecycle_thread, nullptr, 0, nullptr);
218
1/2
✗ Branch 28 → 29 not taken.
✓ Branch 28 → 31 taken 2 times.
2 if (!g_worker_thread)
219 {
220 unwind_early_attach_failure();
221 return FALSE;
222 }
223
224 2 return TRUE;
225 }
226
227 5 void request_shutdown() noexcept
228 {
229
2/2
✓ Branch 2 → 3 taken 3 times.
✓ Branch 2 → 4 taken 2 times.
5 if (g_shutdown_event)
230 {
231 3 SetEvent(g_shutdown_event);
232 }
233 5 }
234
235 2 void on_dll_detach(BOOL is_process_exit) noexcept
236 {
237 2 bool expected = false;
238
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 1 time.
2 if (!g_detach_called.compare_exchange_strong(expected, true,
239 std::memory_order_acq_rel))
240 {
241 1 return;
242 }
243
244 // On process exit Windows has already terminated every other thread in
245 // the process before calling DllMain with DLL_PROCESS_DETACH. Waiting
246 // on g_worker_thread would block forever because the worker was
247 // abruptly killed mid-WaitForSingleObject. Skip the wait and run the
248 // explicit shutdown path directly.
249
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 10 taken 1 time.
1 if (is_process_exit)
250 {
251 if (g_shutdown_fn)
252 {
253 try
254 {
255 g_shutdown_fn();
256 }
257 catch (const std::exception &e)
258 {
259 try
260 {
261 Logger::get_instance().error(
262 "Bootstrap: shutdown_fn threw: {}", e.what());
263 }
264 catch (...)
265 {
266 }
267 }
268 catch (...)
269 {
270 try
271 {
272 Logger::get_instance().error(
273 "Bootstrap: shutdown_fn threw unknown exception.");
274 }
275 catch (...)
276 {
277 }
278 }
279 }
280 try
281 {
282 DMK_Shutdown();
283 }
284 catch (const std::exception &e)
285 {
286 try
287 {
288 Logger::get_instance().error(
289 "Bootstrap: DMK_Shutdown threw: {}", e.what());
290 }
291 catch (...)
292 {
293 }
294 }
295 catch (...)
296 {
297 try
298 {
299 Logger::get_instance().error(
300 "Bootstrap: DMK_Shutdown threw unknown exception.");
301 }
302 catch (...)
303 {
304 }
305 }
306 }
307
1/2
✓ Branch 10 → 11 taken 1 time.
✗ Branch 10 → 12 not taken.
1 else if (g_shutdown_event)
308 {
309 // Dynamic unload under loader lock. Signal the worker but do NOT
310 // wait here: blocking under loader lock deadlocks any peer DllMain
311 // that touches Win32 APIs. The contract (see bootstrap.hpp) is
312 // that callers who need a clean unload call request_shutdown()
313 // before FreeLibrary and give the worker time to drain.
314 1 SetEvent(g_shutdown_event);
315 }
316
317
1/2
✓ Branch 12 → 13 taken 1 time.
✗ Branch 12 → 15 not taken.
1 if (g_shutdown_event)
318 {
319 1 CloseHandle(g_shutdown_event);
320 1 g_shutdown_event = nullptr;
321 }
322
323
1/2
✓ Branch 15 → 16 taken 1 time.
✗ Branch 15 → 18 not taken.
1 if (g_worker_thread)
324 {
325 1 CloseHandle(g_worker_thread);
326 1 g_worker_thread = nullptr;
327 }
328
329 1 release_instance_mutex_locked();
330 1 g_module = nullptr;
331 }
332
333 2 HMODULE module_handle() noexcept
334 {
335 2 return g_module;
336 }
337
338 8 void on_logic_dll_unload(std::span<const std::string_view> hook_names,
339 std::span<const std::string_view> binding_names) noexcept
340 {
341 8 Logger &logger = Logger::get_instance();
342 8 size_t hooks_removed = 0;
343 8 size_t bindings_removed = 0;
344
345
2/2
✓ Branch 26 → 5 taken 8 times.
✓ Branch 26 → 27 taken 8 times.
24 for (const auto name : hook_names)
346 {
347 try
348 {
349
2/4
✓ Branch 7 → 8 taken 8 times.
✗ Branch 7 → 48 not taken.
✓ Branch 8 → 9 taken 8 times.
✗ Branch 8 → 48 not taken.
8 auto result = HookManager::get_instance().remove_hook(name);
350
2/2
✓ Branch 10 → 11 taken 5 times.
✓ Branch 10 → 12 taken 3 times.
8 if (result)
351 {
352 5 ++hooks_removed;
353 }
354 else
355 {
356
1/2
✓ Branch 14 → 15 taken 3 times.
✗ Branch 14 → 46 not taken.
3 logger.debug(
357 "Bootstrap: on_logic_dll_unload: hook '{}' not removed ({}).",
358 6 name, Hook::error_to_string(result.error()));
359 }
360 }
361 catch (const std::exception &e)
362 {
363 logger.error(
364 "Bootstrap: on_logic_dll_unload caught exception removing hook '{}': {}",
365 name, e.what());
366 }
367 catch (...)
368 {
369 logger.error(
370 "Bootstrap: on_logic_dll_unload caught unknown exception removing hook '{}'.",
371 name);
372 }
373 }
374
375
2/2
✓ Branch 42 → 29 taken 9 times.
✓ Branch 42 → 43 taken 8 times.
25 for (const auto name : binding_names)
376 {
377 try
378 {
379 // Pass invoke_callbacks=false because this helper is documented
380 // as safe from DllMain detach paths. User on_state_change(false)
381 // callbacks for held bindings live in the unloading Logic DLL;
382 // running them under loader lock is the deadlock-or-crash
383 // vector that the v3.2.1 leak-on-purpose discipline was set
384 // up to forbid.
385 9 bindings_removed += InputManager::get_instance().remove_binding_by_name(name, false);
386 }
387 catch (const std::exception &e)
388 {
389 logger.error(
390 "Bootstrap: on_logic_dll_unload caught exception removing binding '{}': {}",
391 name, e.what());
392 }
393 catch (...)
394 {
395 logger.error(
396 "Bootstrap: on_logic_dll_unload caught unknown exception removing binding '{}'.",
397 name);
398 }
399 }
400
401 8 logger.info(
402 "Bootstrap: on_logic_dll_unload drained {} hook(s) and {} binding(s).",
403 hooks_removed, bindings_removed);
404
405 // Wipe the Config registry last because the prior hook and
406 // binding teardown may invoke a registered setter one final
407 // time (a setter that observes a binding-driven flag, for
408 // instance). Clearing first would orphan that final-fire path
409 // mid-call. The registered std::function setters' call
410 // operators, vtables, and destructors live in the Logic DLL's
411 // .text segment; once the loader unmaps that segment, every
412 // surviving entry becomes a use-after-unload hazard. The next
413 // attach's replace_or_append destroys the stale slot before
414 // installing the fresh one, which would invoke the old
415 // setter's destructor against freed pages.
416 try
417 {
418
1/2
✓ Branch 44 → 45 taken 8 times.
✗ Branch 44 → 57 not taken.
8 Config::clear_registered_items();
419 }
420 catch (const std::exception &e)
421 {
422 try
423 {
424 logger.error(
425 "Bootstrap: on_logic_dll_unload caught exception in clear_registered_items: {}",
426 e.what());
427 }
428 catch (...)
429 {
430 }
431 }
432 catch (...)
433 {
434 try
435 {
436 logger.error(
437 "Bootstrap: on_logic_dll_unload caught unknown exception in clear_registered_items.");
438 }
439 catch (...)
440 {
441 }
442 }
443 8 }
444
445 9 void on_logic_dll_unload_all() noexcept
446 {
447 9 Logger &logger = Logger::get_instance();
448
449 // Hooks first so the original prologue bytes are restored before
450 // the binding teardown can disturb any callback that still trampolines
451 // through SafetyHook. remove_all_hooks() resets m_shutdown_called
452 // at the end, leaving HookManager re-usable for the next attach.
453 try
454 {
455
2/4
✓ Branch 3 → 4 taken 9 times.
✗ Branch 3 → 11 not taken.
✓ Branch 4 → 5 taken 9 times.
✗ Branch 4 → 11 not taken.
9 HookManager::get_instance().remove_all_hooks();
456 }
457 catch (const std::exception &e)
458 {
459 try
460 {
461 logger.error(
462 "Bootstrap: on_logic_dll_unload_all caught exception in remove_all_hooks: {}",
463 e.what());
464 }
465 catch (...)
466 {
467 }
468 }
469 catch (...)
470 {
471 try
472 {
473 logger.error(
474 "Bootstrap: on_logic_dll_unload_all caught unknown exception in remove_all_hooks.");
475 }
476 catch (...)
477 {
478 }
479 }
480
481 // clear_bindings() leaves the poll thread running and ready to accept
482 // fresh bindings, matching the "tear down per-Logic-DLL state but keep
483 // the manager re-usable" contract that the named-list overload honours.
484 // Pass invoke_callbacks=false because this helper is documented as
485 // safe from DllMain detach paths: user release callbacks live in the
486 // unloading Logic DLL and must not be invoked under loader lock.
487 try
488 {
489 9 InputManager::get_instance().clear_bindings(false);
490 }
491 catch (const std::exception &e)
492 {
493 try
494 {
495 logger.error(
496 "Bootstrap: on_logic_dll_unload_all caught exception in clear_bindings: {}",
497 e.what());
498 }
499 catch (...)
500 {
501 }
502 }
503 catch (...)
504 {
505 try
506 {
507 logger.error(
508 "Bootstrap: on_logic_dll_unload_all caught unknown exception in clear_bindings.");
509 }
510 catch (...)
511 {
512 }
513 }
514
515 try
516 {
517
1/2
✓ Branch 7 → 8 taken 9 times.
✗ Branch 7 → 30 not taken.
9 logger.info("Bootstrap: on_logic_dll_unload_all drained all hooks and bindings.");
518 }
519 catch (...)
520 {
521 }
522
523 // Wipe the Config registry last for the same reason as the
524 // named-list overload: the prior remove_all_hooks /
525 // clear_bindings calls may fire a registered setter one final
526 // time, and clearing first would orphan that path. The
527 // registered std::function setters' call operators, vtables,
528 // and destructors live in the unloading Logic DLL's .text;
529 // every surviving entry becomes a use-after-unload hazard the
530 // moment the loader reclaims those pages.
531 try
532 {
533
1/2
✓ Branch 9 → 10 taken 9 times.
✗ Branch 9 → 34 not taken.
9 Config::clear_registered_items();
534 }
535 catch (const std::exception &e)
536 {
537 try
538 {
539 logger.error(
540 "Bootstrap: on_logic_dll_unload_all caught exception in clear_registered_items: {}",
541 e.what());
542 }
543 catch (...)
544 {
545 }
546 }
547 catch (...)
548 {
549 try
550 {
551 logger.error(
552 "Bootstrap: on_logic_dll_unload_all caught unknown exception in clear_registered_items.");
553 }
554 catch (...)
555 {
556 }
557 }
558 9 }
559 } // namespace DetourModKit::Bootstrap
560