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