GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 92.5% 37 / 0 / 40
Functions: 100.0% 2 / 0 / 2
Branches: 66.7% 36 / 0 / 54

src/config_input_fusion.hpp
Line Branch Exec Source
1 #ifndef DETOURMODKIT_CONFIG_INPUT_FUSION_HPP
2 #define DETOURMODKIT_CONFIG_INPUT_FUSION_HPP
3
4 /**
5 * @file config_input_fusion.hpp
6 * @brief Internal teardown gate shared by the Config combo-binding fusions (register_press_combo /
7 * register_hold_combo).
8 * @details Holds the per-binding HoldGate that gives a hold binding's RAII guard a correct cancellation lifecycle: a
9 * cancelled hold must deliver exactly one balancing on_state_change(false) and never let a stale true land
10 * after it. The logic lives in src/ (not in an anonymous namespace inside config.cpp) so the synchronization
11 * can be unit-tested directly, mirroring src/input_intercept.hpp and src/x86_decode.hpp. Not installed and not
12 * part of the public API.
13 */
14
15 #include <atomic>
16 #include <functional>
17 #include <memory>
18 #include <mutex>
19
20 namespace DetourModKit
21 {
22 namespace detail
23 {
24 /**
25 * @struct HoldGate
26 * @brief Per-binding teardown gate shared between a hold binding's callback wrapper and its guard.
27 * @details A hold callback carries lingering state: the consumer is told true (held) until told false
28 * (released). Cancelling the binding mid-hold must therefore deliver exactly one balancing false and
29 * never let a stale true land after it. The poller invokes the wrapper on edges from the poll thread,
30 * update_binding_combos / shutdown can invoke it from another thread, and the guard's release() runs
31 * on a control-plane thread -- all of them route through this gate.
32 *
33 * The recursive_mutex serializes every delivery against teardown: a cross-thread release() waits
34 * behind any in-flight callback (so it observes the true last-delivered state and cannot interleave),
35 * while a re-entrant self-release from inside the callback on the same thread re-locks without
36 * deadlocking and defers its balancing edge to the wrapper's unwind. Both invocation paths already run
37 * outside every InputPoller lock, so this gate is a leaf lock with no ordering inversion.
38 */
39 struct HoldGate
40 {
41 std::recursive_mutex mutex;
42 std::shared_ptr<std::atomic<bool>> enabled;
43 std::function<void(bool)> on_state_change;
44
45 // A true edge was delivered to the user and has not yet been balanced by a false edge.
46 bool forwarded_active = false;
47 // The guard has torn the binding down; further edges are swallowed.
48 bool released = false;
49 // The user callback is currently on this thread's stack.
50 bool delivering = false;
51 // A self-release happened mid-delivery; emit the balancing false on unwind.
52 bool deferred_final = false;
53
54 /**
55 * @brief Wrapper invoked by InputManager on each hold edge; gates the edge and forwards to the user
56 * callback.
57 */
58 16166 void deliver(bool active)
59 {
60
1/2
✓ Branch 2 → 3 taken 16166 times.
✗ Branch 2 → 50 not taken.
16166 std::lock_guard<std::recursive_mutex> lock(mutex);
61
2/2
✓ Branch 3 → 4 taken 203 times.
✓ Branch 3 → 5 taken 15963 times.
16166 if (released)
62 {
63 203 return;
64 }
65
3/6
✓ Branch 6 → 7 taken 15963 times.
✗ Branch 6 → 11 not taken.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 15963 times.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 15963 times.
15963 if (enabled && !enabled->load(std::memory_order_acquire))
66 {
67 return;
68 }
69 15963 forwarded_active = active;
70 15963 delivering = true;
71 try
72 {
73
1/2
✓ Branch 15 → 16 taken 15963 times.
✗ Branch 15 → 17 not taken.
15963 if (on_state_change)
74 {
75
2/2
✓ Branch 16 → 17 taken 15961 times.
✓ Branch 16 → 32 taken 2 times.
15963 on_state_change(active);
76 }
77 }
78 2 catch (...)
79 {
80 // Restore the gate invariant before the exception unwinds to the poller's callback handler (which
81 // logs it). Leaving `delivering` true would make a later release() defer its balancing edge
82 // forever.
83 2 delivering = false;
84 // A self-release that ran inside this now-throwing delivery set released = true and deferred its
85 // balancing false to the drain below, which the throw would skip; a later release() then
86 // short-circuits on `released` and the consumer is stranded observing the stale true. Drain it
87 // here, swallowing any secondary throw so the original exception is the one that surfaces.
88
2/2
✓ Branch 34 → 35 taken 1 time.
✓ Branch 34 → 42 taken 1 time.
2 if (deferred_final)
89 {
90 1 deferred_final = false;
91
3/6
✓ Branch 35 → 36 taken 1 time.
✗ Branch 35 → 39 not taken.
✓ Branch 37 → 38 taken 1 time.
✗ Branch 37 → 39 not taken.
✓ Branch 40 → 41 taken 1 time.
✗ Branch 40 → 42 not taken.
1 if (forwarded_active && on_state_change)
92 {
93 1 forwarded_active = false;
94 try
95 {
96
1/2
✓ Branch 41 → 42 taken 1 time.
✗ Branch 41 → 43 not taken.
1 on_state_change(false);
97 }
98 catch (...)
99 {
100 }
101 }
102 }
103 2 throw;
104 2 }
105 15961 delivering = false;
106
107 // A release() that arrived while the user callback was on this stack deferred its balancing edge here,
108 // so the callback is never re-entered while still executing. Fire it once the stack has unwound, if a
109 // true edge is still outstanding.
110
2/2
✓ Branch 17 → 18 taken 1 time.
✓ Branch 17 → 25 taken 15960 times.
15961 if (deferred_final)
111 {
112 1 deferred_final = false;
113
3/6
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 22 not taken.
✓ Branch 20 → 21 taken 1 time.
✗ Branch 20 → 22 not taken.
✓ Branch 23 → 24 taken 1 time.
✗ Branch 23 → 25 not taken.
1 if (forwarded_active && on_state_change)
114 {
115 1 forwarded_active = false;
116
1/2
✓ Branch 24 → 25 taken 1 time.
✗ Branch 24 → 48 not taken.
1 on_state_change(false);
117 }
118 }
119
2/2
✓ Branch 27 → 28 taken 15961 times.
✓ Branch 27 → 30 taken 203 times.
16166 }
120
121 /**
122 * @brief Guard teardown: disables further delivery and synthesizes one balancing false if still held.
123 */
124 213 void release()
125 {
126
1/2
✓ Branch 2 → 3 taken 213 times.
✗ Branch 2 → 23 not taken.
213 std::lock_guard<std::recursive_mutex> lock(mutex);
127
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 212 times.
213 if (released)
128 {
129 1 return;
130 }
131 212 released = true;
132
2/2
✓ Branch 5 → 6 taken 2 times.
✓ Branch 5 → 7 taken 210 times.
212 if (delivering)
133 {
134 // Re-entrant self-release from inside on_state_change on this same thread (the recursive_mutex let
135 // us re-lock without waiting on ourselves). Defer the balancing edge to deliver()'s unwind rather
136 // than re-entering the user callback while it is still on the stack.
137 2 deferred_final = true;
138 2 return;
139 }
140 // Cross-thread or post-delivery release. The lock serialized us behind any in-flight delivery, so
141 // forwarded_active reflects the last edge actually delivered; balance a still-held binding with one
142 // synthetic false. No edge can race past this because `released` is now set under the same lock the
143 // wrapper takes.
144
5/6
✓ Branch 7 → 8 taken 105 times.
✓ Branch 7 → 11 taken 105 times.
✓ Branch 9 → 10 taken 105 times.
✗ Branch 9 → 11 not taken.
✓ Branch 12 → 13 taken 105 times.
✓ Branch 12 → 14 taken 105 times.
210 if (forwarded_active && on_state_change)
145 {
146 105 forwarded_active = false;
147
1/2
✓ Branch 13 → 14 taken 105 times.
✗ Branch 13 → 21 not taken.
105 on_state_change(false);
148 }
149
2/2
✓ Branch 16 → 17 taken 210 times.
✓ Branch 16 → 19 taken 3 times.
213 }
150 };
151 } // namespace detail
152 } // namespace DetourModKit
153
154 #endif // DETOURMODKIT_CONFIG_INPUT_FUSION_HPP
155