GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 87.5% 478 / 0 / 546
Functions: 98.1% 53 / 0 / 54
Branches: 77.5% 314 / 0 / 405

src/wheel_host/wheel_host.cpp
Line Branch Exec Source
1 /**
2 * @file wheel_host.cpp
3 * @brief Implements the resident broker behind the wheel host C ABI.
4 */
5
6 #include "DetourModKit/abi/wheel_host.h"
7
8 #include <windows.h>
9
10 #include <algorithm>
11 #include <array>
12 #include <atomic>
13 #include <cstdint>
14 #include <limits>
15
16 namespace
17 {
18 constexpr std::uint64_t CAPTURE_ENABLED_BIT = 1u;
19 constexpr std::uint64_t CAPTURE_FOCUS_BIT = 2u;
20 constexpr std::uint64_t CAPTURE_FLAG_MASK = CAPTURE_ENABLED_BIT | CAPTURE_FOCUS_BIT;
21 constexpr int CAPTURE_EPOCH_SHIFT = 2;
22 constexpr int COUNT_BITS = 11;
23 constexpr std::uint64_t COUNT_MASK = (std::uint64_t{1} << COUNT_BITS) - 1u;
24 constexpr std::uint32_t MAX_COUNT = 1024;
25 constexpr int REMAINDER_BITS = 8;
26 constexpr std::uint64_t REMAINDER_MASK = (std::uint64_t{1} << REMAINDER_BITS) - 1u;
27 constexpr std::uint64_t REMAINDER_OWNED_BIT = std::uint64_t{1} << REMAINDER_BITS;
28 constexpr int REMAINDER_EPOCH_SHIFT = REMAINDER_BITS + 1;
29 // pack_remainder biases by WHEEL_DELTA, so the encoded value spans 0 through 2 * WHEEL_DELTA - 1.
30 static_assert(2 * WHEEL_DELTA <= (1 << REMAINDER_BITS), "a biased sub-notch remainder must fit the value field");
31 constexpr int CONSUME_EPOCH_SHIFT = 4;
32 constexpr std::uint32_t CONSUME_MASK = (1u << DMK_WHEEL_DIRECTIONS) - 1u;
33 constexpr std::uint64_t MAX_EPOCH = std::numeric_limits<std::uint64_t>::max() >> COUNT_BITS;
34
35 /// Bounds every control-plane wait for admitted callback phases.
36 constexpr std::uint32_t DEFAULT_DRAIN_TIMEOUT_MS = 2000;
37
38 class ControlLock
39 {
40 public:
41 960 explicit ControlLock(SRWLOCK &lock) noexcept : m_lock(&lock) { AcquireSRWLockExclusive(m_lock); }
42 960 ~ControlLock() noexcept { ReleaseSRWLockExclusive(m_lock); }
43
44 ControlLock(const ControlLock &) = delete;
45 ControlLock &operator=(const ControlLock &) = delete;
46
47 private:
48 SRWLOCK *m_lock;
49 };
50
51 878 [[nodiscard]] std::uint64_t next_nonzero(std::uint64_t value, std::uint64_t maximum) noexcept
52 {
53
1/2
✓ Branch 2 → 3 taken 878 times.
✗ Branch 2 → 4 not taken.
878 return value >= maximum ? 1u : value + 1u;
54 }
55
56 678 [[nodiscard]] constexpr std::uint64_t pack_capture(std::uint64_t epoch, std::uint64_t flags) noexcept
57 {
58 678 return (epoch << CAPTURE_EPOCH_SHIFT) | (flags & CAPTURE_FLAG_MASK);
59 }
60
61 64 [[nodiscard]] std::uint64_t capture_epoch(std::uint64_t state) noexcept
62 {
63 64 return state >> CAPTURE_EPOCH_SHIFT;
64 }
65
66 2302 [[nodiscard]] constexpr std::uint64_t pack_count(std::uint64_t epoch, std::uint32_t count) noexcept
67 {
68 2302 return (epoch << COUNT_BITS) | std::min(count, MAX_COUNT);
69 }
70
71 70 [[nodiscard]] std::uint64_t count_epoch(std::uint64_t state) noexcept
72 {
73 70 return state >> COUNT_BITS;
74 }
75
76 70 [[nodiscard]] std::uint32_t unpack_count(std::uint64_t state) noexcept
77 {
78 70 return static_cast<std::uint32_t>(state & COUNT_MASK);
79 }
80
81 1141 [[nodiscard]] constexpr std::uint64_t pack_remainder(std::uint64_t epoch, bool owned, int remainder) noexcept
82 {
83 1141 const int shifted = remainder + WHEEL_DELTA;
84 1141 const auto encoded = static_cast<std::uint64_t>(shifted);
85
2/2
✓ Branch 2 → 3 taken 6 times.
✓ Branch 2 → 4 taken 1135 times.
1141 return (epoch << REMAINDER_EPOCH_SHIFT) | (owned ? REMAINDER_OWNED_BIT : 0u) | encoded;
86 }
87
88 25 [[nodiscard]] std::uint64_t remainder_epoch(std::uint64_t state) noexcept
89 {
90 25 return state >> REMAINDER_EPOCH_SHIFT;
91 }
92
93 25 [[nodiscard]] bool remainder_owned(std::uint64_t state) noexcept
94 {
95 25 return (state & REMAINDER_OWNED_BIT) != 0;
96 }
97
98 20 [[nodiscard]] int unpack_remainder(std::uint64_t state) noexcept
99 {
100 20 return static_cast<int>(state & REMAINDER_MASK) - WHEEL_DELTA;
101 }
102
103 678 [[nodiscard]] constexpr std::uint64_t pack_consume(std::uint64_t epoch, std::uint32_t mask) noexcept
104 {
105 678 return (epoch << CONSUME_EPOCH_SHIFT) | (mask & CONSUME_MASK);
106 }
107
108 /// The control operation whose failure left a transaction pending. control_state_of() names each one.
109 enum class PendingOp : std::uint8_t
110 {
111 None,
112 Close,
113 Retarget,
114 Stop
115 };
116
117 struct HostState
118 {
119 SRWLOCK control_lock = SRWLOCK_INIT;
120 bool started = false;
121 bool stopping = false;
122 HHOOK hook = nullptr;
123 HANDLE target_thread = nullptr;
124 std::uint32_t target_thread_id = 0;
125 std::uint32_t route_state = DMK_WHEELHOST_ROUTE_TARGET_WAIT;
126 std::uint64_t mount_generation = 0;
127 std::uint64_t host_identity = 0;
128 std::uint64_t lease = 0;
129 std::uint64_t owner = 0;
130 std::uint64_t generation = 0;
131 std::uint64_t epoch = 1;
132 std::uint64_t lease_counter = 0;
133 std::uint64_t identity_counter = 0;
134 PendingOp pending_op = PendingOp::None;
135 std::uint64_t pending_lease = 0;
136 std::uint64_t pending_owner = 0;
137 std::uint64_t pending_generation = 0;
138 // The host owns the mount worker and its request-response rendezvous. See mount_resident_hook().
139 HANDLE mount_thread = nullptr;
140 HANDLE mount_request = nullptr;
141 HANDLE mount_done = nullptr;
142 std::uint32_t mount_request_tid = 0;
143 HHOOK mount_result = nullptr;
144 // Counts resident callback frames inside an admission phase. Nested message loops can hold several at once.
145 std::atomic<std::uint32_t> admitted_phases{0};
146 std::atomic<std::uint64_t> capture_state{pack_capture(1, 0)};
147 std::atomic<std::uint64_t> consume_state{pack_consume(1, 0)};
148 std::atomic<std::uint64_t> consume_deadline_ms{0};
149 std::array<std::atomic<std::uint64_t>, 2> remainder{pack_remainder(1, false, 0), pack_remainder(1, false, 0)};
150 std::array<std::atomic<std::uint64_t>, DMK_WHEEL_DIRECTIONS>
151 counts{pack_count(1, 0), pack_count(1, 0), pack_count(1, 0), pack_count(1, 0)};
152
153 558 void reset_data_plane(std::uint64_t active_epoch) noexcept
154 {
155
2/2
✓ Branch 13 → 3 taken 1116 times.
✓ Branch 13 → 14 taken 558 times.
1674 for (auto &slot : remainder)
156 {
157 1116 slot.store(pack_remainder(active_epoch, false, 0), std::memory_order_release);
158 }
159
2/2
✓ Branch 25 → 15 taken 2232 times.
✓ Branch 25 → 26 taken 558 times.
2790 for (auto &slot : counts)
160 {
161 2232 slot.store(pack_count(active_epoch, 0), std::memory_order_release);
162 }
163 558 }
164
165 558 void invalidate_capture() noexcept
166 {
167 558 epoch = next_nonzero(epoch, MAX_EPOCH);
168 558 capture_state.store(pack_capture(epoch, 0), std::memory_order_seq_cst);
169 558 consume_state.store(pack_consume(epoch, 0), std::memory_order_release);
170 558 consume_deadline_ms.store(0, std::memory_order_relaxed);
171 558 reset_data_plane(epoch);
172 558 }
173 };
174
175 constinit HostState g_host;
176
177 #if defined(DMK_WHEELHOST_ENABLE_TEST_SEAMS)
178 using HookProbe = void(DMK_WHEELHOST_CALL *)(void);
179 std::atomic<HookProbe> g_hook_probe{nullptr};
180 std::atomic<HookProbe> g_finalize_probe{nullptr};
181 std::atomic<bool> g_force_unhook_failure{false};
182 std::atomic<std::uint32_t> g_drain_timeout_override_ms{0};
183 std::atomic<std::int32_t> g_process_focus_override{-1};
184 #endif
185
186 290 [[nodiscard]] std::uint32_t drain_timeout_ms() noexcept
187 {
188 #if defined(DMK_WHEELHOST_ENABLE_TEST_SEAMS)
189 290 const std::uint32_t override_ms = g_drain_timeout_override_ms.load(std::memory_order_acquire);
190
2/2
✓ Branch 9 → 10 taken 12 times.
✓ Branch 9 → 11 taken 278 times.
290 if (override_ms != 0)
191 {
192 12 return override_ms;
193 }
194 #endif
195 278 return DEFAULT_DRAIN_TIMEOUT_MS;
196 }
197
198 /**
199 * @brief Waits, bounded, for every admitted callback phase to leave.
200 * @details Runs only on the control plane, after invalidate_capture() advanced the epoch. A parked lower-hook
201 * frame holds no phase, so this waits only on the short allocation-free admission windows.
202 * @note Requires g_host.control_lock.
203 */
204 290 [[nodiscard]] bool drain_admitted_phases() noexcept
205 {
206 290 const std::uint64_t deadline_ms = GetTickCount64() + drain_timeout_ms();
207
2/2
✓ Branch 16 → 5 taken 1492785 times.
✓ Branch 16 → 17 taken 284 times.
1493359 while (g_host.admitted_phases.load(std::memory_order_seq_cst) != 0)
208 {
209
2/2
✓ Branch 6 → 7 taken 6 times.
✓ Branch 6 → 8 taken 1492779 times.
1492785 if (GetTickCount64() >= deadline_ms)
210 {
211 6 return false;
212 }
213 1492779 Sleep(0);
214 }
215 284 return true;
216 }
217
218 /// Projects the internal transaction onto the ABI control state. Every PendingOp value is nameable.
219 34 [[nodiscard]] constexpr std::uint32_t control_state_of(PendingOp op) noexcept
220 {
221
4/5
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 6 times.
✓ Branch 2 → 5 taken 1 time.
✓ Branch 2 → 6 taken 26 times.
✗ Branch 2 → 7 not taken.
34 switch (op)
222 {
223 1 case PendingOp::Close:
224 1 return DMK_WHEELHOST_CONTROL_CLOSE_PENDING;
225 6 case PendingOp::Retarget:
226 6 return DMK_WHEELHOST_CONTROL_RETARGET_PENDING;
227 1 case PendingOp::Stop:
228 1 return DMK_WHEELHOST_CONTROL_STOP_PENDING;
229 26 case PendingOp::None:
230 26 break;
231 }
232 26 return DMK_WHEELHOST_CONTROL_IDLE;
233 }
234
235 /// Requires g_host.control_lock. Clears any pending control transaction.
236 96 void clear_pending() noexcept
237 {
238 96 g_host.pending_op = PendingOp::None;
239 96 g_host.pending_lease = 0;
240 96 g_host.pending_owner = 0;
241 96 g_host.pending_generation = 0;
242 96 }
243
244 771 [[nodiscard]] bool valid_context(void *host_context) noexcept
245 {
246 771 return host_context == &g_host;
247 }
248
249 55 [[nodiscard]] bool is_wheel(UINT message) noexcept
250 {
251
4/4
✓ Branch 2 → 3 taken 27 times.
✓ Branch 2 → 4 taken 28 times.
✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 5 taken 24 times.
55 return message == WM_MOUSEWHEEL || message == WM_MOUSEHWHEEL;
252 }
253
254 /// Reports whether this process owns the foreground window.
255 2 [[nodiscard]] bool process_owns_foreground() noexcept
256 {
257 #if defined(DMK_WHEELHOST_ENABLE_TEST_SEAMS)
258
1/2
✓ Branch 9 → 10 taken 2 times.
✗ Branch 9 → 11 not taken.
2 if (const std::int32_t override_value = g_process_focus_override.load(std::memory_order_acquire);
259 override_value >= 0)
260 {
261 2 return override_value != 0;
262 }
263 #endif
264 const HWND foreground = GetForegroundWindow();
265 if (foreground == nullptr)
266 {
267 return false;
268 }
269 DWORD pid = 0;
270 GetWindowThreadProcessId(foreground, &pid);
271 return pid == GetCurrentProcessId();
272 }
273
274 28 [[nodiscard]] bool direction_consumed(std::uint64_t epoch, std::uint32_t direction_bit) noexcept
275 {
276 28 const std::uint64_t state = g_host.consume_state.load(std::memory_order_acquire);
277
3/4
✓ Branch 9 → 10 taken 28 times.
✗ Branch 9 → 11 not taken.
✓ Branch 10 → 11 taken 19 times.
✓ Branch 10 → 12 taken 9 times.
28 if ((state >> CONSUME_EPOCH_SHIFT) != epoch || (state & direction_bit) == 0)
278 {
279 19 return false;
280 }
281 18 return GetTickCount64() < g_host.consume_deadline_ms.load(std::memory_order_relaxed);
282 }
283
284 /// Brackets one admitted callback phase. Allocation-free, nonblocking, and free of host locks.
285 class PhaseGuard
286 {
287 public:
288 35 PhaseGuard() noexcept { g_host.admitted_phases.fetch_add(1, std::memory_order_seq_cst); }
289 35 ~PhaseGuard() noexcept { g_host.admitted_phases.fetch_sub(1, std::memory_order_seq_cst); }
290 PhaseGuard(const PhaseGuard &) = delete;
291 PhaseGuard &operator=(const PhaseGuard &) = delete;
292 };
293
294 25 [[nodiscard]] int fold_axis(std::atomic<std::uint64_t> &slot, std::uint64_t epoch, bool owned, int delta) noexcept
295 {
296 50 std::uint64_t observed = slot.load(std::memory_order_acquire);
297 for (;;)
298 {
299
1/2
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 25 times.
25 if (remainder_epoch(observed) != epoch)
300 {
301 return 0;
302 }
303
2/2
✓ Branch 14 → 15 taken 20 times.
✓ Branch 14 → 16 taken 5 times.
25 const int prior = remainder_owned(observed) == owned ? unpack_remainder(observed) : 0;
304 25 const int total = prior + delta;
305 25 const int notches = total / WHEEL_DELTA;
306 25 const std::uint64_t desired = pack_remainder(epoch, owned, total % WHEEL_DELTA);
307
1/2
✓ Branch 23 → 24 taken 25 times.
✗ Branch 23 → 25 not taken.
50 if (slot.compare_exchange_weak(observed, desired, std::memory_order_acq_rel, std::memory_order_acquire))
308 {
309 25 return notches;
310 }
311 }
312 }
313
314 18 void bump_count(std::size_t direction, std::uint64_t epoch, std::uint32_t increment) noexcept
315 {
316 18 auto &slot = g_host.counts[direction];
317 36 std::uint64_t observed = slot.load(std::memory_order_acquire);
318 for (;;)
319 {
320
1/2
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 18 times.
18 if (count_epoch(observed) != epoch)
321 {
322 return;
323 }
324 18 const std::uint32_t current = unpack_count(observed);
325 18 const std::uint32_t next = std::min<std::uint32_t>(MAX_COUNT, current + increment);
326 18 const std::uint64_t desired = pack_count(epoch, next);
327
1/2
✓ Branch 22 → 23 taken 18 times.
✗ Branch 22 → 24 not taken.
36 if (slot.compare_exchange_weak(observed, desired, std::memory_order_acq_rel, std::memory_order_acquire))
328 {
329 18 return;
330 }
331 }
332 }
333
334 25 void count_direction(bool horizontal, std::uint64_t epoch, int notches) noexcept
335 {
336
2/2
✓ Branch 2 → 3 taken 14 times.
✓ Branch 2 → 7 taken 11 times.
25 if (notches > 0)
337 {
338
2/2
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 5 taken 12 times.
14 bump_count(horizontal ? DMK_WHEEL_RIGHT : DMK_WHEEL_UP, epoch, static_cast<std::uint32_t>(notches));
339 }
340
2/2
✓ Branch 7 → 8 taken 4 times.
✓ Branch 7 → 12 taken 7 times.
11 else if (notches < 0)
341 {
342
2/2
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 3 times.
4 bump_count(horizontal ? DMK_WHEEL_LEFT : DMK_WHEEL_DOWN, epoch, static_cast<std::uint32_t>(-notches));
343 }
344 25 }
345
346 /**
347 * @brief The resident WH_GETMESSAGE callback.
348 * @details Order contract (v2): count admission runs before CallNextHookEx and mutates nothing in the message,
349 * so older hooks see the original record. CallNextHookEx runs exactly once. Consume finalization runs
350 * after it returns and writes WM_NULL only when the saved intent, epoch, consume mask, TTL, and focus
351 * gate all remain current. A newer hook can rewrite the message after this returns; the consume stays
352 * best effort. Each admitted phase is counted so the control plane can drain decisions.
353 */
354 55 LRESULT CALLBACK resident_hook(int code, WPARAM wparam, LPARAM lparam) noexcept
355 {
356
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 5 taken 55 times.
55 if (code != HC_ACTION)
357 {
358 return CallNextHookEx(nullptr, code, wparam, lparam);
359 }
360
361 55 MSG *const message = reinterpret_cast<MSG *>(lparam);
362
5/6
✓ Branch 5 → 6 taken 55 times.
✗ Branch 5 → 8 not taken.
✓ Branch 7 → 8 taken 24 times.
✓ Branch 7 → 9 taken 31 times.
✓ Branch 10 → 11 taken 24 times.
✓ Branch 10 → 13 taken 31 times.
55 if (message == nullptr || !is_wheel(message->message))
363 {
364 24 return CallNextHookEx(nullptr, code, wparam, lparam);
365 }
366
367 // Save the original fields and the entry epoch before any decision.
368 31 const MSG original = *message;
369 31 const std::uint64_t capture = g_host.capture_state.load(std::memory_order_seq_cst);
370
2/2
✓ Branch 20 → 21 taken 2 times.
✓ Branch 20 → 23 taken 29 times.
31 if ((capture & CAPTURE_ENABLED_BIT) == 0)
371 {
372 2 return CallNextHookEx(nullptr, code, wparam, lparam);
373 }
374
375 29 const bool horizontal = original.message == WM_MOUSEHWHEEL;
376 29 const int delta = static_cast<short>(HIWORD(original.wParam));
377
2/4
✓ Branch 23 → 24 taken 29 times.
✗ Branch 23 → 25 not taken.
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 27 taken 29 times.
29 if (delta == 0 || wparam != PM_REMOVE)
378 {
379 // PM_NOREMOVE passes through without a counter, remainder, ownership, epoch, or consume change.
380 return CallNextHookEx(nullptr, code, wparam, lparam);
381 }
382
383
4/4
✓ Branch 27 → 28 taken 3 times.
✓ Branch 27 → 31 taken 26 times.
✓ Branch 28 → 29 taken 2 times.
✓ Branch 28 → 30 taken 1 time.
55 const std::uint32_t direction_bit = horizontal ? (delta > 0 ? DMK_WHEEL_CONSUME_RIGHT : DMK_WHEEL_CONSUME_LEFT)
384
2/2
✓ Branch 31 → 32 taken 22 times.
✓ Branch 31 → 33 taken 4 times.
26 : (delta > 0 ? DMK_WHEEL_CONSUME_UP : DMK_WHEEL_CONSUME_DOWN);
385 29 const std::uint64_t epoch = capture_epoch(capture);
386 // Count admission: fold, count, and snapshot the consume intent, all before CallNextHookEx.
387 29 bool consume_intent = false;
388 {
389 29 const PhaseGuard phase;
390 #if defined(DMK_WHEELHOST_ENABLE_TEST_SEAMS)
391
2/2
✓ Branch 37 → 38 taken 4 times.
✓ Branch 37 → 39 taken 25 times.
29 if (const HookProbe probe = g_hook_probe.load(std::memory_order_acquire); probe != nullptr)
392 {
393 4 probe();
394 }
395 #endif
396 29 const std::uint64_t recheck = g_host.capture_state.load(std::memory_order_seq_cst);
397
3/4
✓ Branch 47 → 48 taken 26 times.
✓ Branch 47 → 50 taken 3 times.
✓ Branch 48 → 49 taken 26 times.
✗ Branch 48 → 50 not taken.
29 const bool current = capture_epoch(recheck) == epoch && (recheck & CAPTURE_ENABLED_BIT) != 0;
398
3/4
✓ Branch 51 → 52 taken 1 time.
✓ Branch 51 → 54 taken 28 times.
✗ Branch 53 → 54 not taken.
✓ Branch 53 → 55 taken 1 time.
29 const bool focus_ok = (recheck & CAPTURE_FOCUS_BIT) == 0 || process_owns_foreground();
399
4/4
✓ Branch 56 → 57 taken 26 times.
✓ Branch 56 → 66 taken 3 times.
✓ Branch 57 → 58 taken 25 times.
✓ Branch 57 → 66 taken 1 time.
29 if (current && focus_ok)
400 {
401 25 const bool owned = direction_consumed(epoch, direction_bit);
402
2/2
✓ Branch 59 → 60 taken 3 times.
✓ Branch 59 → 61 taken 22 times.
25 const int notches = fold_axis(g_host.remainder[horizontal ? 1u : 0u], epoch, owned, delta);
403 25 count_direction(horizontal, epoch, notches);
404 25 consume_intent = owned;
405 }
406 29 }
407
408 // Pass the original record to older hooks with no DMK mutation, exactly once.
409 29 const LRESULT result = CallNextHookEx(nullptr, code, wparam, lparam);
410
411
2/2
✓ Branch 68 → 69 taken 23 times.
✓ Branch 68 → 70 taken 6 times.
29 if (!consume_intent)
412 {
413 23 return result;
414 }
415
416 // Consume finalization: the write happens only while every admission condition still holds.
417 {
418 6 const PhaseGuard phase;
419 #if defined(DMK_WHEELHOST_ENABLE_TEST_SEAMS)
420
2/2
✓ Branch 72 → 73 taken 3 times.
✓ Branch 72 → 74 taken 3 times.
6 if (const HookProbe probe = g_finalize_probe.load(std::memory_order_acquire); probe != nullptr)
421 {
422 3 probe();
423 }
424 #endif
425 6 const std::uint64_t recheck = g_host.capture_state.load(std::memory_order_seq_cst);
426
3/4
✓ Branch 82 → 83 taken 4 times.
✓ Branch 82 → 85 taken 2 times.
✓ Branch 83 → 84 taken 4 times.
✗ Branch 83 → 85 not taken.
6 const bool current = capture_epoch(recheck) == epoch && (recheck & CAPTURE_ENABLED_BIT) != 0;
427
3/4
✓ Branch 86 → 87 taken 1 time.
✓ Branch 86 → 89 taken 5 times.
✗ Branch 88 → 89 not taken.
✓ Branch 88 → 90 taken 1 time.
6 const bool focus_ok = (recheck & CAPTURE_FOCUS_BIT) == 0 || process_owns_foreground();
428
7/8
✓ Branch 91 → 92 taken 4 times.
✓ Branch 91 → 96 taken 2 times.
✓ Branch 92 → 93 taken 3 times.
✓ Branch 92 → 96 taken 1 time.
✓ Branch 94 → 95 taken 3 times.
✗ Branch 94 → 96 not taken.
✓ Branch 97 → 98 taken 3 times.
✓ Branch 97 → 99 taken 3 times.
6 if (current && focus_ok && direction_consumed(epoch, direction_bit))
429 {
430 3 message->message = WM_NULL;
431 3 message->wParam = 0;
432 3 message->lParam = 0;
433 }
434 6 }
435 6 return result;
436 }
437
438 int32_t DMK_WHEELHOST_CALL
439 335 open_lease(void *host_context, std::uint64_t owner, std::uint64_t generation, WheelHostLease *out_lease) noexcept
440 {
441
8/10
✓ Branch 3 → 4 taken 334 times.
✓ Branch 3 → 7 taken 1 time.
✓ Branch 4 → 5 taken 333 times.
✓ Branch 4 → 7 taken 1 time.
✓ Branch 5 → 6 taken 333 times.
✗ Branch 5 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 333 times.
✓ Branch 9 → 10 taken 2 times.
✓ Branch 9 → 11 taken 333 times.
335 if (!valid_context(host_context) || out_lease == nullptr || owner == 0 || generation == 0)
442 {
443 2 return DMK_WHEELHOST_ERR_INVALID;
444 }
445 333 *out_lease = 0;
446 333 const ControlLock lock(g_host.control_lock);
447
2/2
✓ Branch 12 → 13 taken 2 times.
✓ Branch 12 → 14 taken 331 times.
333 if (g_host.pending_op != PendingOp::None)
448 {
449 2 return DMK_WHEELHOST_ERR_PENDING;
450 }
451
2/4
✓ Branch 14 → 15 taken 331 times.
✗ Branch 14 → 16 not taken.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 331 times.
331 if (!g_host.started || g_host.stopping)
452 {
453 return DMK_WHEELHOST_ERR_STATE;
454 }
455
2/2
✓ Branch 17 → 18 taken 101 times.
✓ Branch 17 → 19 taken 230 times.
331 if (g_host.lease != 0)
456 {
457 101 return DMK_WHEELHOST_ERR_BUSY;
458 }
459
460 230 g_host.lease_counter = next_nonzero(g_host.lease_counter, std::numeric_limits<std::uint64_t>::max());
461 230 g_host.invalidate_capture();
462 230 g_host.lease = g_host.lease_counter;
463 230 g_host.owner = owner;
464 230 g_host.generation = generation;
465 230 *out_lease = g_host.lease;
466 230 return DMK_WHEELHOST_OK;
467 333 }
468
469 125 int32_t DMK_WHEELHOST_CALL publish_capture(
470 void *host_context,
471 WheelHostLease lease,
472 std::uint32_t capture_enabled,
473 std::uint32_t consume_mask,
474 std::uint32_t ttl_ms
475 ) noexcept
476 {
477
4/8
✓ Branch 3 → 4 taken 125 times.
✗ Branch 3 → 6 not taken.
✓ Branch 4 → 5 taken 125 times.
✗ Branch 4 → 6 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 125 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 125 times.
125 if (!valid_context(host_context) || (capture_enabled & ~CAPTURE_FLAG_MASK) != 0 ||
478 (consume_mask & ~CONSUME_MASK) != 0)
479 {
480 return DMK_WHEELHOST_ERR_INVALID;
481 }
482 125 const ControlLock lock(g_host.control_lock);
483
2/2
✓ Branch 11 → 12 taken 3 times.
✓ Branch 11 → 13 taken 122 times.
125 if (g_host.pending_op != PendingOp::None)
484 {
485 3 return DMK_WHEELHOST_ERR_PENDING;
486 }
487
1/2
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 122 times.
122 if (g_host.stopping)
488 {
489 return DMK_WHEELHOST_ERR_STATE;
490 }
491
2/2
✓ Branch 15 → 16 taken 1 time.
✓ Branch 15 → 17 taken 121 times.
122 if (g_host.lease == 0)
492 {
493 1 return DMK_WHEELHOST_ERR_NO_LEASE;
494 }
495
2/2
✓ Branch 17 → 18 taken 1 time.
✓ Branch 17 → 19 taken 120 times.
121 if (lease != g_host.lease)
496 {
497 1 return DMK_WHEELHOST_ERR_STALE;
498 }
499
500
4/4
✓ Branch 19 → 20 taken 107 times.
✓ Branch 19 → 22 taken 13 times.
✓ Branch 20 → 21 taken 7 times.
✓ Branch 20 → 22 taken 100 times.
120 const bool consume_active = ttl_ms != 0 && consume_mask != 0;
501
2/2
✓ Branch 23 → 24 taken 7 times.
✓ Branch 23 → 37 taken 113 times.
120 if (consume_active)
502 {
503 7 const std::uint64_t now = GetTickCount64();
504 7 const std::uint64_t maximum = std::numeric_limits<std::uint64_t>::max();
505 g_host.consume_deadline_ms.store(
506
1/2
✓ Branch 25 → 26 taken 7 times.
✗ Branch 25 → 27 not taken.
7 now > maximum - ttl_ms ? maximum : now + ttl_ms,
507 std::memory_order_relaxed
508 );
509 }
510 else
511 {
512 g_host.consume_deadline_ms.store(0, std::memory_order_relaxed);
513 }
514
2/2
✓ Branch 46 → 47 taken 7 times.
✓ Branch 46 → 48 taken 113 times.
120 g_host.consume_state.store(
515 pack_consume(g_host.epoch, consume_active ? consume_mask : 0u),
516 std::memory_order_release
517 );
518 // Capture arms only through a mounted, ready route. An unmounted lease keeps counting disabled.
519 1 const std::uint64_t flags =
520
2/2
✓ Branch 58 → 59 taken 119 times.
✓ Branch 58 → 60 taken 1 time.
120 g_host.route_state == DMK_WHEELHOST_ROUTE_READY ? (capture_enabled & CAPTURE_FLAG_MASK) : 0u;
521 120 g_host.capture_state.store(pack_capture(g_host.epoch, flags), std::memory_order_seq_cst);
522 120 return DMK_WHEELHOST_OK;
523 125 }
524
525 int32_t DMK_WHEELHOST_CALL
526 16 drain_counts(void *host_context, WheelHostLease lease, std::uint32_t out_counts[DMK_WHEEL_DIRECTIONS]) noexcept
527 {
528
5/6
✓ Branch 3 → 4 taken 15 times.
✓ Branch 3 → 5 taken 1 time.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 15 times.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 15 times.
16 if (!valid_context(host_context) || out_counts == nullptr)
529 {
530 1 return DMK_WHEELHOST_ERR_INVALID;
531 }
532 15 const ControlLock lock(g_host.control_lock);
533
1/2
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 15 times.
15 if (g_host.pending_op != PendingOp::None)
534 {
535 return DMK_WHEELHOST_ERR_PENDING;
536 }
537
1/2
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 15 times.
15 if (g_host.stopping)
538 {
539 return DMK_WHEELHOST_ERR_STATE;
540 }
541
2/2
✓ Branch 14 → 15 taken 2 times.
✓ Branch 14 → 16 taken 13 times.
15 if (g_host.lease == 0)
542 {
543 2 return DMK_WHEELHOST_ERR_NO_LEASE;
544 }
545
1/2
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 13 times.
13 if (lease != g_host.lease)
546 {
547 return DMK_WHEELHOST_ERR_STALE;
548 }
549
550
2/2
✓ Branch 28 → 19 taken 52 times.
✓ Branch 28 → 29 taken 13 times.
65 for (std::size_t i = 0; i < DMK_WHEEL_DIRECTIONS; ++i)
551 {
552 const std::uint64_t state =
553 52 g_host.counts[i].exchange(pack_count(g_host.epoch, 0), std::memory_order_acq_rel);
554
1/2
✓ Branch 24 → 25 taken 52 times.
✗ Branch 24 → 26 not taken.
52 out_counts[i] = count_epoch(state) == g_host.epoch ? unpack_count(state) : 0;
555 }
556 13 return DMK_WHEELHOST_OK;
557 15 }
558
559 int32_t DMK_WHEELHOST_CALL
560 240 close_lease(void *host_context, WheelHostLease lease, std::uint64_t owner, std::uint64_t generation) noexcept
561 {
562
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 240 times.
240 if (!valid_context(host_context))
563 {
564 return DMK_WHEELHOST_ERR_INVALID;
565 }
566 240 const ControlLock lock(g_host.control_lock);
567
2/2
✓ Branch 6 → 7 taken 6 times.
✓ Branch 6 → 30 taken 234 times.
240 if (g_host.pending_op != PendingOp::None)
568 {
569
2/2
✓ Branch 7 → 8 taken 3 times.
✓ Branch 7 → 16 taken 3 times.
6 if (g_host.pending_op == PendingOp::Retarget)
570 {
571 3 const bool matching_lease =
572
5/6
✓ Branch 8 → 9 taken 3 times.
✗ Branch 8 → 12 not taken.
✓ Branch 9 → 10 taken 2 times.
✓ Branch 9 → 12 taken 1 time.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 1 time.
3 lease == g_host.pending_lease && owner == g_host.owner && generation == g_host.generation;
573
2/2
✓ Branch 13 → 14 taken 2 times.
✓ Branch 13 → 15 taken 1 time.
3 if (!matching_lease)
574 {
575 2 return DMK_WHEELHOST_ERR_PENDING;
576 }
577 // An authorized close supersedes the disabled retarget transaction and drains the same lease below.
578 1 clear_pending();
579 }
580
1/2
✓ Branch 16 → 17 taken 3 times.
✗ Branch 16 → 29 not taken.
3 else if (g_host.pending_op == PendingOp::Close)
581 {
582
3/4
✓ Branch 17 → 18 taken 3 times.
✗ Branch 17 → 21 not taken.
✓ Branch 18 → 19 taken 2 times.
✓ Branch 18 → 21 taken 1 time.
5 const bool exact_retry = lease == g_host.pending_lease && owner == g_host.pending_owner &&
583
1/2
✓ Branch 19 → 20 taken 2 times.
✗ Branch 19 → 21 not taken.
2 generation == g_host.pending_generation;
584
2/2
✓ Branch 22 → 23 taken 1 time.
✓ Branch 22 → 24 taken 2 times.
3 if (!exact_retry)
585 {
586 1 return DMK_WHEELHOST_ERR_PENDING;
587 }
588 // The Closing lease is already invalidated. The retry only has to finish the drain.
589
1/2
✗ Branch 25 → 26 not taken.
✓ Branch 25 → 27 taken 2 times.
2 if (!drain_admitted_phases())
590 {
591 return DMK_WHEELHOST_ERR_DRAIN;
592 }
593 2 clear_pending();
594 2 g_host.lease = 0;
595 2 g_host.owner = 0;
596 2 g_host.generation = 0;
597 2 return DMK_WHEELHOST_OK;
598 }
599 else
600 {
601 return DMK_WHEELHOST_ERR_PENDING;
602 }
603 }
604
1/2
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 32 taken 235 times.
235 if (g_host.stopping)
605 {
606 return DMK_WHEELHOST_ERR_STATE;
607 }
608
2/2
✓ Branch 32 → 33 taken 3 times.
✓ Branch 32 → 34 taken 232 times.
235 if (g_host.lease == 0)
609 {
610 3 return DMK_WHEELHOST_ERR_NO_LEASE;
611 }
612
5/6
✓ Branch 34 → 35 taken 232 times.
✗ Branch 34 → 37 not taken.
✓ Branch 35 → 36 taken 231 times.
✓ Branch 35 → 37 taken 1 time.
✓ Branch 36 → 37 taken 1 time.
✓ Branch 36 → 38 taken 230 times.
232 if (lease != g_host.lease || owner != g_host.owner || generation != g_host.generation)
613 {
614 2 return DMK_WHEELHOST_ERR_STALE;
615 }
616
617 230 g_host.invalidate_capture();
618
2/2
✓ Branch 40 → 41 taken 3 times.
✓ Branch 40 → 42 taken 227 times.
230 if (!drain_admitted_phases())
619 {
620 // Closing state: the lease is retained and disabled. Only the exact retry can finish it, and a
621 // successor open is refused until it does.
622 3 g_host.pending_op = PendingOp::Close;
623 3 g_host.pending_lease = lease;
624 3 g_host.pending_owner = owner;
625 3 g_host.pending_generation = generation;
626 3 return DMK_WHEELHOST_ERR_DRAIN;
627 }
628 227 g_host.lease = 0;
629 227 g_host.owner = 0;
630 227 g_host.generation = 0;
631 227 return DMK_WHEELHOST_OK;
632 240 }
633
634 42 DWORD WINAPI mount_thread_main(LPVOID) noexcept
635 {
636 for (;;)
637 {
638
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 89 times.
89 if (WaitForSingleObject(g_host.mount_request, INFINITE) != WAIT_OBJECT_0)
639 {
640 return 0;
641 }
642
2/2
✓ Branch 6 → 7 taken 42 times.
✓ Branch 6 → 8 taken 47 times.
89 if (g_host.mount_request_tid == 0)
643 {
644 42 return 0;
645 }
646 47 g_host.mount_result = SetWindowsHookExW(WH_GETMESSAGE, &resident_hook, nullptr, g_host.mount_request_tid);
647
1/2
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 47 times.
47 if (SetEvent(g_host.mount_done) == 0)
648 {
649 return 0;
650 }
651 }
652 }
653
654 /**
655 * @brief Releases the handles for a terminated mount worker.
656 * @note Requires g_host.control_lock.
657 */
658 42 void clear_mount_thread() noexcept
659 {
660 42 CloseHandle(g_host.mount_thread);
661 42 CloseHandle(g_host.mount_request);
662 42 CloseHandle(g_host.mount_done);
663 42 g_host.mount_thread = nullptr;
664 42 g_host.mount_request = nullptr;
665 42 g_host.mount_done = nullptr;
666 42 g_host.mount_request_tid = 0;
667 42 g_host.mount_result = nullptr;
668 42 }
669
670 /**
671 * @brief Stops the mount worker and releases its handles.
672 * @note Requires g_host.control_lock.
673 */
674 43 [[nodiscard]] bool stop_mount_thread() noexcept
675 {
676
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 42 times.
43 if (g_host.mount_thread == nullptr)
677 {
678 1 return true;
679 }
680
681 42 g_host.mount_request_tid = 0;
682
2/6
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 9 taken 42 times.
✗ Branch 7 → 8 not taken.
✗ Branch 7 → 9 not taken.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 42 times.
42 if (SetEvent(g_host.mount_request) == 0 && WaitForSingleObject(g_host.mount_thread, 0) != WAIT_OBJECT_0)
683 {
684 return false;
685 }
686
1/2
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 42 times.
42 if (WaitForSingleObject(g_host.mount_thread, INFINITE) != WAIT_OBJECT_0)
687 {
688 return false;
689 }
690 42 clear_mount_thread();
691 42 return true;
692 }
693
694 /**
695 * @brief Requires g_host.control_lock. Installs the resident hook from the host-owned mount thread.
696 * @details Win32 removes a hook when the thread that installed it exits.
697 * A transient poller can therefore leave a false ready route after its thread exits.
698 * The host worker lasts until wheel_host_stop.
699 */
700 47 [[nodiscard]] HHOOK mount_resident_hook(std::uint32_t target_thread_id) noexcept
701 {
702
2/2
✓ Branch 2 → 3 taken 42 times.
✓ Branch 2 → 16 taken 5 times.
47 if (g_host.mount_thread == nullptr)
703 {
704 42 const HANDLE request = CreateEventW(nullptr, FALSE, FALSE, nullptr);
705 42 const HANDLE done = CreateEventW(nullptr, FALSE, FALSE, nullptr);
706 42 HANDLE thread = nullptr;
707
2/4
✓ Branch 5 → 6 taken 42 times.
✗ Branch 5 → 9 not taken.
✓ Branch 6 → 7 taken 42 times.
✗ Branch 6 → 9 not taken.
42 if (request != nullptr && done != nullptr)
708 {
709 42 g_host.mount_request = request;
710 42 g_host.mount_done = done;
711 42 thread = CreateThread(
712 nullptr,
713 static_cast<SIZE_T>(64u) * 1024u,
714 &mount_thread_main,
715 nullptr,
716 STACK_SIZE_PARAM_IS_A_RESERVATION,
717 nullptr
718 );
719 }
720
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 15 taken 42 times.
42 if (thread == nullptr)
721 {
722 if (request != nullptr)
723 {
724 CloseHandle(request);
725 }
726 if (done != nullptr)
727 {
728 CloseHandle(done);
729 }
730 g_host.mount_request = nullptr;
731 g_host.mount_done = nullptr;
732 return nullptr;
733 }
734 42 g_host.mount_thread = thread;
735 }
736 47 g_host.mount_request_tid = target_thread_id;
737 47 g_host.mount_result = nullptr;
738
1/2
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 22 taken 47 times.
47 if (SetEvent(g_host.mount_request) == 0)
739 {
740 if (WaitForSingleObject(g_host.mount_thread, 0) == WAIT_OBJECT_0)
741 {
742 clear_mount_thread();
743 }
744 return nullptr;
745 }
746 const std::array<HANDLE, 2> wait_handles = {
747 47 g_host.mount_done,
748 47 g_host.mount_thread,
749 47 };
750 const DWORD wait_result =
751 94 WaitForMultipleObjects(static_cast<DWORD>(wait_handles.size()), wait_handles.data(), FALSE, INFINITE);
752
1/2
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 30 taken 47 times.
47 if (wait_result != WAIT_OBJECT_0)
753 {
754 if (wait_result == WAIT_OBJECT_0 + 1)
755 {
756 clear_mount_thread();
757 }
758 return nullptr;
759 }
760 47 return g_host.mount_result;
761 }
762
763 43 [[nodiscard]] bool pin_host_module() noexcept
764 {
765 43 HMODULE module = nullptr;
766 43 return GetModuleHandleExW(
767 GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_PIN,
768 reinterpret_cast<LPCWSTR>(&resident_hook),
769 &module
770 43 ) != 0;
771 }
772
773 50 [[nodiscard]] HANDLE open_target_thread(std::uint32_t target_thread_id) noexcept
774 {
775 50 HANDLE thread = OpenThread(SYNCHRONIZE | THREAD_QUERY_LIMITED_INFORMATION, FALSE, target_thread_id);
776
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 50 times.
50 if (thread == nullptr)
777 {
778 return nullptr;
779 }
780
5/6
✓ Branch 7 → 8 taken 50 times.
✗ Branch 7 → 10 not taken.
✓ Branch 9 → 10 taken 3 times.
✓ Branch 9 → 11 taken 47 times.
✓ Branch 12 → 13 taken 3 times.
✓ Branch 12 → 15 taken 47 times.
50 if (GetProcessIdOfThread(thread) != GetCurrentProcessId() || WaitForSingleObject(thread, 0) == WAIT_OBJECT_0)
781 {
782 3 CloseHandle(thread);
783 3 return nullptr;
784 }
785 47 return thread;
786 }
787
788 49 [[nodiscard]] bool remove_hook(HHOOK hook) noexcept
789 {
790 #if defined(DMK_WHEELHOST_ENABLE_TEST_SEAMS)
791
2/2
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 5 taken 47 times.
49 if (g_force_unhook_failure.load(std::memory_order_acquire))
792 {
793 2 return false;
794 }
795 #endif
796 47 return UnhookWindowsHookEx(hook) != 0;
797 }
798
799 /// Requires g_host.control_lock. Reports whether the mounted target thread exited.
800 50 [[nodiscard]] bool target_thread_exited() noexcept
801 {
802
3/4
✓ Branch 2 → 3 taken 50 times.
✗ Branch 2 → 6 not taken.
✓ Branch 4 → 5 taken 3 times.
✓ Branch 4 → 6 taken 47 times.
50 return g_host.target_thread != nullptr && WaitForSingleObject(g_host.target_thread, 0) == WAIT_OBJECT_0;
803 }
804
805 /// Requires g_host.control_lock. Drops the mounted route state. Thread exit is authoritative hook retirement.
806 void retire_route_after_thread_exit() noexcept
807 {
808 if (g_host.hook != nullptr)
809 {
810 // The hook died with its thread. The call only releases the handle; its result carries no authority.
811 (void)UnhookWindowsHookEx(g_host.hook);
812 g_host.hook = nullptr;
813 }
814 if (g_host.target_thread != nullptr)
815 {
816 CloseHandle(g_host.target_thread);
817 g_host.target_thread = nullptr;
818 }
819 g_host.target_thread_id = 0;
820 g_host.invalidate_capture();
821 g_host.route_state = DMK_WHEELHOST_ROUTE_RETRYABLE;
822 }
823
824 /**
825 * @brief Requires g_host.control_lock. Rechecks target-thread liveness and settles the route state.
826 * @details A dead target cannot remain ready, and a cleanup-blocked route whose old thread exited becomes
827 * retryable. Runs from route_status and before every retarget attempt.
828 */
829 48 void settle_route_state() noexcept
830 {
831
4/6
✓ Branch 2 → 3 taken 39 times.
✓ Branch 2 → 6 taken 9 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 39 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 48 times.
48 if (g_host.hook != nullptr && target_thread_exited())
832 {
833 retire_route_after_thread_exit();
834 }
835 48 }
836
837 39 int32_t DMK_WHEELHOST_CALL route_status(
838 void *host_context,
839 WheelHostLease lease,
840 std::uint32_t status_capacity,
841 WheelHostRouteStatus *out_status
842 ) noexcept
843 {
844
6/6
✓ Branch 3 → 4 taken 38 times.
✓ Branch 3 → 5 taken 1 time.
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 37 times.
✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 9 taken 37 times.
39 if (!valid_context(host_context) || out_status == nullptr)
845 {
846 2 return DMK_WHEELHOST_ERR_INVALID;
847 }
848
2/2
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 11 taken 36 times.
37 if (status_capacity < sizeof(WheelHostRouteStatus))
849 {
850 1 return DMK_WHEELHOST_ERR_ABI;
851 }
852 36 const ControlLock lock(g_host.control_lock);
853
2/2
✓ Branch 12 → 13 taken 1 time.
✓ Branch 12 → 14 taken 35 times.
36 if (!g_host.started)
854 {
855 1 return DMK_WHEELHOST_ERR_STATE;
856 }
857 // A zero lease is an unqualified probe, so a loader can read the route before it hands the table out.
858
4/4
✓ Branch 14 → 15 taken 32 times.
✓ Branch 14 → 17 taken 3 times.
✓ Branch 15 → 16 taken 31 times.
✓ Branch 15 → 17 taken 1 time.
35 const bool lease_matches = lease != 0 && lease == g_host.lease;
859
4/4
✓ Branch 18 → 19 taken 32 times.
✓ Branch 18 → 21 taken 3 times.
✓ Branch 19 → 20 taken 1 time.
✓ Branch 19 → 21 taken 31 times.
35 if (lease != 0 && !lease_matches)
860 {
861 1 return DMK_WHEELHOST_ERR_STALE;
862 }
863 // Liveness recheck only. The query settles physical mount health and never ends a control transaction.
864 34 settle_route_state();
865
866 34 WheelHostRouteStatus status{};
867 34 status.struct_size = static_cast<std::uint32_t>(sizeof(WheelHostRouteStatus));
868 34 status.route_state = g_host.route_state;
869 34 status.control_state = control_state_of(g_host.pending_op);
870 // The host owns its own precondition lattice so no client re-derives it.
871
4/4
✓ Branch 24 → 25 taken 26 times.
✓ Branch 24 → 28 taken 7 times.
✓ Branch 25 → 26 taken 23 times.
✓ Branch 25 → 28 taken 3 times.
33 status.capture_armable = !g_host.stopping && g_host.pending_op == PendingOp::None && lease_matches &&
872
2/2
✓ Branch 26 → 27 taken 21 times.
✓ Branch 26 → 28 taken 2 times.
23 g_host.route_state == DMK_WHEELHOST_ROUTE_READY
873
2/2
✓ Branch 23 → 24 taken 33 times.
✓ Branch 23 → 28 taken 1 time.
67 ? 1u
874 : 0u;
875
2/2
✓ Branch 29 → 30 taken 28 times.
✓ Branch 29 → 31 taken 6 times.
34 status.mounted_thread_id = g_host.hook != nullptr ? g_host.target_thread_id : 0;
876 34 status.mount_generation = g_host.mount_generation;
877 34 *out_status = status;
878 34 return DMK_WHEELHOST_OK;
879 36 }
880
881 int32_t DMK_WHEELHOST_CALL
882 16 retarget(void *host_context, WheelHostLease lease, std::uint32_t target_thread_id) noexcept
883 {
884
3/6
✓ Branch 3 → 4 taken 16 times.
✗ Branch 3 → 5 not taken.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 16 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 16 times.
16 if (!valid_context(host_context) || target_thread_id == 0)
885 {
886 return DMK_WHEELHOST_ERR_INVALID;
887 }
888 16 const ControlLock lock(g_host.control_lock);
889
4/4
✓ Branch 10 → 11 taken 6 times.
✓ Branch 10 → 13 taken 10 times.
✓ Branch 11 → 12 taken 5 times.
✓ Branch 11 → 13 taken 1 time.
16 const bool retrying = g_host.pending_op == PendingOp::Retarget && lease == g_host.pending_lease;
890
4/4
✓ Branch 14 → 15 taken 7 times.
✓ Branch 14 → 17 taken 9 times.
✓ Branch 15 → 16 taken 2 times.
✓ Branch 15 → 17 taken 5 times.
16 if (g_host.pending_op != PendingOp::None && !retrying)
891 {
892 2 return DMK_WHEELHOST_ERR_PENDING;
893 }
894
2/4
✓ Branch 17 → 18 taken 14 times.
✗ Branch 17 → 19 not taken.
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 14 times.
14 if (g_host.stopping || !g_host.started)
895 {
896 return DMK_WHEELHOST_ERR_STATE;
897 }
898
1/2
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 14 times.
14 if (g_host.lease == 0)
899 {
900 return DMK_WHEELHOST_ERR_NO_LEASE;
901 }
902
1/2
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 14 times.
14 if (lease != g_host.lease)
903 {
904 return DMK_WHEELHOST_ERR_STALE;
905 }
906
907 14 settle_route_state();
908
909 // Same-thread retarget of a live mount keeps the mount and its generation. Queue-wide admission needs no
910 // republish for a window change on the same thread.
911
4/4
✓ Branch 25 → 26 taken 11 times.
✓ Branch 25 → 34 taken 3 times.
✓ Branch 26 → 27 taken 2 times.
✓ Branch 26 → 34 taken 9 times.
14 if (g_host.hook != nullptr && g_host.target_thread_id == target_thread_id &&
912
1/2
✓ Branch 27 → 28 taken 2 times.
✗ Branch 27 → 34 not taken.
2 g_host.route_state == DMK_WHEELHOST_ROUTE_READY)
913 {
914
2/2
✓ Branch 28 → 29 taken 1 time.
✓ Branch 28 → 33 taken 1 time.
2 if (retrying)
915 {
916
1/2
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 32 taken 1 time.
1 if (!drain_admitted_phases())
917 {
918 return DMK_WHEELHOST_ERR_DRAIN;
919 }
920 1 clear_pending();
921 }
922 2 return DMK_WHEELHOST_OK;
923 }
924
925 // Disable capture and consume, advance the epoch, and drain admitted decisions before route replacement.
926 12 g_host.invalidate_capture();
927 6 const auto fail_pending = [&](std::uint32_t state, int32_t status) noexcept -> int32_t
928 {
929 6 g_host.route_state = state;
930 6 g_host.pending_op = PendingOp::Retarget;
931 6 g_host.pending_lease = lease;
932 6 return status;
933 12 };
934
2/2
✓ Branch 36 → 37 taken 2 times.
✓ Branch 36 → 41 taken 10 times.
12 if (!drain_admitted_phases())
935 {
936 2 const std::uint32_t state =
937
1/2
✓ Branch 37 → 38 taken 2 times.
✗ Branch 37 → 39 not taken.
2 g_host.hook != nullptr ? DMK_WHEELHOST_ROUTE_READY : DMK_WHEELHOST_ROUTE_RETRYABLE;
938 2 return fail_pending(state, DMK_WHEELHOST_ERR_DRAIN);
939 }
940
941 // Remove the old hook before the new hook mounts. Hooks never overlap.
942
2/2
✓ Branch 41 → 42 taken 7 times.
✓ Branch 41 → 52 taken 3 times.
10 if (g_host.hook != nullptr)
943 {
944
1/2
✗ Branch 43 → 44 not taken.
✓ Branch 43 → 45 taken 7 times.
7 if (target_thread_exited())
945 {
946 retire_route_after_thread_exit();
947 }
948
2/2
✓ Branch 46 → 47 taken 1 time.
✓ Branch 46 → 48 taken 6 times.
7 else if (!remove_hook(g_host.hook))
949 {
950 1 return fail_pending(DMK_WHEELHOST_ROUTE_CLEANUP_BLOCKED, DMK_WHEELHOST_ERR_THREAD);
951 }
952 else
953 {
954 6 g_host.hook = nullptr;
955
1/2
✓ Branch 48 → 49 taken 6 times.
✗ Branch 48 → 51 not taken.
6 if (g_host.target_thread != nullptr)
956 {
957 6 CloseHandle(g_host.target_thread);
958 6 g_host.target_thread = nullptr;
959 }
960 6 g_host.target_thread_id = 0;
961 }
962 }
963
964 9 HANDLE new_thread = open_target_thread(target_thread_id);
965
2/2
✓ Branch 53 → 54 taken 3 times.
✓ Branch 53 → 55 taken 6 times.
9 if (new_thread == nullptr)
966 {
967 3 return fail_pending(DMK_WHEELHOST_ROUTE_RETRYABLE, DMK_WHEELHOST_ERR_THREAD);
968 }
969 6 const HHOOK new_hook = mount_resident_hook(target_thread_id);
970
1/2
✗ Branch 56 → 57 not taken.
✓ Branch 56 → 59 taken 6 times.
6 if (new_hook == nullptr)
971 {
972 CloseHandle(new_thread);
973 return fail_pending(DMK_WHEELHOST_ROUTE_RETRYABLE, DMK_WHEELHOST_ERR_THREAD);
974 }
975
976 6 g_host.hook = new_hook;
977 6 g_host.target_thread = new_thread;
978 6 g_host.target_thread_id = target_thread_id;
979 6 g_host.mount_generation = next_nonzero(g_host.mount_generation, std::numeric_limits<std::uint64_t>::max());
980 6 g_host.route_state = DMK_WHEELHOST_ROUTE_READY;
981 6 clear_pending();
982 6 return DMK_WHEELHOST_OK;
983 16 }
984 } // namespace
985
986 49 int32_t DMK_WHEELHOST_CALL wheel_host_start(
987 uint32_t target_thread_id,
988 uint32_t requested_abi_version,
989 uint32_t table_capacity,
990 WheelHostTable *out_table
991 ) noexcept
992 {
993
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 48 times.
49 if (out_table == nullptr)
994 {
995 1 return DMK_WHEELHOST_ERR_INVALID;
996 }
997
4/4
✓ Branch 4 → 5 taken 46 times.
✓ Branch 4 → 6 taken 2 times.
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 45 times.
48 if (requested_abi_version != DMK_WHEELHOST_ABI_VERSION || table_capacity < sizeof(WheelHostTable))
998 {
999 3 return DMK_WHEELHOST_ERR_ABI;
1000 }
1001
1002 45 const ControlLock lock(g_host.control_lock);
1003
2/2
✓ Branch 8 → 9 taken 2 times.
✓ Branch 8 → 10 taken 43 times.
45 if (g_host.started)
1004 {
1005 2 return DMK_WHEELHOST_ERR_STATE;
1006 }
1007
1/2
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 43 times.
43 if (!pin_host_module())
1008 {
1009 return DMK_WHEELHOST_ERR_THREAD;
1010 }
1011
1012 43 HANDLE target_thread = nullptr;
1013 43 HHOOK hook = nullptr;
1014
2/2
✓ Branch 13 → 14 taken 41 times.
✓ Branch 13 → 22 taken 2 times.
43 if (target_thread_id != 0)
1015 {
1016 41 target_thread = open_target_thread(target_thread_id);
1017
1/2
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 41 times.
41 if (target_thread == nullptr)
1018 {
1019 return DMK_WHEELHOST_ERR_THREAD;
1020 }
1021 41 hook = mount_resident_hook(target_thread_id);
1022
1/2
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 22 taken 41 times.
41 if (hook == nullptr)
1023 {
1024 CloseHandle(target_thread);
1025 (void)stop_mount_thread();
1026 return DMK_WHEELHOST_ERR_THREAD;
1027 }
1028 }
1029
1030 43 g_host.hook = hook;
1031 43 g_host.target_thread = target_thread;
1032
2/2
✓ Branch 22 → 23 taken 41 times.
✓ Branch 22 → 24 taken 2 times.
43 g_host.target_thread_id = hook != nullptr ? target_thread_id : 0;
1033
2/2
✓ Branch 25 → 26 taken 41 times.
✓ Branch 25 → 27 taken 2 times.
43 g_host.route_state = hook != nullptr ? DMK_WHEELHOST_ROUTE_READY : DMK_WHEELHOST_ROUTE_TARGET_WAIT;
1034
2/2
✓ Branch 28 → 29 taken 41 times.
✓ Branch 28 → 32 taken 2 times.
43 if (hook != nullptr)
1035 {
1036 41 g_host.mount_generation = next_nonzero(g_host.mount_generation, std::numeric_limits<std::uint64_t>::max());
1037 }
1038 43 g_host.started = true;
1039 43 g_host.stopping = false;
1040 43 g_host.lease = 0;
1041 43 g_host.owner = 0;
1042 43 g_host.generation = 0;
1043 43 clear_pending();
1044 43 g_host.invalidate_capture();
1045 43 g_host.identity_counter = next_nonzero(g_host.identity_counter, std::numeric_limits<std::uint64_t>::max());
1046 43 g_host.host_identity = g_host.identity_counter;
1047
1048 43 WheelHostTable table{};
1049 43 table.struct_size = static_cast<std::uint32_t>(sizeof(WheelHostTable));
1050 43 table.abi_version = DMK_WHEELHOST_ABI_VERSION;
1051 43 table.capability_bits =
1052 DMK_WHEELHOST_CAP_VERTICAL | DMK_WHEELHOST_CAP_HORIZONTAL | DMK_WHEELHOST_CAP_CONSUME | DMK_WHEELHOST_CAP_ROUTE;
1053 43 table.host_identity = g_host.host_identity;
1054 43 table.host_context = &g_host;
1055 43 table.open_lease = &open_lease;
1056 43 table.publish_capture = &publish_capture;
1057 43 table.drain_counts = &drain_counts;
1058 43 table.close_lease = &close_lease;
1059 43 table.route_status = &route_status;
1060 43 table.retarget = &retarget;
1061 43 *out_table = table;
1062 43 return DMK_WHEELHOST_OK;
1063 45 }
1064
1065 48 int32_t DMK_WHEELHOST_CALL wheel_host_stop(void) noexcept
1066 {
1067 48 const ControlLock lock(g_host.control_lock);
1068
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 47 times.
48 if (!g_host.started)
1069 {
1070 1 return DMK_WHEELHOST_ERR_STATE;
1071 }
1072 // Stop is the loader authority over a host it started. It retries its own pending Stop and supersedes a
1073 // pending Close, whose generation already asked to leave and can no longer be required to retry. A lease whose
1074 // holder still wants it stays BUSY, which includes a pending Retarget: that generation can still close it.
1075
6/6
✓ Branch 5 → 6 taken 45 times.
✓ Branch 5 → 9 taken 2 times.
✓ Branch 6 → 7 taken 44 times.
✓ Branch 6 → 9 taken 1 time.
✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 9 taken 42 times.
47 if (g_host.pending_op != PendingOp::Stop && g_host.pending_op != PendingOp::Close && g_host.lease != 0)
1076 {
1077 // Busy without mutation: the lease keeps its owner, generation, token, route, and capture state.
1078 2 return DMK_WHEELHOST_ERR_BUSY;
1079 }
1080
1081
2/2
✓ Branch 9 → 10 taken 43 times.
✓ Branch 9 → 11 taken 2 times.
45 if (!g_host.stopping)
1082 {
1083 43 g_host.stopping = true;
1084 43 g_host.invalidate_capture();
1085 }
1086
2/2
✓ Branch 12 → 13 taken 1 time.
✓ Branch 12 → 14 taken 44 times.
45 if (!drain_admitted_phases())
1087 {
1088 // The host stays started and disabled through the exact Stop retry.
1089 1 g_host.pending_op = PendingOp::Stop;
1090 1 return DMK_WHEELHOST_ERR_DRAIN;
1091 }
1092
8/8
✓ Branch 14 → 15 taken 42 times.
✓ Branch 14 → 20 taken 2 times.
✓ Branch 16 → 17 taken 4 times.
✓ Branch 16 → 20 taken 38 times.
✓ Branch 18 → 19 taken 1 time.
✓ Branch 18 → 20 taken 3 times.
✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 23 taken 43 times.
44 if (g_host.hook != nullptr && !remove_hook(g_host.hook) && !target_thread_exited())
1093 {
1094 1 g_host.pending_op = PendingOp::Stop;
1095 1 return DMK_WHEELHOST_ERR_THREAD;
1096 }
1097 43 g_host.hook = nullptr;
1098 43 g_host.route_state = DMK_WHEELHOST_ROUTE_TARGET_WAIT;
1099
1/2
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 26 taken 43 times.
43 if (!stop_mount_thread())
1100 {
1101 g_host.pending_op = PendingOp::Stop;
1102 return DMK_WHEELHOST_ERR_THREAD;
1103 }
1104
1105
2/2
✓ Branch 26 → 27 taken 41 times.
✓ Branch 26 → 29 taken 2 times.
43 if (g_host.target_thread != nullptr)
1106 {
1107 41 CloseHandle(g_host.target_thread);
1108 41 g_host.target_thread = nullptr;
1109 }
1110 43 g_host.target_thread_id = 0;
1111 43 g_host.route_state = DMK_WHEELHOST_ROUTE_TARGET_WAIT;
1112 43 g_host.lease = 0;
1113 43 g_host.owner = 0;
1114 43 g_host.generation = 0;
1115 43 clear_pending();
1116 43 g_host.started = false;
1117 43 g_host.stopping = false;
1118 43 return DMK_WHEELHOST_OK;
1119 48 }
1120
1121 #if defined(DMK_WHEELHOST_ENABLE_TEST_SEAMS)
1122 42 extern "C" void DMK_WHEELHOST_CALL wheel_host_test_set_hook_probe(void(DMK_WHEELHOST_CALL *probe)(void)) noexcept
1123 {
1124 42 g_hook_probe.store(probe, std::memory_order_release);
1125 42 }
1126
1127 40 extern "C" void DMK_WHEELHOST_CALL wheel_host_test_set_finalize_probe(void(DMK_WHEELHOST_CALL *probe)(void)) noexcept
1128 {
1129 40 g_finalize_probe.store(probe, std::memory_order_release);
1130 40 }
1131
1132 38 extern "C" void DMK_WHEELHOST_CALL wheel_host_test_force_unhook_failure(uint32_t enabled) noexcept
1133 {
1134 38 g_force_unhook_failure.store(enabled != 0, std::memory_order_release);
1135 38 }
1136
1137 39 extern "C" void DMK_WHEELHOST_CALL wheel_host_test_set_drain_timeout(uint32_t timeout_ms) noexcept
1138 {
1139 g_drain_timeout_override_ms.store(timeout_ms, std::memory_order_release);
1140 39 }
1141
1142 36 extern "C" void DMK_WHEELHOST_CALL wheel_host_test_set_process_focus(int32_t focused) noexcept
1143 {
1144 g_process_focus_override.store(focused, std::memory_order_release);
1145 36 }
1146
1147 102 extern "C" void DMK_WHEELHOST_CALL wheel_host_test_snapshot(
1148 uint32_t *mounted_hooks,
1149 uint32_t *thread_handles,
1150 uint32_t *active_leases,
1151 uint64_t *mount_generation
1152 ) noexcept
1153 {
1154 102 const ControlLock lock(g_host.control_lock);
1155
1/2
✓ Branch 3 → 4 taken 102 times.
✗ Branch 3 → 8 not taken.
102 if (mounted_hooks != nullptr)
1156 {
1157
2/2
✓ Branch 4 → 5 taken 101 times.
✓ Branch 4 → 6 taken 1 time.
102 *mounted_hooks = g_host.hook != nullptr ? 1u : 0u;
1158 }
1159
1/2
✓ Branch 8 → 9 taken 102 times.
✗ Branch 8 → 16 not taken.
102 if (thread_handles != nullptr)
1160 {
1161
4/4
✓ Branch 9 → 10 taken 101 times.
✓ Branch 9 → 11 taken 1 time.
✓ Branch 12 → 13 taken 101 times.
✓ Branch 12 → 14 taken 1 time.
102 *thread_handles = (g_host.target_thread != nullptr ? 1u : 0u) + (g_host.mount_thread != nullptr ? 1u : 0u);
1162 }
1163
2/2
✓ Branch 16 → 17 taken 101 times.
✓ Branch 16 → 21 taken 1 time.
102 if (active_leases != nullptr)
1164 {
1165
1/2
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 101 times.
101 *active_leases = g_host.lease != 0 ? 1u : 0u;
1166 }
1167
2/2
✓ Branch 21 → 22 taken 101 times.
✓ Branch 21 → 23 taken 1 time.
102 if (mount_generation != nullptr)
1168 {
1169 101 *mount_generation = g_host.mount_generation;
1170 }
1171 102 }
1172 #endif
1173