./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
423 / 494
85.6%
Branches:
167 / 238
70.2%
Functions:
49 / 49
100.0%

src/session.cpp
Line Branch Exec Source
1 #include "DetourModKit/session.hpp"
2
3 #include "DetourModKit/config.hpp"
4 #include "DetourModKit/diagnostics.hpp"
5 #include "DetourModKit/input.hpp"
6 #include "DetourModKit/logger.hpp"
7 #include "DetourModKit/memory.hpp"
8
9 #include "internal/config_reload_gate.hpp"
10 #include "internal/diagnostics_population.hpp"
11 #include "internal/input_binding_lifecycle.hpp"
12 #include "internal/input_delivery_scope.hpp"
13 #include "internal/lifecycle_context.hpp"
14 #include "internal/memory_guarded.hpp"
15 #include "platform.hpp"
16
17 #include <windows.h>
18
19 #include <algorithm>
20 #include <array>
21 #include <atomic>
22 #include <chrono>
23 #include <climits>
24 #include <cstdint>
25 #include <cwchar>
26 #include <exception>
27 #include <new>
28 #include <optional>
29 #include <string_view>
30 #include <type_traits>
31 #include <utility>
32
33 namespace DetourModKit
34 {
35 namespace detail
36 {
37 #if defined(DMK_ENABLE_TEST_SEAMS)
38 void (*g_session_configure_test_hook)() = nullptr;
39 #endif
40
41 struct SessionBootstrapAccess
42 {
43 52 [[nodiscard]] static Session make(void *instance_mutex) noexcept { return Session(instance_mutex); }
44 };
45 } // namespace detail
46
47 namespace
48 {
49 // Static bootstrap machinery (the async DllMain path only)
50 // The synchronous Session::start path touches none of these: it returns a Session the caller holds directly.
51 // These statics exist only to host a Session on a worker thread across a DllMain attach/detach pair.
52 // request_shutdown() may signal from any thread while a control thread retires the event. The high bit closes
53 // admission; the remaining bits count callers that may already have loaded the handle.
54 constexpr std::uint64_t SHUTDOWN_EVENT_RETIRED = std::uint64_t{1} << 63;
55 constexpr std::uint64_t SHUTDOWN_EVENT_READER_MASK = ~SHUTDOWN_EVENT_RETIRED;
56 constexpr size_t SHUTDOWN_EVENT_DRAIN_YIELDS = 1024;
57 std::atomic<HANDLE> s_shutdown_event{nullptr};
58 std::atomic<std::uint64_t> s_shutdown_event_access{SHUTDOWN_EVENT_RETIRED};
59 static_assert(std::atomic<std::uint64_t>::is_always_lock_free);
60 HANDLE s_worker_thread = nullptr;
61
62 enum class BootstrapState : std::uint8_t
63 {
64 Drained,
65 Starting,
66 Ready,
67 Draining,
68 Detached
69 };
70
71 // The single admission authority for the bootstrap statics, and the serializer for the one terminal action on
72 // them, without taking a lock from DllMain. Every transition into and out of Drained brackets the whole of the
73 // publication or retirement it names, so no other static may be used to decide admission: a drain nulls the
74 // worker handle and the event well before it has finished retiring the callback and the module identity.
75 std::atomic<BootstrapState> s_bootstrap_state{BootstrapState::Drained};
76
77 constexpr size_t BOOTSTRAP_TEXT_CAPACITY = 32768;
78
79 struct BootstrapLoggerInfo
80 {
81 std::array<char, BOOTSTRAP_TEXT_CAPACITY> name{};
82 std::array<char, BOOTSTRAP_TEXT_CAPACITY> log_file{};
83 size_t name_size{0};
84 size_t log_file_size{0};
85 size_t queue_capacity{DEFAULT_QUEUE_CAPACITY};
86 size_t batch_size{DEFAULT_BATCH_SIZE};
87 std::chrono::milliseconds flush_interval{DEFAULT_FLUSH_INTERVAL};
88 OverflowPolicy overflow_policy{OverflowPolicy::DropOldest};
89 size_t spin_backoff_iterations{DEFAULT_SPIN_BACKOFF_ITERATIONS};
90 std::chrono::milliseconds block_timeout_ms{16};
91 size_t block_max_spin_iterations{1000};
92 LogOpenMode log_open_mode{LogOpenMode::Truncate};
93 LogSourceStampMode log_source_stamp_mode{};
94
95 55 [[nodiscard]] bool stage(const ModInfo &info) noexcept
96 {
97
5/6
✓ Branch 5 → 6 taken 54 times.
✓ Branch 5 → 10 taken 1 time.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 54 times.
✓ Branch 12 → 13 taken 1 time.
✓ Branch 12 → 14 taken 54 times.
164 if (info.name.size() > name.size() || info.log_file.size() > log_file.size())
98 {
99 1 return false;
100 }
101
102 54 std::copy(info.name.begin(), info.name.end(), name.begin());
103 54 std::copy(info.log_file.begin(), info.log_file.end(), log_file.begin());
104 54 name_size = info.name.size();
105 54 log_file_size = info.log_file.size();
106 54 queue_capacity = info.log.queue_capacity;
107 54 batch_size = info.log.batch_size;
108 54 flush_interval = info.log.flush_interval;
109 54 overflow_policy = info.log.overflow_policy;
110 54 spin_backoff_iterations = info.log.spin_backoff_iterations;
111 54 block_timeout_ms = info.log.block_timeout_ms;
112 54 block_max_spin_iterations = info.log.block_max_spin_iterations;
113 54 log_open_mode = info.log_open_mode;
114 54 log_source_stamp_mode = info.log_source_stamp_mode;
115 54 return true;
116 }
117
118 52 [[nodiscard]] std::string_view name_view() const noexcept { return {name.data(), name_size}; }
119 52 [[nodiscard]] std::string_view log_file_view() const noexcept { return {log_file.data(), log_file_size}; }
120
121 52 [[nodiscard]] AsyncLoggerConfig logger_config() const
122 {
123 52 AsyncLoggerConfig config{};
124 52 config.queue_capacity = queue_capacity;
125 52 config.batch_size = batch_size;
126 52 config.flush_interval = flush_interval;
127 52 config.overflow_policy = overflow_policy;
128 52 config.spin_backoff_iterations = spin_backoff_iterations;
129 52 config.block_timeout_ms = block_timeout_ms;
130 52 config.block_max_spin_iterations = block_max_spin_iterations;
131 52 return config;
132 }
133
134 49 void clear() noexcept
135 {
136 49 name_size = 0;
137 49 log_file_size = 0;
138 49 }
139 };
140
141 BootstrapLoggerInfo s_bootstrap_logger_info;
142
143 // These two objects may still own consumer state when DLL_PROCESS_DETACH runs. Construct them into raw static
144 // storage so the CRT never registers destructors for them: a clean off-loader-lock drain resets their contents,
145 // while loader detach retains them untouched instead of destroying callback captures inside DllMain.
146 using ReadyCallback = std::move_only_function<Result<void>(Session &)>;
147 alignas(std::optional<Session>) unsigned char s_pending_session_storage[sizeof(std::optional<Session>)];
148 alignas(ReadyCallback) unsigned char s_on_ready_storage[sizeof(ReadyCallback)];
149 std::optional<Session> &s_pending_session =
150 *::new (static_cast<void *>(s_pending_session_storage)) std::optional<Session>();
151 ReadyCallback &s_on_ready = *::new (static_cast<void *>(s_on_ready_storage)) ReadyCallback();
152
153 // This trivial pointer needs no raw-storage treatment.
154 BootstrapReadyFn s_on_ready_fn = nullptr;
155 static_assert(
156 std::is_trivially_copyable_v<BootstrapReadyFn> && std::is_trivially_destructible_v<BootstrapReadyFn>,
157 "the DllMain attach callback must stage no consumer capture (see bootstrap_attach)."
158 );
159
160 #if defined(DMK_ENABLE_TEST_SEAMS)
161 // Counts signals that reached SetEvent on a handle the kernel had already invalidated. Admission is supposed to
162 // make that impossible, so a nonzero count is the direct observation of the use-after-close this word prevents,
163 // and a test can assert it unconditionally rather than inferring safety from which retirement branch ran.
164 std::atomic<std::uint64_t> s_signal_on_invalid_event{0};
165 void (*s_bootstrap_pre_setup_probe)() noexcept = nullptr;
166 #endif
167
168 9096876 void signal_shutdown_event() noexcept
169 {
170 // Observing "not retired" and registering as a reader must be ONE atomic step. A plain load followed by an
171 // unconditional fetch_add lets a caller preempted between the two land its increment on a generation that
172 // was retired and reopened meanwhile: its matching decrement then underflows the reopened word to all-ones
173 // (which has the retired bit set) and no later request can ever signal that generation again.
174 9543314 std::uint64_t access = s_shutdown_event_access.load(std::memory_order_acquire);
175 do
176 {
177
2/2
✓ Branch 10 → 11 taken 1230222 times.
✓ Branch 10 → 12 taken 11107123 times.
12337345 if ((access & SHUTDOWN_EVENT_RETIRED) != 0)
178 {
179 1230222 return;
180 }
181 } while (
182 11587485 !s_shutdown_event_access
183
2/2
✓ Branch 17 → 10 taken 2794031 times.
✓ Branch 17 → 18 taken 8793454 times.
22694608 .compare_exchange_weak(access, access + 1, std::memory_order_acq_rel, std::memory_order_acquire)
184 );
185
186
2/2
✓ Branch 19 → 20 taken 7949740 times.
✓ Branch 19 → 25 taken 538619 times.
8793454 if (HANDLE event = s_shutdown_event.load(std::memory_order_acquire))
187 {
188 #if defined(DMK_ENABLE_TEST_SEAMS)
189
1/2
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 25 taken 8131447 times.
7949740 if (!SetEvent(event))
190 {
191 s_signal_on_invalid_event.fetch_add(1, std::memory_order_relaxed);
192 }
193 #else
194 SetEvent(event);
195 #endif
196 }
197 s_shutdown_event_access.fetch_sub(1, std::memory_order_release);
198 }
199
200 /**
201 * @brief Closes admission and drops the event pointer, retaining any live kernel object.
202 * @return true when a live handle was dropped, so the caller records the retention it just created.
203 */
204 6 [[nodiscard]] bool abandon_shutdown_event() noexcept
205 {
206 s_shutdown_event_access.fetch_or(SHUTDOWN_EVENT_RETIRED, std::memory_order_acq_rel);
207 6 return s_shutdown_event.exchange(nullptr, std::memory_order_acq_rel) != nullptr;
208 }
209
210 2 void close_shutdown_event_at_process_exit() noexcept
211 {
212 s_shutdown_event_access.fetch_or(SHUTDOWN_EVENT_RETIRED, std::memory_order_acq_rel);
213
1/2
✓ Branch 5 → 6 taken 2 times.
✗ Branch 5 → 7 not taken.
2 if (HANDLE event = s_shutdown_event.exchange(nullptr, std::memory_order_acq_rel))
214 {
215 2 CloseHandle(event);
216 }
217 2 }
218
219 46 void retire_shutdown_event_after_drain() noexcept
220 {
221 s_shutdown_event_access.fetch_or(SHUTDOWN_EVENT_RETIRED, std::memory_order_acq_rel);
222 46 const HANDLE event = s_shutdown_event.exchange(nullptr, std::memory_order_acq_rel);
223
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 46 times.
46 if (event == nullptr)
224 {
225 ✗ return;
226 }
227
228
1/2
✓ Branch 20 → 8 taken 52 times.
✗ Branch 20 → 21 not taken.
52 for (size_t i = 0; i < SHUTDOWN_EVENT_DRAIN_YIELDS; ++i)
229 {
230
2/2
✓ Branch 15 → 16 taken 46 times.
✓ Branch 15 → 18 taken 6 times.
52 if ((s_shutdown_event_access.load(std::memory_order_acquire) & SHUTDOWN_EVENT_READER_MASK) == 0)
231 {
232 46 CloseHandle(event);
233 46 return;
234 }
235 6 SwitchToThread();
236 }
237
238 // A suspended signaler may retain access indefinitely. Keep its handle valid instead of blocking teardown.
239 ✗ diagnostics::record_intentional_leak(diagnostics::LeakSubsystem::Bootstrap);
240 }
241
242 1660 Result<bool> is_target_process(std::string_view expected, const char *operation) noexcept
243 {
244
2/2
✓ Branch 3 → 4 taken 1639 times.
✓ Branch 3 → 6 taken 21 times.
1660 if (expected.empty())
245 {
246 1639 return true;
247 }
248
249 // The full path can exceed MAX_PATH. Static storage covers Windows' extended path limit without allocating
250 // in a DllMain caller; lifecycle admission serializes every writer of this scratch buffer.
251 21 constexpr DWORD MODULE_PATH_CAPACITY = 32768;
252 static wchar_t s_exe_path[MODULE_PATH_CAPACITY];
253 21 const DWORD path_size = GetModuleFileNameW(nullptr, s_exe_path, MODULE_PATH_CAPACITY);
254
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 12 taken 21 times.
21 if (path_size == 0)
255 {
256 ✗ return std::unexpected(Error{ErrorCode::SystemCallFailed, operation, GetLastError()});
257 }
258
1/2
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 16 taken 21 times.
21 if (path_size >= MODULE_PATH_CAPACITY)
259 {
260 ✗ return std::unexpected(Error{ErrorCode::SystemCallFailed, operation, ERROR_INSUFFICIENT_BUFFER});
261 }
262
263 21 const wchar_t *exe_name = std::wcsrchr(s_exe_path, L'\\');
264
1/2
✓ Branch 17 → 18 taken 21 times.
✗ Branch 17 → 19 not taken.
21 exe_name = exe_name ? exe_name + 1 : s_exe_path;
265
266 // MultiByteToWideChar takes an int input count. Reject only values outside that domain.
267 // The fixed buffer rejects malformed input and values beyond MAX_PATH - 1 UTF-16 code units.
268
1/2
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 24 taken 21 times.
21 if (expected.size() > static_cast<size_t>(INT_MAX))
269 {
270 ✗ return false;
271 }
272 wchar_t expected_buf[MAX_PATH];
273 21 const int wide_len = MultiByteToWideChar(
274 CP_UTF8,
275 MB_ERR_INVALID_CHARS,
276 expected.data(),
277 21 static_cast<int>(expected.size()),
278 expected_buf,
279 MAX_PATH - 1
280 );
281
1/2
✗ Branch 27 → 28 not taken.
✓ Branch 27 → 30 taken 21 times.
21 if (wide_len <= 0)
282 {
283 ✗ return false;
284 }
285 21 return CompareStringOrdinal(exe_name, -1, expected_buf, wide_len, TRUE) == CSTR_EQUAL;
286 }
287
288 /**
289 * @brief Classifies the outcome of a single-instance mutex acquisition.
290 */
291 enum class MutexAcquire : std::uint8_t
292 {
293 /// A fresh mutex was created; @p out holds the handle the Session must close.
294 Acquired,
295 /// No prefix was requested; @p out is null and no single-instance guard exists.
296 NoGuard,
297 /// The named mutex already existed: another load of this mod is live.
298 AlreadyHeld,
299 /// CreateMutexW failed; @p err holds GetLastError().
300 SystemError
301 };
302
303 // Builds a per-PID named mutex from @p prefix and reports whether this load won the single-instance race.
304 // Static scratch storage avoids heap work in bootstrap; lifecycle admission serializes every writer.
305 1654 MutexAcquire acquire_instance_mutex(std::string_view prefix, HANDLE &out, DWORD &err) noexcept
306 {
307 1654 out = nullptr;
308 1654 err = 0;
309
2/2
✓ Branch 3 → 4 taken 1637 times.
✓ Branch 3 → 5 taken 17 times.
1654 if (prefix.empty())
310 {
311 1637 return MutexAcquire::NoGuard;
312 }
313
314 17 constexpr size_t MUTEX_NAME_CAPACITY = 32768;
315 static wchar_t s_mutex_name[MUTEX_NAME_CAPACITY];
316 17 constexpr size_t MAX_PID_DIGITS = 10;
317
1/2
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 17 times.
17 if (prefix.size() > MUTEX_NAME_CAPACITY - MAX_PID_DIGITS - 1)
318 {
319 ✗ err = ERROR_FILENAME_EXCED_RANGE;
320 ✗ return MutexAcquire::SystemError;
321 }
322
323 17 size_t name_size = 0;
324
2/2
✓ Branch 11 → 10 taken 578 times.
✓ Branch 11 → 12 taken 17 times.
595 for (const char character : prefix)
325 {
326 578 s_mutex_name[name_size++] = static_cast<wchar_t>(static_cast<unsigned char>(character));
327 }
328
329 wchar_t reversed_pid[MAX_PID_DIGITS];
330 17 size_t pid_digits = 0;
331 17 DWORD pid = GetCurrentProcessId();
332 do
333 {
334 67 reversed_pid[pid_digits++] = static_cast<wchar_t>(L'0' + pid % 10);
335 67 pid /= 10;
336
2/2
✓ Branch 14 → 15 taken 50 times.
✓ Branch 14 → 16 taken 17 times.
67 } while (pid != 0);
337
2/2
✓ Branch 18 → 17 taken 67 times.
✓ Branch 18 → 19 taken 17 times.
84 while (pid_digits != 0)
338 {
339 67 s_mutex_name[name_size++] = reversed_pid[--pid_digits];
340 }
341 17 s_mutex_name[name_size] = L'\0';
342
343 17 HANDLE handle = CreateMutexW(nullptr, FALSE, s_mutex_name);
344
1/2
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 23 taken 17 times.
17 if (!handle)
345 {
346 ✗ err = GetLastError();
347 ✗ return MutexAcquire::SystemError;
348 }
349
2/2
✓ Branch 24 → 25 taken 5 times.
✓ Branch 24 → 27 taken 12 times.
17 if (GetLastError() == ERROR_ALREADY_EXISTS)
350 {
351 5 CloseHandle(handle);
352 5 return MutexAcquire::AlreadyHeld;
353 }
354 12 out = handle;
355 12 return MutexAcquire::Acquired;
356 }
357
358 [[nodiscard]] Result<HANDLE>
359 1663 begin_session(const ModInfo &info, const char *operation, detail::LoaderContext loader_context) noexcept
360 {
361
2/2
✓ Branch 4 → 5 taken 3 times.
✓ Branch 4 → 8 taken 1660 times.
1663 if (!detail::lifecycle().begin_start())
362 {
363 3 return std::unexpected(Error{ErrorCode::SessionAlreadyActive, operation});
364 }
365
366 1660 Result<bool> target_process = is_target_process(info.game_process_name, operation);
367
1/2
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 17 taken 1660 times.
1660 if (!target_process)
368 {
369 ✗ detail::lifecycle().mark_stopped();
370 ✗ return std::unexpected(target_process.error());
371 }
372
2/2
✓ Branch 18 → 19 taken 6 times.
✓ Branch 18 → 24 taken 1654 times.
1660 if (!*target_process)
373 {
374 6 detail::lifecycle().mark_stopped();
375 6 return std::unexpected(Error{ErrorCode::ProcessMismatch, operation});
376 }
377
378 1654 HANDLE mutex = nullptr;
379 1654 DWORD error = 0;
380
2/4
✓ Branch 25 → 26 taken 5 times.
✗ Branch 25 → 31 not taken.
✓ Branch 25 → 36 taken 1649 times.
✗ Branch 25 → 37 not taken.
1654 switch (acquire_instance_mutex(info.instance_mutex_prefix, mutex, error))
381 {
382 5 case MutexAcquire::AlreadyHeld:
383 5 detail::lifecycle().mark_stopped();
384 5 return std::unexpected(Error{ErrorCode::InstanceAlreadyRunning, operation});
385 ✗ case MutexAcquire::SystemError:
386 ✗ detail::lifecycle().mark_stopped();
387 ✗ return std::unexpected(Error{ErrorCode::SystemCallFailed, operation, error});
388 1649 case MutexAcquire::NoGuard:
389 case MutexAcquire::Acquired:
390 1649 break;
391 }
392
393 // This identity takes no reference. The bootstrap worker acquires its separate keepalive before
394 // CreateThread.
395 1649 constexpr DWORD CAPTURE_FLAGS =
396 GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT;
397 1649 HMODULE captured_module = nullptr;
398
1/2
✗ Branch 38 → 39 not taken.
✓ Branch 38 → 47 taken 1649 times.
1649 if (!GetModuleHandleExW(CAPTURE_FLAGS, reinterpret_cast<LPCWSTR>(&begin_session), &captured_module))
399 {
400 ✗ error = GetLastError();
401
0/2
✗ Branch 40 → 41 not taken.
✗ Branch 40 → 42 not taken.
✗ if (mutex != nullptr)
402 {
403 ✗ CloseHandle(mutex);
404 }
405 ✗ detail::lifecycle().mark_stopped();
406 ✗ return std::unexpected(Error{ErrorCode::SystemCallFailed, operation, error});
407 }
408 1649 detail::lifecycle().publish_module(captured_module);
409
410 // Publish the phase only once the fallible gates have passed. begin_start() already reset the context to
411 // Normal for this epoch, so every rollback above leaves a neutral phase behind rather than stranding a
412 // non-blocking Attach that would fail-close every later teardown in a process that never started.
413 1649 detail::lifecycle().set_loader_context(loader_context);
414 1649 return mutex;
415 }
416
417 /**
418 * @brief Applies the subsystem order in the Session contract.
419 * @details SessionTeardown.FullStackTeardownShutsEveryLeafDown proves leaf completion.
420 * Lifecycle.DiagnosticsTlsIndexReturnsWithTheSession pins the diagnostics release after config::clear.
421 */
422 1637 void run_subsystem_teardown() noexcept
423 {
424 // First: the watcher thread can fire on_reload into any state below.
425 1637 config::disable_auto_reload();
426 1637 input::Input::instance().shutdown();
427 1637 memory::shutdown_cache();
428 1637 config::clear();
429 // After config::clear, because a config callback can own a subscription.
430 1637 detail::release_diagnostics_emit_owners(
431 detail::DiagnosticsTeardown::Session,
432 1637 detail::blocking_teardown_permitted()
433 );
434 // Last: every prior step can still log.
435 1637 log().shutdown();
436 1637 }
437
438 /**
439 * @brief Rolls a pre-publication attach back to a retryable Drained slot.
440 * @param instance_mutex The preflight mutex, or nullptr when no guard was requested.
441 * @details No Session or callback has been published when this runs. Closing the local mutex releases the
442 * instance gate without subsystem teardown or consumer destruction in DllMain.
443 */
444 3 void unwind_bootstrap(HANDLE instance_mutex) noexcept
445 {
446
2/2
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 5 taken 1 time.
3 if (abandon_shutdown_event())
447 {
448 2 diagnostics::record_intentional_leak(diagnostics::LeakSubsystem::Bootstrap);
449 }
450
2/2
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 2 times.
3 if (instance_mutex != nullptr)
451 {
452 1 CloseHandle(instance_mutex);
453 }
454 3 s_bootstrap_logger_info.clear();
455 3 detail::lifecycle().clear_module();
456 // Retire the Attach phase with the attach it described. Leaving it published would fail-close every later
457 // teardown in a process whose consumer declined the load but kept using the library.
458 3 detail::lifecycle().set_loader_context(detail::LoaderContext::Normal);
459 3 detail::lifecycle().mark_stopped();
460 3 s_bootstrap_state.store(BootstrapState::Drained, std::memory_order_release);
461 3 }
462
463 46 void retire_bootstrap_after_drain() noexcept
464 {
465 // Idempotent after the worker's own clear. It stays because this is the single retirement point for a
466 // caller that arrives without a completed join. LifecycleContext::clear_worker_thread owns the hazard.
467 46 detail::lifecycle().clear_worker_thread();
468 46 retire_shutdown_event_after_drain();
469 46 s_on_ready = nullptr;
470 46 s_on_ready_fn = nullptr;
471 46 s_bootstrap_logger_info.clear();
472 46 detail::lifecycle().clear_module();
473 // Retire the drain phase with everything else this drain retires, so the published word keeps describing
474 // the phase the process is actually in. Both Normal and ExplicitDrain authorize blocking, so this is a
475 // consistency store rather than a change of permission.
476 46 detail::lifecycle().set_loader_context(detail::LoaderContext::Normal);
477 46 }
478
479 /**
480 * @brief Retires the worker identity and releases its module reference on thread exit.
481 * @details FreeLibraryAndExitThread cannot return through code that its final reference release unmaps.
482 */
483 49 [[noreturn]] void exit_bootstrap_worker(HMODULE self_ref) noexcept
484 {
485 49 detail::lifecycle().clear_worker_thread();
486 49 detail::module_pin_observability::note_released(diagnostics::ModulePinReason::Bootstrap);
487 49 FreeLibraryAndExitThread(self_ref, 0);
488 }
489
490 // The bootstrap worker. It finishes Session setup, runs on_ready, and performs teardown off the loader lock.
491 52 DWORD WINAPI lifecycle_thread(LPVOID param) noexcept
492 {
493 52 const HMODULE self_ref = static_cast<HMODULE>(param);
494
495 // Publish this thread's identity FIRST, before any consumer code can run on it. Collecting it from
496 // CreateThread's out-parameter instead would leave a window in which on_ready is already executing while
497 // the id is still 0, and a drain requested from inside on_ready would then slip past the self-drain guard
498 // and report success for a session that is fully live. Publishing here closes that window without
499 // resuming a thread from under the loader lock. The same identity authorizes this thread's teardown to
500 // block regardless of the phase the DllMain thread publishes.
501 52 detail::lifecycle().publish_worker_thread();
502
503 // bootstrap_core publishes the thread handle before the Session and callback.
504 // An off-loader-lock rich bootstrap requires this short publication wait.
505 52 BootstrapState state = s_bootstrap_state.load(std::memory_order_acquire);
506
1/2
✗ Branch 8 → 6 not taken.
✓ Branch 8 → 9 taken 52 times.
52 while (state == BootstrapState::Starting)
507 {
508 ✗ SwitchToThread();
509 ✗ state = s_bootstrap_state.load(std::memory_order_acquire);
510 }
511
512 // A control thread may claim Ready -> Draining before this thread observes Ready. Draining still owns a
513 // fully published Session, callback, and shutdown event; adopt them and let the already-signalled event
514 // drive the ordinary teardown so the waiting control thread can complete the drain.
515 // An empty optional is unreachable. bootstrap_core stages the Session before its Ready release store, and
516 // every state the spin can observe follows that store. Guard anyway.
517
5/8
✓ Branch 9 → 10 taken 13 times.
✓ Branch 9 → 11 taken 39 times.
✓ Branch 10 → 11 taken 13 times.
✗ Branch 10 → 13 not taken.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 52 times.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 52 times.
52 if ((state != BootstrapState::Ready && state != BootstrapState::Draining) || !s_pending_session)
518 {
519 ✗ exit_bootstrap_worker(self_ref);
520 }
521
522 #if defined(DMK_ENABLE_TEST_SEAMS)
523
2/2
✓ Branch 17 → 18 taken 1 time.
✓ Branch 17 → 19 taken 51 times.
52 if (s_bootstrap_pre_setup_probe != nullptr)
524 {
525 1 s_bootstrap_pre_setup_probe();
526 }
527 #endif
528
529 // A thread created from DllMain cannot execute its entry point until the loader releases the attach
530 // notification. Publishing Normal retires the Attach phase before logger/file setup begins.
531 52 detail::lifecycle().set_loader_context(detail::LoaderContext::Normal);
532
533 {
534 104 Session session = std::move(*s_pending_session);
535 52 s_pending_session.reset();
536
537 try
538 {
539
1/2
✓ Branch 28 → 29 taken 52 times.
✗ Branch 28 → 60 not taken.
52 Logger::configure(
540 s_bootstrap_logger_info.name_view(),
541 s_bootstrap_logger_info.log_file_view(),
542 DEFAULT_TIMESTAMP_FORMAT,
543 s_bootstrap_logger_info.log_open_mode,
544 s_bootstrap_logger_info.log_source_stamp_mode
545 );
546 52 DetourModKit::log().enable_async_mode(s_bootstrap_logger_info.logger_config());
547 }
548
0/2
✗ Branch 60 → 61 not taken.
✗ Branch 60 → 64 not taken.
✗ catch (const std::bad_alloc &)
549 {
550 ✗ OutputDebugStringA(
551 "DetourModKit: bootstrap logger setup ran out of memory; continuing without "
552 "guaranteed logging.\n"
553 );
554 ✗ }
555 ✗ catch (...)
556 {
557 ✗ OutputDebugStringA(
558 "DetourModKit: bootstrap logger setup failed; continuing without guaranteed "
559 "logging.\n"
560 );
561 ✗ }
562 #if !defined(_MSC_VER) && defined(_WIN64)
563 52 detail::reopen_guarded_engine();
564 #endif
565 52 detail::lifecycle().mark_running();
566
567
4/6
✓ Branch 37 → 38 taken 36 times.
✓ Branch 37 → 40 taken 16 times.
✓ Branch 39 → 40 taken 36 times.
✗ Branch 39 → 41 not taken.
✓ Branch 42 → 43 taken 52 times.
✗ Branch 42 → 55 not taken.
52 if (const BootstrapReadyFn ready_fn = s_on_ready_fn; ready_fn != nullptr || s_on_ready)
568 {
569 try
570 {
571
5/6
✓ Branch 43 → 44 taken 16 times.
✓ Branch 43 → 45 taken 36 times.
✓ Branch 44 → 46 taken 16 times.
✗ Branch 44 → 68 not taken.
✓ Branch 45 → 46 taken 35 times.
✓ Branch 45 → 68 taken 1 time.
52 Result<void> ready = ready_fn != nullptr ? ready_fn(session) : s_on_ready(session);
572
2/2
✓ Branch 47 → 48 taken 1 time.
✓ Branch 47 → 54 taken 50 times.
51 if (!ready)
573 {
574 2 (void)log().try_log(
575 LogLevel::Error,
576 "bootstrap: on_ready reported failure: {}",
577
1/2
✓ Branch 50 → 51 taken 1 time.
✗ Branch 50 → 67 not taken.
2 ready.error().message()
578 );
579 }
580 }
581
1/2
✓ Branch 69 → 70 taken 1 time.
✗ Branch 69 → 75 not taken.
1 catch (const std::exception &e)
582 {
583 1 (void)log().try_log(LogLevel::Error, "bootstrap: on_ready threw: {}", e.what());
584 1 }
585 ✗ catch (...)
586 {
587 ✗ (void)log().try_log(LogLevel::Error, "bootstrap: on_ready threw an unknown exception.");
588 ✗ }
589 }
590
591
1/2
✓ Branch 56 → 57 taken 52 times.
✗ Branch 56 → 58 not taken.
52 if (HANDLE event = s_shutdown_event.load(std::memory_order_acquire))
592 {
593 52 WaitForSingleObject(event, INFINITE);
594 }
595
596 // `session` destructs at the end of this inner scope -> ordered teardown off the loader lock -> leaves
597 // JOIN, all while self_ref keeps this module's code mapped through the teardown.
598 49 }
599
600 // The identity retires only after the teardown it authorized.
601 49 exit_bootstrap_worker(self_ref);
602 }
603
604 #if defined(DMK_ENABLE_TEST_SEAMS)
605 // Forces worker launch to fail after process and instance gating but before consumer state is published.
606 std::atomic<bool> s_fail_worker_launch{false};
607 #endif
608
609 // Creates the bootstrap worker. Routed through one function so the test seam fails into CreateThread's own
610 // rollback branch rather than duplicating it.
611 54 [[nodiscard]] HANDLE launch_bootstrap_worker(HMODULE worker_ref) noexcept
612 {
613 #if defined(DMK_ENABLE_TEST_SEAMS)
614
2/2
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 6 taken 52 times.
54 if (s_fail_worker_launch.load(std::memory_order_acquire))
615 {
616 2 SetLastError(ERROR_NOT_ENOUGH_MEMORY);
617 2 return nullptr;
618 }
619 #endif
620 52 return CreateThread(nullptr, 0, lifecycle_thread, worker_ref, 0, nullptr);
621 }
622
623 // A generation publishes one callback representation.
624 [[nodiscard]] Result<void>
625 63 bootstrap_core(const ModInfo &info, ReadyCallback *rich_on_ready, BootstrapReadyFn fn_on_ready) noexcept
626 {
627 // Claim the slot before touching any other static. A drain nulls the worker handle and the shutdown event
628 // early but publishes Drained only after it has also retired the init callback and the module identity, so
629 // admitting on those handles would let this generation publish into the tail of that retirement and have
630 // its own callback destroyed and its own Ready overwritten by the drainer that is still finishing.
631 63 BootstrapState expected = BootstrapState::Drained;
632
2/2
✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 15 taken 59 times.
63 if (!s_bootstrap_state
633 63 .compare_exchange_strong(expected, BootstrapState::Starting, std::memory_order_acq_rel))
634 {
635
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 8 taken 3 times.
4 if (expected == BootstrapState::Draining)
636 {
637 1 return std::unexpected(Error{ErrorCode::SessionShutdownInProgress, "bootstrap"});
638 }
639
2/2
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 12 taken 2 times.
3 if (expected == BootstrapState::Detached)
640 {
641 1 return std::unexpected(Error{ErrorCode::SessionShutdownUnavailable, "bootstrap"});
642 }
643 // Starting or Ready: another generation owns the slot.
644 2 return std::unexpected(Error{ErrorCode::SessionAlreadyActive, "bootstrap"});
645 }
646
647 // A retirement that had to retain its event leaves a previous generation's signaler still inside SetEvent.
648 // Its pending fetch_sub would underflow the access word this generation reopens, so refuse until that
649 // signaler has left rather than start on a corrupted admission count. Checked here as well as at the
650 // reopen so a doomed attach costs nothing: the reopen below is the authority.
651
1/2
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 27 taken 59 times.
59 if (s_shutdown_event_access.load(std::memory_order_acquire) != SHUTDOWN_EVENT_RETIRED)
652 {
653 ✗ s_bootstrap_state.store(BootstrapState::Drained, std::memory_order_release);
654 ✗ return std::unexpected(Error{ErrorCode::SessionShutdownInProgress, "bootstrap"});
655 }
656
657 // Keep the gates that let DllMain decline the load synchronous, but defer logger/file setup to the worker.
658 59 Result<HANDLE> instance_mutex = begin_session(info, "bootstrap", detail::LoaderContext::Attach);
659
2/2
✓ Branch 29 → 30 taken 4 times.
✓ Branch 29 → 35 taken 55 times.
59 if (!instance_mutex)
660 {
661 4 s_bootstrap_state.store(BootstrapState::Drained, std::memory_order_release);
662 4 return std::unexpected(instance_mutex.error());
663 }
664 // begin_session publishes the DllMain module identity without a consumer-supplied handle.
665 55 (void)DisableThreadLibraryCalls(detail::lifecycle().module());
666
667
2/2
✓ Branch 39 → 40 taken 1 time.
✓ Branch 39 → 45 taken 54 times.
55 if (!s_bootstrap_logger_info.stage(info))
668 {
669 1 unwind_bootstrap(*instance_mutex);
670 1 return std::unexpected(Error{ErrorCode::InvalidArg, "bootstrap"});
671 }
672
673 // Create into a local first, then publish with a release store so the worker's / consumer's acquire load
674 // observes a fully-constructed handle. Owning Starting is what makes the slot free to publish into. The
675 // TRUE second argument makes this a MANUAL-RESET event: a shutdown request is a one-way latch, so once
676 // request_shutdown() signals it the event stays signaled. The worker observes it whether or not it was
677 // already waiting, and a repeated request_shutdown() is idempotent (an auto-reset event would clear itself
678 // after a single wait woke and could drop a later observer).
679 54 const HANDLE shutdown_event = CreateEventW(nullptr, TRUE, FALSE, nullptr);
680
1/2
✗ Branch 46 → 47 not taken.
✓ Branch 46 → 53 taken 54 times.
54 if (!shutdown_event)
681 {
682 ✗ const DWORD err = GetLastError();
683 ✗ unwind_bootstrap(*instance_mutex);
684 ✗ return std::unexpected(Error{ErrorCode::SystemCallFailed, "bootstrap", err});
685 }
686 // Reopen admission by CLAIMING the retired word, never by storing over it: a straggling signaler from the
687 // retired generation can still be counted, and a blind store would discard its registration and leave its
688 // pending decrement to underflow the word. Reopening before the handle is published also keeps a signaler
689 // admitted after this point from ever loading a half-published pointer.
690 54 std::uint64_t retired_access = SHUTDOWN_EVENT_RETIRED;
691 54 if (!s_shutdown_event_access
692
1/2
✗ Branch 58 → 59 not taken.
✓ Branch 58 → 65 taken 54 times.
54 .compare_exchange_strong(retired_access, 0, std::memory_order_acq_rel, std::memory_order_acquire))
693 {
694 ✗ CloseHandle(shutdown_event);
695 ✗ unwind_bootstrap(*instance_mutex);
696 ✗ return std::unexpected(Error{ErrorCode::SessionShutdownInProgress, "bootstrap"});
697 }
698 54 s_shutdown_event.store(shutdown_event, std::memory_order_release);
699
700 // Acquire the worker's module reference BEFORE CreateThread. A thread created from DllMain may not execute
701 // its entry point until after the loader releases the attach notification, but the caller can FreeLibrary
702 // immediately after LoadLibrary returns. The reference therefore has to exist before the worker is
703 // scheduled, not at the top of the worker function.
704 54 const HMODULE worker_ref = detail::try_acquire_module_ref(diagnostics::ModulePinReason::Bootstrap);
705
1/2
✗ Branch 67 → 68 not taken.
✓ Branch 67 → 74 taken 54 times.
54 if (worker_ref == nullptr)
706 {
707 ✗ const DWORD err = GetLastError();
708 ✗ unwind_bootstrap(*instance_mutex);
709 ✗ return std::unexpected(Error{ErrorCode::SystemCallFailed, "bootstrap", err});
710 }
711
712 // The worker publishes its own thread id as its first instruction, so this call does not collect it. A
713 // CREATE_SUSPENDED plus ResumeThread is not leaf-safe inside DllMain.
714 54 s_worker_thread = launch_bootstrap_worker(worker_ref);
715
2/2
✓ Branch 75 → 76 taken 2 times.
✓ Branch 75 → 83 taken 52 times.
54 if (!s_worker_thread)
716 {
717 2 const DWORD err = GetLastError();
718 2 detail::release_module_ref(worker_ref, diagnostics::ModulePinReason::Bootstrap);
719 2 unwind_bootstrap(*instance_mutex);
720 2 return std::unexpected(Error{ErrorCode::SystemCallFailed, "bootstrap", err});
721 }
722
723 // No fallible operation remains. Stage the Session and callback, then release the worker from its Starting
724 // wait. This ordering also keeps a launch failure from destroying consumer state or shutting subsystems
725 // down inside DllMain.
726 52 s_pending_session.emplace(detail::SessionBootstrapAccess::make(*instance_mutex));
727
2/2
✓ Branch 87 → 88 taken 36 times.
✓ Branch 87 → 91 taken 16 times.
52 if (rich_on_ready != nullptr)
728 {
729 36 s_on_ready = std::move(*rich_on_ready);
730 }
731 52 s_on_ready_fn = fn_on_ready;
732 52 s_bootstrap_state.store(BootstrapState::Ready, std::memory_order_release);
733 52 return {};
734 }
735
736 } // anonymous namespace
737
738 1644 Session::Session(void *instance_mutex) noexcept : m_instance_mutex(instance_mutex), m_active(true) {}
739
740 3274 Session::Session(Session &&other) noexcept
741 6548 : m_scope(std::move(other.m_scope)), m_instance_mutex(other.m_instance_mutex), m_active(other.m_active)
742 {
743 // Leave the source inert so its destructor does nothing: exactly one Session ever carries the live teardown.
744 3274 other.m_instance_mutex = nullptr;
745 3274 other.m_active = false;
746 3274 }
747
748 1 Session &Session::operator=(Session &&other) noexcept
749 {
750
1/2
✓ Branch 2 → 3 taken 1 time.
✗ Branch 2 → 8 not taken.
1 if (this != &other)
751 {
752 // Assigning over a live Session ends it: run the full ordered teardown of THIS session first (which also
753 // clears the single-session guard), then adopt the source. A mod holds one session at a time, so the source
754 // is always inert here; a no-op release() when this Session is already inert makes the common
755 // reassign-a-moved-from-Session case free.
756 1 release();
757 2 m_scope = std::move(other.m_scope);
758 1 m_instance_mutex = other.m_instance_mutex;
759 1 m_active = other.m_active;
760 1 other.m_instance_mutex = nullptr;
761 1 other.m_active = false;
762 }
763 1 return *this;
764 }
765
766 4913 Session::~Session() noexcept
767 {
768 // Moved-from or abandon()ed sessions are inert, so release() is a no-op and a double-drop never
769 // double-tears-down.
770 4913 release();
771 4913 }
772
773 4914 void Session::release() noexcept
774 {
775 // The Session class `[B-100]` warning owns the caller precondition.
776
2/2
✓ Branch 2 → 3 taken 3277 times.
✓ Branch 2 → 4 taken 1637 times.
4914 if (!m_active)
777 {
778 3277 return;
779 }
780
781 // Enter Stopping so a start racing this teardown stays rejected until the slot is fully released.
782 1637 detail::lifecycle().begin_stop();
783 1637 m_scope.clear();
784 1637 run_subsystem_teardown();
785
2/2
✓ Branch 8 → 9 taken 11 times.
✓ Branch 8 → 11 taken 1626 times.
1637 if (m_instance_mutex)
786 {
787 11 CloseHandle(static_cast<HANDLE>(m_instance_mutex));
788 11 m_instance_mutex = nullptr;
789 }
790 1637 m_active = false;
791 // The identity retires with the session, after every teardown callback that can still read it.
792 1637 detail::lifecycle().clear_module();
793 1637 detail::lifecycle().mark_stopped();
794 }
795
796 1604 Result<Session> Session::start(const ModInfo &info) noexcept
797 {
798 1604 Result<HANDLE> instance_mutex = begin_session(info, "Session::start", detail::LoaderContext::Normal);
799
2/2
✓ Branch 4 → 5 taken 10 times.
✓ Branch 4 → 9 taken 1594 times.
1604 if (!instance_mutex)
800 {
801 10 return std::unexpected(instance_mutex.error());
802 }
803
804 // Logger::configure can throw while it builds owned strings. A std::bad_alloc maps to OutOfMemory.
805 // Every other exception maps to Unknown because a retry cannot clear an arbitrary fault.
806 // enable_async_mode contains its failures, so this catch owns only configuration.
807 try
808 {
809 #if defined(DMK_ENABLE_TEST_SEAMS)
810
2/2
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 11 taken 1593 times.
1594 if (const auto probe = detail::g_session_configure_test_hook)
811
1/2
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 24 taken 1 time.
1 probe();
812 #endif
813 1593 Logger::configure(
814 info.name,
815 info.log_file,
816 DEFAULT_TIMESTAMP_FORMAT,
817
2/2
✓ Branch 11 → 12 taken 1592 times.
✓ Branch 11 → 24 taken 1 time.
1593 info.log_open_mode,
818 info.log_source_stamp_mode
819 );
820 // Qualified: inside this static member the free accessor is hidden by the non-static Session::log().
821 1592 DetourModKit::log().enable_async_mode(info.log);
822 }
823
2/2
✓ Branch 24 → 25 taken 1 time.
✓ Branch 24 → 37 taken 1 time.
2 catch (const std::bad_alloc &)
824 {
825
1/2
✗ Branch 27 → 28 not taken.
✓ Branch 27 → 30 taken 1 time.
1 if (*instance_mutex != nullptr)
826 {
827 ✗ CloseHandle(*instance_mutex);
828 }
829 1 detail::lifecycle().clear_module();
830 1 detail::lifecycle().mark_stopped();
831 1 return std::unexpected(Error{ErrorCode::OutOfMemory, "Session::start"});
832 1 }
833 1 catch (...)
834 {
835
1/2
✗ Branch 39 → 40 not taken.
✓ Branch 39 → 42 taken 1 time.
1 if (*instance_mutex != nullptr)
836 {
837 ✗ CloseHandle(*instance_mutex);
838 }
839 1 detail::lifecycle().clear_module();
840 1 detail::lifecycle().mark_stopped();
841 1 return std::unexpected(Error{ErrorCode::Unknown, "Session::start"});
842 1 }
843
844 // Setup succeeded. The session now owns the single-session slot and mutex handle until release().
845 #if !defined(_MSC_VER) && defined(_WIN64)
846 1592 detail::reopen_guarded_engine();
847 #endif
848 1592 detail::lifecycle().mark_running();
849 1592 return Session(*instance_mutex);
850 }
851
852 14 Logger &Session::log() const noexcept
853 {
854 // Qualified: the member Session::log() would otherwise hide the free accessor and recurse.
855 14 return DetourModKit::log();
856 }
857
858 29 config::Ini Session::ini() const noexcept
859 {
860 29 return config::Ini{};
861 }
862
863 21 input::Input &Session::input() const noexcept
864 {
865 21 return input::Input::instance();
866 }
867
868 301 input::Scope &Session::scope() noexcept
869 {
870 301 return m_scope;
871 }
872
873 4 void Session::abandon() noexcept
874 {
875 // abandon() in session.hpp owns the process-termination-only rule. m_scope is a member whose own destructor
876 // still runs after this Session, so abandon it explicitly. Scope::abandon retains the guard container, so
877 // neither release logic nor consumer callback destruction can run during process detach.
878 4 m_scope.abandon();
879 4 m_active = false;
880 4 m_instance_mutex = nullptr;
881 4 detail::lifecycle().clear_module();
882 4 detail::lifecycle().mark_stopped();
883 4 }
884
885 // Bootstrap free functions
886
887 23 Result<void> bootstrap_attach(const ModInfo &info, BootstrapReadyFn on_ready) noexcept
888 {
889 23 return bootstrap_core(info, nullptr, on_ready);
890 }
891
892 40 Result<void> bootstrap(const ModInfo &info, std::move_only_function<Result<void>(Session &)> on_ready) noexcept
893 {
894 40 return bootstrap_core(info, &on_ready, nullptr);
895 }
896
897 8 void bootstrap_detach(void *reserved) noexcept
898 {
899 // This entry point is called only from DllMain. lpReserved distinguishes process termination from an explicit
900 // unload. Publish that context even when a prior drain already retired the handles, so later CRT destructors
901 // cannot inherit ExplicitDrain and treat a false heuristic result as permission to block under the loader lock.
902 8 const detail::LoaderContext context =
903
2/2
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 4 taken 6 times.
8 reserved != nullptr ? detail::LoaderContext::ProcessExit : detail::LoaderContext::LoaderDetach;
904 8 detail::lifecycle().set_loader_context(context);
905
906
2/2
✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 26 taken 6 times.
8 if (context == detail::LoaderContext::ProcessExit)
907 {
908 const BootstrapState previous =
909 2 s_bootstrap_state.exchange(BootstrapState::Detached, std::memory_order_acq_rel);
910
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 2 times.
2 if (previous == BootstrapState::Detached)
911 {
912 5 return;
913 }
914
915 // PROCESS TERMINATION (abandon). The OS has already killed the worker; its adopted Session lives in that
916 // dead frame and is leaked untouched. If the worker never adopted the pending Session, leave it engaged in
917 // the never-destroyed storage too. Neither path runs consumer destruction inside DllMain.
918
1/2
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 15 taken 2 times.
2 if (s_pending_session)
919 {
920 ✗ s_pending_session->abandon();
921 }
922 2 detail::lifecycle().clear_worker_thread();
923
1/2
✓ Branch 17 → 18 taken 2 times.
✗ Branch 17 → 20 not taken.
2 if (s_worker_thread)
924 {
925 2 CloseHandle(s_worker_thread);
926 2 s_worker_thread = nullptr;
927 }
928 // Process termination: the OS has already terminated every other thread before this DllMain notification,
929 // so no request_shutdown() can be in flight and closing the event is safe.
930 2 close_shutdown_event_at_process_exit();
931 2 detail::lifecycle().clear_module();
932
1/2
✓ Branch 23 → 24 taken 2 times.
✗ Branch 23 → 25 not taken.
2 if (previous != BootstrapState::Drained)
933 {
934 2 diagnostics::record_intentional_leak(diagnostics::LeakSubsystem::Bootstrap);
935 }
936 2 return;
937 }
938
939 6 BootstrapState expected = BootstrapState::Ready;
940
2/2
✓ Branch 27 → 28 taken 3 times.
✓ Branch 27 → 31 taken 3 times.
6 if (!s_bootstrap_state.compare_exchange_strong(expected, BootstrapState::Detached, std::memory_order_acq_rel))
941 {
942 // A drained unload has no handles left. Only its terminal transition remains.
943
1/2
✓ Branch 28 → 29 taken 3 times.
✗ Branch 28 → 30 not taken.
3 if (expected == BootstrapState::Drained)
944 {
945 (void)s_bootstrap_state
946 3 .compare_exchange_strong(expected, BootstrapState::Detached, std::memory_order_acq_rel);
947 }
948 3 return;
949 }
950
951 // EXPLICIT FreeLibrary. Signal the worker, then stop admitting signalers and retain the event rather than
952 // waiting for an already-admitted request_shutdown() call. The worker's counted module reference keeps its code
953 // mapped until it exits; this path never waits or destroys callback state under the loader lock.
954 3 signal_shutdown_event();
955
1/2
✓ Branch 33 → 34 taken 3 times.
✗ Branch 33 → 35 not taken.
3 if (abandon_shutdown_event())
956 {
957 3 diagnostics::record_intentional_leak(diagnostics::LeakSubsystem::Bootstrap);
958 }
959 // The published Detached state is terminal, so no drain, attach, or later detach can still read the worker's
960 // thread handle. Closing it does not disturb the running worker and keeps a repeated load/unload cycle from
961 // leaking one kernel thread object per load.
962
1/2
✓ Branch 35 → 36 taken 3 times.
✗ Branch 35 → 38 not taken.
3 if (s_worker_thread)
963 {
964 3 CloseHandle(s_worker_thread);
965 3 s_worker_thread = nullptr;
966 }
967 3 detail::lifecycle().clear_module();
968 }
969
970 9461768 void request_shutdown() noexcept
971 {
972 // The access word admits this caller before it loads the handle. A clean drain closes the handle only after
973 // closing admission and observing every admitted caller leave SetEvent.
974 9461768 signal_shutdown_event();
975 9715042 }
976
977 51 Result<void> shutdown_and_wait() noexcept
978 {
979 // Refuse a self-drain before touching the state machine, so a worker-thread caller perturbs nothing and an
980 // ordinary control thread can still drain afterwards. Waiting here would block on the calling thread's own
981 // exit, which only that wait prevents.
982
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 8 taken 50 times.
51 if (detail::lifecycle().is_worker_thread())
983 {
984 1 return std::unexpected(Error{ErrorCode::SessionShutdownWouldBlock, "shutdown_and_wait"});
985 }
986
987 50 BootstrapState expected = BootstrapState::Ready;
988
2/2
✓ Branch 9 → 10 taken 3 times.
✓ Branch 9 → 19 taken 47 times.
50 if (!s_bootstrap_state.compare_exchange_strong(expected, BootstrapState::Draining, std::memory_order_acq_rel))
989 {
990
2/2
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 2 times.
3 if (expected == BootstrapState::Drained)
991 {
992 1 return {};
993 }
994
2/2
✓ Branch 12 → 13 taken 1 time.
✓ Branch 12 → 16 taken 1 time.
2 if (expected == BootstrapState::Detached)
995 {
996 1 return std::unexpected(Error{ErrorCode::SessionShutdownUnavailable, "shutdown_and_wait"});
997 }
998 1 return std::unexpected(Error{ErrorCode::SessionShutdownInProgress, "shutdown_and_wait"});
999 }
1000
1001 // LoaderContext describes the phase, not the current thread's lock ownership.
1002 // Only the per-thread probe decides whether this caller can wait.
1003
2/2
✓ Branch 20 → 21 taken 1 time.
✓ Branch 20 → 25 taken 46 times.
47 if (detail::is_loader_lock_held())
1004 {
1005 1 s_bootstrap_state.store(BootstrapState::Ready, std::memory_order_release);
1006 1 return std::unexpected(Error{ErrorCode::SessionShutdownWouldBlock, "shutdown_and_wait"});
1007 }
1008
1009 46 detail::lifecycle().set_loader_context(detail::LoaderContext::ExplicitDrain);
1010 46 request_shutdown();
1011
1012
1/2
✓ Branch 28 → 29 taken 46 times.
✗ Branch 28 → 43 not taken.
46 if (s_worker_thread != nullptr)
1013 {
1014 46 const DWORD wait_result = WaitForSingleObject(s_worker_thread, INFINITE);
1015
1/2
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 41 taken 46 times.
46 if (wait_result != WAIT_OBJECT_0)
1016 {
1017
0/2
✗ Branch 31 → 32 not taken.
✗ Branch 31 → 34 not taken.
✗ const DWORD error = wait_result == WAIT_FAILED ? GetLastError() : ERROR_GEN_FAILURE;
1018 // The shutdown request is already latched, so the worker drains regardless of this failure; only the
1019 // slot is restored, for a caller that wants to retry the wait. Retire the drain phase with the drain
1020 // this caller no longer owns. The worker's own authorization comes from its identity, not this word.
1021 ✗ detail::lifecycle().set_loader_context(detail::LoaderContext::Normal);
1022 ✗ s_bootstrap_state.store(BootstrapState::Ready, std::memory_order_release);
1023 ✗ return std::unexpected(Error{ErrorCode::SystemCallFailed, "shutdown_and_wait", error});
1024 }
1025 46 CloseHandle(s_worker_thread);
1026 46 s_worker_thread = nullptr;
1027 }
1028
1029 46 retire_bootstrap_after_drain();
1030 46 s_bootstrap_state.store(BootstrapState::Drained, std::memory_order_release);
1031 46 return {};
1032 }
1033
1034 11112223 ModuleHandle module_handle() noexcept
1035 {
1036 11112223 return detail::lifecycle().module();
1037 }
1038
1039 #if defined(DMK_ENABLE_TEST_SEAMS)
1040 // Test-only accessor for the bootstrap shutdown event handle. A test captures it before a synchronous drain and
1041 // confirms the handle closes only after racing request_shutdown() callers have left SetEvent. Not declared in a
1042 // public header; the test extern-declares it like the loader-lock override seams.
1043 5 HANDLE bootstrap_shutdown_event_for_test() noexcept
1044 {
1045 5 return s_shutdown_event.load(std::memory_order_acquire);
1046 }
1047
1048 // Arms or disarms the worker-launch failure. Set it around one bootstrap entry call only.
1049 4 void bootstrap_fail_worker_launch_for_test(bool fail) noexcept
1050 {
1051 4 s_fail_worker_launch.store(fail, std::memory_order_release);
1052 4 }
1053
1054 3 void bootstrap_pre_setup_probe_for_test(void (*probe)() noexcept) noexcept
1055 {
1056 3 s_bootstrap_pre_setup_probe = probe;
1057 3 }
1058
1059 // How many signals reached SetEvent on an already-invalidated handle. Monotonic across the process, so a case
1060 // brackets its own window with two reads.
1061 2 std::uint64_t bootstrap_signals_on_invalid_event_for_test() noexcept
1062 {
1063 2 return s_signal_on_invalid_event.load(std::memory_order_relaxed);
1064 }
1065 #endif
1066
1067 namespace
1068 {
1069 /**
1070 * @brief Runs both halves of the [B-74] transaction under one deadline.
1071 */
1072 template <typename DrainInput>
1073 requires std::is_nothrow_invocable_r_v<input::CallbackDrainStatus, DrainInput, std::chrono::milliseconds>
1074 [[nodiscard]] LogicDllUnloadStatus
1075 1565 run_logic_dll_unload(std::chrono::milliseconds timeout, DrainInput drain_input) noexcept
1076 {
1077
3/4
DetourModKit::LogicDllUnloadStatus DetourModKit::(anonymous namespace)::run_logic_dll_unload<DetourModKit::prepare_logic_dll_unload(std::span<std::basic_string_view<char, std::char_traits<char> > const, 18446744073709551615ull>, std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}>(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >, DetourModKit::prepare_logic_dll_unload(std::span<std::basic_string_view<char, std::char_traits<char> > const, 18446744073709551615ull>, std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}):
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 17 times.
DetourModKit::LogicDllUnloadStatus DetourModKit::(anonymous namespace)::run_logic_dll_unload<DetourModKit::prepare_logic_dll_unload_all(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}>(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >, DetourModKit::prepare_logic_dll_unload_all(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 1547 times.
1565 if (!detail::blocking_teardown_permitted())
1078 {
1079 1 return LogicDllUnloadStatus::LoaderLock;
1080 }
1081
3/4
DetourModKit::LogicDllUnloadStatus DetourModKit::(anonymous namespace)::run_logic_dll_unload<DetourModKit::prepare_logic_dll_unload(std::span<std::basic_string_view<char, std::char_traits<char> > const, 18446744073709551615ull>, std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}>(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >, DetourModKit::prepare_logic_dll_unload(std::span<std::basic_string_view<char, std::char_traits<char> > const, 18446744073709551615ull>, std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}):
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 8 taken 16 times.
DetourModKit::LogicDllUnloadStatus DetourModKit::(anonymous namespace)::run_logic_dll_unload<DetourModKit::prepare_logic_dll_unload_all(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}>(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >, DetourModKit::prepare_logic_dll_unload_all(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}):
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 1547 times.
1564 if (detail::current_thread_in_delivery())
1082 {
1083 1 return LogicDllUnloadStatus::SelfDelivery;
1084 }
1085
1086 1563 const auto deadline = detail::drain_deadline(timeout);
1087 1563 const config::detail::ReloadDrainStatus begin_status = config::detail::begin_reload_drain();
1088
2/4
DetourModKit::LogicDllUnloadStatus DetourModKit::(anonymous namespace)::run_logic_dll_unload<DetourModKit::prepare_logic_dll_unload(std::span<std::basic_string_view<char, std::char_traits<char> > const, 18446744073709551615ull>, std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}>(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >, DetourModKit::prepare_logic_dll_unload(std::span<std::basic_string_view<char, std::char_traits<char> > const, 18446744073709551615ull>, std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}):
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 16 times.
DetourModKit::LogicDllUnloadStatus DetourModKit::(anonymous namespace)::run_logic_dll_unload<DetourModKit::prepare_logic_dll_unload_all(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}>(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >, DetourModKit::prepare_logic_dll_unload_all(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}):
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 1547 times.
1563 if (begin_status == config::detail::ReloadDrainStatus::SelfDelivery)
1089 {
1090 ✗ return LogicDllUnloadStatus::SelfDelivery;
1091 }
1092
3/4
DetourModKit::LogicDllUnloadStatus DetourModKit::(anonymous namespace)::run_logic_dll_unload<DetourModKit::prepare_logic_dll_unload(std::span<std::basic_string_view<char, std::char_traits<char> > const, 18446744073709551615ull>, std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}>(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >, DetourModKit::prepare_logic_dll_unload(std::span<std::basic_string_view<char, std::char_traits<char> > const, 18446744073709551615ull>, std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}):
✓ Branch 12 → 13 taken 1 time.
✓ Branch 12 → 14 taken 15 times.
DetourModKit::LogicDllUnloadStatus DetourModKit::(anonymous namespace)::run_logic_dll_unload<DetourModKit::prepare_logic_dll_unload_all(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}>(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >, DetourModKit::prepare_logic_dll_unload_all(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}):
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 1547 times.
1563 if (begin_status == config::detail::ReloadDrainStatus::InProgress)
1093 {
1094 1 return LogicDllUnloadStatus::InProgress;
1095 }
1096
1097 1562 const auto now = std::chrono::steady_clock::now();
1098 1562 const auto input_timeout = now < deadline
1099
2/4
DetourModKit::LogicDllUnloadStatus DetourModKit::(anonymous namespace)::run_logic_dll_unload<DetourModKit::prepare_logic_dll_unload(std::span<std::basic_string_view<char, std::char_traits<char> > const, 18446744073709551615ull>, std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}>(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >, DetourModKit::prepare_logic_dll_unload(std::span<std::basic_string_view<char, std::char_traits<char> > const, 18446744073709551615ull>, std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}):
✓ Branch 17 → 18 taken 15 times.
✗ Branch 17 → 20 not taken.
DetourModKit::LogicDllUnloadStatus DetourModKit::(anonymous namespace)::run_logic_dll_unload<DetourModKit::prepare_logic_dll_unload_all(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}>(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >, DetourModKit::prepare_logic_dll_unload_all(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}):
✓ Branch 17 → 18 taken 1547 times.
✗ Branch 17 → 20 not taken.
1562 ? std::chrono::duration_cast<std::chrono::milliseconds>(deadline - now)
1100 ✗ : std::chrono::milliseconds{0};
1101 1562 const input::CallbackDrainStatus input_status = drain_input(input_timeout);
1102
1103 1562 const config::detail::ReloadDrainStatus config_status = config::detail::finish_reload_drain(deadline);
1104 1562 LogicDllUnloadStatus status = LogicDllUnloadStatus::TimedOut;
1105
4/8
DetourModKit::LogicDllUnloadStatus DetourModKit::(anonymous namespace)::run_logic_dll_unload<DetourModKit::prepare_logic_dll_unload(std::span<std::basic_string_view<char, std::char_traits<char> > const, 18446744073709551615ull>, std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}>(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >, DetourModKit::prepare_logic_dll_unload(std::span<std::basic_string_view<char, std::char_traits<char> > const, 18446744073709551615ull>, std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}):
✓ Branch 23 → 24 taken 15 times.
✗ Branch 23 → 25 not taken.
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 26 taken 15 times.
DetourModKit::LogicDllUnloadStatus DetourModKit::(anonymous namespace)::run_logic_dll_unload<DetourModKit::prepare_logic_dll_unload_all(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}>(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >, DetourModKit::prepare_logic_dll_unload_all(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}):
✓ Branch 23 → 24 taken 1547 times.
✗ Branch 23 → 25 not taken.
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 26 taken 1547 times.
1562 if (input_status == input::CallbackDrainStatus::SelfDelivery ||
1106 config_status == config::detail::ReloadDrainStatus::SelfDelivery)
1107 {
1108 ✗ status = LogicDllUnloadStatus::SelfDelivery;
1109 }
1110
4/8
DetourModKit::LogicDllUnloadStatus DetourModKit::(anonymous namespace)::run_logic_dll_unload<DetourModKit::prepare_logic_dll_unload(std::span<std::basic_string_view<char, std::char_traits<char> > const, 18446744073709551615ull>, std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}>(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >, DetourModKit::prepare_logic_dll_unload(std::span<std::basic_string_view<char, std::char_traits<char> > const, 18446744073709551615ull>, std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}):
✓ Branch 26 → 27 taken 15 times.
✗ Branch 26 → 28 not taken.
✗ Branch 27 → 28 not taken.
✓ Branch 27 → 29 taken 15 times.
DetourModKit::LogicDllUnloadStatus DetourModKit::(anonymous namespace)::run_logic_dll_unload<DetourModKit::prepare_logic_dll_unload_all(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}>(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >, DetourModKit::prepare_logic_dll_unload_all(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}):
✓ Branch 26 → 27 taken 1547 times.
✗ Branch 26 → 28 not taken.
✗ Branch 27 → 28 not taken.
✓ Branch 27 → 29 taken 1547 times.
1562 else if (input_status == input::CallbackDrainStatus::InProgress ||
1111 config_status == config::detail::ReloadDrainStatus::InProgress)
1112 {
1113 ✗ status = LogicDllUnloadStatus::InProgress;
1114 }
1115
2/4
DetourModKit::LogicDllUnloadStatus DetourModKit::(anonymous namespace)::run_logic_dll_unload<DetourModKit::prepare_logic_dll_unload(std::span<std::basic_string_view<char, std::char_traits<char> > const, 18446744073709551615ull>, std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}>(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >, DetourModKit::prepare_logic_dll_unload(std::span<std::basic_string_view<char, std::char_traits<char> > const, 18446744073709551615ull>, std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}):
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 31 taken 15 times.
DetourModKit::LogicDllUnloadStatus DetourModKit::(anonymous namespace)::run_logic_dll_unload<DetourModKit::prepare_logic_dll_unload_all(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}>(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >, DetourModKit::prepare_logic_dll_unload_all(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}):
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 31 taken 1547 times.
1562 else if (input_status == input::CallbackDrainStatus::RetireFailed)
1116 {
1117 ✗ status = LogicDllUnloadStatus::RetireFailed;
1118 }
1119
8/8
DetourModKit::LogicDllUnloadStatus DetourModKit::(anonymous namespace)::run_logic_dll_unload<DetourModKit::prepare_logic_dll_unload(std::span<std::basic_string_view<char, std::char_traits<char> > const, 18446744073709551615ull>, std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}>(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >, DetourModKit::prepare_logic_dll_unload(std::span<std::basic_string_view<char, std::char_traits<char> > const, 18446744073709551615ull>, std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}):
✓ Branch 31 → 32 taken 14 times.
✓ Branch 31 → 37 taken 1 time.
✓ Branch 32 → 33 taken 12 times.
✓ Branch 32 → 37 taken 2 times.
DetourModKit::LogicDllUnloadStatus DetourModKit::(anonymous namespace)::run_logic_dll_unload<DetourModKit::prepare_logic_dll_unload_all(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}>(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >, DetourModKit::prepare_logic_dll_unload_all(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}):
✓ Branch 31 → 32 taken 1544 times.
✓ Branch 31 → 37 taken 3 times.
✓ Branch 32 → 33 taken 1542 times.
✓ Branch 32 → 37 taken 2 times.
1562 else if (input_status == input::CallbackDrainStatus::Drained &&
1120 config_status == config::detail::ReloadDrainStatus::Ready)
1121 {
1122
2/4
DetourModKit::LogicDllUnloadStatus DetourModKit::(anonymous namespace)::run_logic_dll_unload<DetourModKit::prepare_logic_dll_unload(std::span<std::basic_string_view<char, std::char_traits<char> > const, 18446744073709551615ull>, std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}>(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >, DetourModKit::prepare_logic_dll_unload(std::span<std::basic_string_view<char, std::char_traits<char> > const, 18446744073709551615ull>, std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}):
✓ Branch 34 → 35 taken 12 times.
✗ Branch 34 → 36 not taken.
DetourModKit::LogicDllUnloadStatus DetourModKit::(anonymous namespace)::run_logic_dll_unload<DetourModKit::prepare_logic_dll_unload_all(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}>(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >, DetourModKit::prepare_logic_dll_unload_all(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)::{lambda(std::chrono::duration<long long, std::ratio<1ll, 1000ll> >)#1}):
✓ Branch 34 → 35 taken 1542 times.
✗ Branch 34 → 36 not taken.
1554 if (detail::open_input_callback_admission())
1123 {
1124 1554 return LogicDllUnloadStatus::SafeToUnload;
1125 }
1126 ✗ status = LogicDllUnloadStatus::InProgress;
1127 }
1128
1129 // The unresolved drain closes admission until a later transaction proves the old callbacks gone.
1130 8 detail::mark_input_callback_drain_pending();
1131 8 return status;
1132 }
1133 } // namespace
1134
1135 18 LogicDllUnloadStatus prepare_logic_dll_unload(
1136 std::span<const std::string_view> binding_names,
1137 std::chrono::milliseconds timeout
1138 ) noexcept
1139 {
1140 18 return run_logic_dll_unload(
1141 timeout,
1142 15 [binding_names](std::chrono::milliseconds input_timeout) noexcept -> input::CallbackDrainStatus
1143 15 { return input::Input::instance().prepare_logic_dll_unload(binding_names, input_timeout); }
1144 18 );
1145 }
1146
1147 1547 LogicDllUnloadStatus prepare_logic_dll_unload_all(std::chrono::milliseconds timeout) noexcept
1148 {
1149 1547 return run_logic_dll_unload(
1150 timeout,
1151 1547 [](std::chrono::milliseconds input_timeout) noexcept -> input::CallbackDrainStatus
1152 1547 { return input::Input::instance().prepare_logic_dll_unload_all(input_timeout); }
1153 1547 );
1154 }
1155
1156 6 void on_logic_dll_unload(std::span<const std::string_view> binding_names) noexcept
1157 {
1158
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 7 taken 6 times.
6 if (!detail::blocking_teardown_permitted())
1159 {
1160 ✗ detail::close_input_callback_admission();
1161 ✗ config::detail::disable_reloads_for_unload();
1162 ✗ return;
1163 }
1164 6 (void)prepare_logic_dll_unload(binding_names);
1165 }
1166
1167 7 void on_logic_dll_unload_all() noexcept
1168 {
1169
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 7 taken 7 times.
7 if (!detail::blocking_teardown_permitted())
1170 {
1171 ✗ detail::close_input_callback_admission();
1172 ✗ config::detail::disable_reloads_for_unload();
1173 ✗ return;
1174 }
1175 7 (void)prepare_logic_dll_unload_all();
1176 }
1177 } // namespace DetourModKit
1178