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 |