src/internal/input_binding_gate.hpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #ifndef DETOURMODKIT_INTERNAL_INPUT_BINDING_GATE_HPP | ||
| 2 | #define DETOURMODKIT_INTERNAL_INPUT_BINDING_GATE_HPP | ||
| 3 | |||
| 4 | /** | ||
| 5 | * @file input_binding_gate.hpp | ||
| 6 | * @brief Provides per-binding teardown gates for press and hold callbacks. | ||
| 7 | * @details Each gate serializes callback delivery against teardown. Consumer callbacks and capture destructors run | ||
| 8 | * outside the gate mutex. | ||
| 9 | * | ||
| 10 | * A control-plane release waits for every delivery and teardown span. Retirement uses a deadline and refuses | ||
| 11 | * if it cannot prove quiescence. A caller must hold no lock that consumer code can acquire. It must own no | ||
| 12 | * join that consumer code can await. | ||
| 13 | * | ||
| 14 | * A callback-side release never waits. It defers the final Hold edge to delivery unwind or emits that edge | ||
| 15 | * inline when no delivery exists. | ||
| 16 | * | ||
| 17 | * docs/design/input.md owns the delivery-scope and ABBA rationale. This private header keeps the | ||
| 18 | * synchronization unit-testable. | ||
| 19 | */ | ||
| 20 | |||
| 21 | #include "internal/input_binding_lifecycle.hpp" | ||
| 22 | #include "internal/input_delivery_scope.hpp" | ||
| 23 | |||
| 24 | #include <atomic> | ||
| 25 | #include <chrono> | ||
| 26 | #include <condition_variable> | ||
| 27 | #include <cstddef> | ||
| 28 | #include <exception> | ||
| 29 | #include <functional> | ||
| 30 | #include <memory> | ||
| 31 | #include <mutex> | ||
| 32 | #include <utility> | ||
| 33 | |||
| 34 | namespace DetourModKit | ||
| 35 | { | ||
| 36 | namespace detail | ||
| 37 | { | ||
| 38 | /** | ||
| 39 | * @class GateCallbackSlot | ||
| 40 | * @brief Owns one gate callback behind a stable heap address. | ||
| 41 | * @details A std::function move can destroy an inline source target. Retirement moves only the pointer, so the | ||
| 42 | * mutex never covers consumer destruction. | ||
| 43 | */ | ||
| 44 | template <typename Signature> class GateCallbackSlot; | ||
| 45 | |||
| 46 | template <typename Return, typename... Args> class GateCallbackSlot<Return(Args...)> | ||
| 47 | { | ||
| 48 | public: | ||
| 49 | using Function = std::function<Return(Args...)>; | ||
| 50 | using Owner = std::unique_ptr<Function>; | ||
| 51 | |||
| 52 | 1702 | GateCallbackSlot() noexcept = default; | |
| 53 | 1693 | ~GateCallbackSlot() noexcept = default; | |
| 54 | |||
| 55 | GateCallbackSlot(const GateCallbackSlot &) = delete; | ||
| 56 | GateCallbackSlot &operator=(const GateCallbackSlot &) = delete; | ||
| 57 | GateCallbackSlot(GateCallbackSlot &&) = delete; | ||
| 58 | GateCallbackSlot &operator=(GateCallbackSlot &&) = delete; | ||
| 59 | |||
| 60 | /// Installs a callback during unpublished gate setup and stores no owner for an empty callback. | ||
| 61 | 1702 | GateCallbackSlot &operator=(Function callback) | |
| 62 | { | ||
| 63 | 1702 | Owner replacement; | |
| 64 |
3/4DetourModKit::detail::GateCallbackSlot<void (bool)>::operator=(std::function<void (bool)>):
✓ Branch 3 → 4 taken 698 times.
✗ Branch 3 → 10 not taken.
DetourModKit::detail::GateCallbackSlot<void ()>::operator=(std::function<void ()>):
✓ Branch 3 → 4 taken 803 times.
✓ Branch 3 → 10 taken 201 times.
|
1702 | if (callback) |
| 65 | { | ||
| 66 |
2/4DetourModKit::detail::GateCallbackSlot<void (bool)>::operator=(std::function<void (bool)>):
✓ Branch 6 → 7 taken 698 times.
✗ Branch 6 → 16 not taken.
DetourModKit::detail::GateCallbackSlot<void ()>::operator=(std::function<void ()>):
✓ Branch 6 → 7 taken 803 times.
✗ Branch 6 → 16 not taken.
|
1501 | replacement = std::make_unique<Function>(std::move(callback)); |
| 67 | } | ||
| 68 | 1700 | m_callback = std::move(replacement); | |
| 69 | 1701 | return *this; | |
| 70 | 1701 | } | |
| 71 | |||
| 72 | /// Clears a callback during unpublished gate setup. | ||
| 73 | 1 | GateCallbackSlot &operator=(std::nullptr_t) noexcept | |
| 74 | { | ||
| 75 | 1 | m_callback.reset(); | |
| 76 | 1 | return *this; | |
| 77 | } | ||
| 78 | |||
| 79 | /// Returns true when this slot owns a callback. | ||
| 80 | 22448 | [[nodiscard]] explicit operator bool() const noexcept { return m_callback != nullptr; } | |
| 81 | |||
| 82 | /// Invokes the owned callback. | ||
| 83 | 44880 | Return operator()(Args... args) const { return (*m_callback)(std::forward<Args>(args)...); } | |
| 84 | |||
| 85 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 86 | /// Installs a test-only probe that runs immediately after ownership transfer. | ||
| 87 | 2 | void set_after_take_probe_for_test(void (*probe)(void *) noexcept, void *context) noexcept | |
| 88 | { | ||
| 89 | 2 | m_after_take_probe_for_test = probe; | |
| 90 | 2 | m_after_take_context_for_test = context; | |
| 91 | 2 | } | |
| 92 | #endif | ||
| 93 | |||
| 94 | /// Transfers callback ownership by pointer while the std::function target stays at one address. | ||
| 95 | 151 | [[nodiscard]] Owner take() noexcept | |
| 96 | { | ||
| 97 | 302 | Owner callback = std::move(m_callback); | |
| 98 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 99 |
4/4DetourModKit::detail::GateCallbackSlot<void (bool)>::take():
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 21 times.
DetourModKit::detail::GateCallbackSlot<void ()>::take():
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 128 times.
|
151 | if (m_after_take_probe_for_test != nullptr) |
| 100 | { | ||
| 101 | 2 | m_after_take_probe_for_test(m_after_take_context_for_test); | |
| 102 | } | ||
| 103 | #endif | ||
| 104 | 151 | return callback; | |
| 105 | } | ||
| 106 | |||
| 107 | private: | ||
| 108 | Owner m_callback; | ||
| 109 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 110 | void (*m_after_take_probe_for_test)(void *) noexcept = nullptr; | ||
| 111 | void *m_after_take_context_for_test = nullptr; | ||
| 112 | #endif | ||
| 113 | }; | ||
| 114 | |||
| 115 | /** | ||
| 116 | * @struct BindingGate | ||
| 117 | * @brief Type-erased handle a binding's engine entry keeps on the gate its wrappers dispatch through. | ||
| 118 | * @details A gate has two strong owners, the poller's exploded engine entries and the BindingGuard's one-shot | ||
| 119 | * release closure, and it owns the consumer callback. Dropping the engine entries therefore retires | ||
| 120 | * the binding without destroying the callback, which is safe for an ordinary reshape and unsafe | ||
| 121 | * before a Logic DLL is unmapped. This base gives the unload drain a way to reach the gate itself. | ||
| 122 | */ | ||
| 123 | struct BindingGate | ||
| 124 | { | ||
| 125 | 1693 | virtual ~BindingGate() = default; | |
| 126 | |||
| 127 | /** | ||
| 128 | * @brief Ends delivery, emits a still-held hold's balancing edge, and destroys the consumer callback. | ||
| 129 | * @param deadline Bound on the wait for an in-flight delivery to unwind. | ||
| 130 | * @return False when the deadline expired with a delivery still running; the callback is then retained, | ||
| 131 | * because destroying a callable a poll thread is executing would free code out from under it. | ||
| 132 | * @details Idempotent. Unlike @c release(), which leaves the callback in place for the binding's guard, | ||
| 133 | * this hands ownership of the callback to the calling thread and destroys it there, so no later | ||
| 134 | * guard release can reach it. Control-plane only: the wait would deadlock a caller that is | ||
| 135 | * itself inside a delivery. | ||
| 136 | */ | ||
| 137 | [[nodiscard]] virtual bool retire(std::chrono::steady_clock::time_point deadline) = 0; | ||
| 138 | }; | ||
| 139 | |||
| 140 | /** | ||
| 141 | * @struct HoldGate | ||
| 142 | * @brief Per-binding teardown gate shared between a hold binding's callback wrapper and its guard. | ||
| 143 | * @details A hold callback carries lingering state: the consumer is told true (held) until told false | ||
| 144 | * (released), so cancelling mid-hold must deliver exactly one balancing false and never let a stale | ||
| 145 | * true land after it. A multi-combo hold ("X = combo A | combo B") explodes into N engine entries that | ||
| 146 | * all share ONE gate; the gate reference-counts the held entries in @ref active_entries and forwards | ||
| 147 | * only the 0->1 and 1->0 crossings, so the consumer sees "held" for the whole span any combo is down. | ||
| 148 | * | ||
| 149 | * Deliveries arrive from the poll thread (edges) and from control threads (a reshape's synchronized | ||
| 150 | * released(false)); @ref lifecycle guards against a late true resurrecting a torn-down binding. | ||
| 151 | */ | ||
| 152 | struct HoldGate : BindingGate | ||
| 153 | { | ||
| 154 | std::mutex mutex; | ||
| 155 | std::condition_variable idle_cv; | ||
| 156 | std::shared_ptr<std::atomic<bool>> enabled; | ||
| 157 | // Shared with the binding's engine entries. Its tombstone is the resurrection guard below; may be null for | ||
| 158 | // a gate not tied to an engine entry (never happens in the facade, tolerated for direct unit use). | ||
| 159 | std::shared_ptr<BindingLifecycle> lifecycle; | ||
| 160 | GateCallbackSlot<void(bool)> on_state_change; | ||
| 161 | |||
| 162 | // Exploded entries sharing this gate that are currently held; consumer-visible held state is (count > 0). | ||
| 163 | int active_entries = 0; | ||
| 164 | // A true edge was delivered to the user and not yet balanced by a false edge. | ||
| 165 | bool forwarded_active = false; | ||
| 166 | // The guard has torn the binding down; further edges are swallowed. | ||
| 167 | bool released = false; | ||
| 168 | // Callback invocations running outside the mutex, including a guard-release balancing edge. | ||
| 169 | int in_flight = 0; | ||
| 170 | // A release arrived while a delivery was in flight and could not block; the delivery emits the balancing | ||
| 171 | // false on its unwind. | ||
| 172 | bool deferred_final = false; | ||
| 173 | // A control-plane teardown (a guard release or the unload drain's retire) has marked the gate released but | ||
| 174 | // has not finished its consumer-code span: the release's balancing edge, or retirement's edge plus callable | ||
| 175 | // disposal. `in_flight` cannot express this on its own: the claimant drops the mutex to wait for deliveries | ||
| 176 | // to drain, so without a claim taken at the same moment as `released` a second teardown could observe | ||
| 177 | // `in_flight == 0` inside that window and report a quiesced gate while the first is about to enter the | ||
| 178 | // callback. | ||
| 179 | bool teardown_active = false; | ||
| 180 | // Native thread running that span, set and cleared with `teardown_active`. The same-gate recursion guard: | ||
| 181 | // it names the one claim a waiter must not wait for, its own. Win32 thread identity is allocation-free, | ||
| 182 | // unlike std::this_thread::get_id() on a foreign MinGW/winpthreads thread. | ||
| 183 | std::uint32_t teardown_owner = 0; | ||
| 184 | |||
| 185 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 186 | // Set these test pause points before worker start. Clear them after every worker joins. | ||
| 187 | void (*before_idle_wait_for_test)(void *) noexcept = nullptr; | ||
| 188 | void *before_idle_wait_context_for_test = nullptr; | ||
| 189 | void (*after_bookkeeping_for_test)(void *) noexcept = nullptr; | ||
| 190 | void *after_bookkeeping_context_for_test = nullptr; | ||
| 191 | #endif | ||
| 192 | |||
| 193 | /** | ||
| 194 | * @brief Holds one @ref in_flight slot for an unlocked callback. | ||
| 195 | * @details deliver() owns the primary callback slot through its critical sections. | ||
| 196 | * This holder covers a deferred false edge after that callback returns. | ||
| 197 | * release() and retire() wait for in_flight == 0 before they destroy consumer state. | ||
| 198 | * The holder releases its slot on every exit, even after an exception. | ||
| 199 | * TeardownScope covers consumer code that teardown invokes. | ||
| 200 | */ | ||
| 201 | struct InFlightSlot | ||
| 202 | { | ||
| 203 | 6 | explicit InFlightSlot(HoldGate *owner) noexcept : m_gate(owner) {} | |
| 204 | 6 | ~InFlightSlot() noexcept | |
| 205 | { | ||
| 206 | 6 | std::lock_guard<std::mutex> exit_lock(m_gate->mutex); | |
| 207 | 6 | --m_gate->in_flight; | |
| 208 | 6 | m_gate->idle_cv.notify_all(); | |
| 209 | 6 | } | |
| 210 | |||
| 211 | // One slot decrements exactly once, like the DeliveryScope it brackets. A copy would decrement twice | ||
| 212 | // and drive in_flight negative, at which point the == 0 predicate release() and retire() wait on is | ||
| 213 | // either already true while a callback runs or never true again. | ||
| 214 | InFlightSlot(const InFlightSlot &) = delete; | ||
| 215 | InFlightSlot &operator=(const InFlightSlot &) = delete; | ||
| 216 | InFlightSlot(InFlightSlot &&) = delete; | ||
| 217 | InFlightSlot &operator=(InFlightSlot &&) = delete; | ||
| 218 | |||
| 219 | private: | ||
| 220 | HoldGate *m_gate; | ||
| 221 | }; | ||
| 222 | |||
| 223 | /** | ||
| 224 | * @brief RAII holder for a teardown path's claim on @ref teardown_active. | ||
| 225 | * @details Claimed under the mutex at the same moment the path sets @ref released, and cleared once its | ||
| 226 | * consumer-code span is over. A release spans its balancing edge; retirement also spans callable | ||
| 227 | * disposal. A concurrent teardown therefore waits for the whole span rather than for the callback | ||
| 228 | * window alone. | ||
| 229 | */ | ||
| 230 | struct TeardownScope | ||
| 231 | { | ||
| 232 | 207 | explicit TeardownScope(HoldGate *owner) noexcept : m_gate(owner) {} | |
| 233 | 207 | ~TeardownScope() noexcept | |
| 234 | { | ||
| 235 | 207 | std::lock_guard<std::mutex> exit_lock(m_gate->mutex); | |
| 236 | 207 | m_gate->teardown_active = false; | |
| 237 | 207 | m_gate->teardown_owner = 0; | |
| 238 | 207 | m_gate->idle_cv.notify_all(); | |
| 239 | 207 | } | |
| 240 | |||
| 241 | // One claim, cleared exactly once, for the same reason InFlightSlot is neither copyable nor movable. | ||
| 242 | TeardownScope(const TeardownScope &) = delete; | ||
| 243 | TeardownScope &operator=(const TeardownScope &) = delete; | ||
| 244 | TeardownScope(TeardownScope &&) = delete; | ||
| 245 | TeardownScope &operator=(TeardownScope &&) = delete; | ||
| 246 | |||
| 247 | private: | ||
| 248 | HoldGate *m_gate; | ||
| 249 | }; | ||
| 250 | |||
| 251 | /** | ||
| 252 | * @brief Wrapper the poller (and a reshape's release path) invoke on each hold edge; forwards only the | ||
| 253 | * aggregate held/released transition to the user callback, run outside the mutex. | ||
| 254 | */ | ||
| 255 | 37493 | void deliver(bool active) | |
| 256 | { | ||
| 257 |
1/2✓ Branch 2 → 3 taken 37518 times.
✗ Branch 2 → 121 not taken.
|
37493 | std::unique_lock<std::mutex> lock(mutex); |
| 258 |
2/2✓ Branch 3 → 4 taken 11011 times.
✓ Branch 3 → 5 taken 26507 times.
|
37518 | if (released) |
| 259 | { | ||
| 260 | 11011 | return; | |
| 261 | } | ||
| 262 |
4/6✓ Branch 6 → 7 taken 21687 times.
✓ Branch 6 → 11 taken 4820 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 21687 times.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 26507 times.
|
26507 | if (enabled && !enabled->load(std::memory_order_acquire)) |
| 263 | { | ||
| 264 | ✗ | return; | |
| 265 | } | ||
| 266 | // Serialize this delivery's callback against any other in-flight delivery for this gate, so a true and | ||
| 267 | // a false forwarded from two threads reach the consumer in order. A delivery reached from inside a | ||
| 268 | // callback (depth > 0) skips the wait, so no wait chain runs through user code; the concurrent-delivery | ||
| 269 | // case that skip would otherwise expose is handled by the defer below. | ||
| 270 | 26507 | const bool in_callback = current_thread_in_delivery(); | |
| 271 |
2/2✓ Branch 15 → 16 taken 26505 times.
✓ Branch 15 → 30 taken 2 times.
|
26507 | if (!in_callback) |
| 272 | { | ||
| 273 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 274 |
2/2✓ Branch 16 → 17 taken 2 times.
✓ Branch 16 → 18 taken 26503 times.
|
26505 | if (before_idle_wait_for_test != nullptr) |
| 275 | { | ||
| 276 | 2 | before_idle_wait_for_test(before_idle_wait_context_for_test); | |
| 277 | } | ||
| 278 | #endif | ||
| 279 |
1/2✓ Branch 18 → 19 taken 26505 times.
✗ Branch 18 → 119 not taken.
|
53081 | idle_cv.wait(lock, [this] { return in_flight == 0; }); |
| 280 |
1/2✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 26505 times.
|
26505 | if (released) |
| 281 | { | ||
| 282 | ✗ | return; | |
| 283 | } | ||
| 284 | // Recheck every predicate after the wait. A concurrent disable must not wake this delivery into a | ||
| 285 | // stale callback (proof: GateRaceProbe.HoldGateDisableDuringParkedDeliveryEmitsNoStaleCallback). | ||
| 286 |
6/6✓ Branch 22 → 23 taken 21685 times.
✓ Branch 22 → 27 taken 4820 times.
✓ Branch 25 → 26 taken 1 time.
✓ Branch 25 → 27 taken 21684 times.
✓ Branch 28 → 29 taken 1 time.
✓ Branch 28 → 30 taken 26504 times.
|
26505 | if (enabled && !enabled->load(std::memory_order_acquire)) |
| 287 | { | ||
| 288 | 1 | return; | |
| 289 | } | ||
| 290 | } | ||
| 291 | // Resurrection guard: once the binding is tombstoned, refuse a new held(true) edge; a balancing | ||
| 292 | // released(false) still passes so a removed-while-held binding ends not-held regardless of the order a | ||
| 293 | // late poll-thread true and the reshape's false arrive in. | ||
| 294 |
6/8✓ Branch 30 → 31 taken 13349 times.
✓ Branch 30 → 37 taken 13157 times.
✓ Branch 32 → 33 taken 420 times.
✓ Branch 32 → 37 taken 12929 times.
✗ Branch 35 → 36 not taken.
✓ Branch 35 → 37 taken 420 times.
✗ Branch 38 → 39 not taken.
✓ Branch 38 → 40 taken 26506 times.
|
26506 | if (active && lifecycle && lifecycle->tombstoned()) |
| 295 | { | ||
| 296 | ✗ | return; | |
| 297 | } | ||
| 298 | |||
| 299 | 26506 | bool crosses_boundary = false; | |
| 300 |
2/2✓ Branch 40 → 41 taken 13349 times.
✓ Branch 40 → 42 taken 13157 times.
|
26506 | if (active) |
| 301 | { | ||
| 302 | 13349 | crosses_boundary = (active_entries == 0); | |
| 303 | 13349 | ++active_entries; | |
| 304 | } | ||
| 305 | else | ||
| 306 | { | ||
| 307 |
2/2✓ Branch 42 → 43 taken 5 times.
✓ Branch 42 → 44 taken 13152 times.
|
13157 | if (active_entries == 0) |
| 308 | { | ||
| 309 | 5 | return; | |
| 310 | } | ||
| 311 | 13152 | --active_entries; | |
| 312 | 13152 | crosses_boundary = (active_entries == 0); | |
| 313 | } | ||
| 314 |
2/2✓ Branch 45 → 46 taken 4253 times.
✓ Branch 45 → 47 taken 22248 times.
|
26501 | if (!crosses_boundary) |
| 315 | { | ||
| 316 | 4253 | return; | |
| 317 | } | ||
| 318 | |||
| 319 | // A delivery reached from inside a callback could not wait for an in-flight delivery to drain (that | ||
| 320 | // would risk the cross-binding deadlock the skip above avoids). If one is in flight on another thread, | ||
| 321 | // running this callback now would deliver two edges for one gate concurrently and out of decision order | ||
| 322 | // (a teardown false racing the poll thread's held true), which can strand the consumer observing the | ||
| 323 | // stale held. Defer this crossing to the in-flight delivery's unwind instead, so the consumer sees held | ||
| 324 | // then released in order with no concurrent callback. At depth > 0 the crossing edge is always a | ||
| 325 | // teardown false (only the poll cycle raises a held true, and it never runs inside a callback), so | ||
| 326 | // exactly one balancing false is owed and the in-flight delivery emits it. | ||
| 327 |
4/6✓ Branch 47 → 48 taken 2 times.
✓ Branch 47 → 51 taken 22246 times.
✓ Branch 48 → 49 taken 2 times.
✗ Branch 48 → 51 not taken.
✓ Branch 49 → 50 taken 2 times.
✗ Branch 49 → 51 not taken.
|
22248 | if (in_callback && in_flight > 0 && !active) |
| 328 | { | ||
| 329 | 2 | deferred_final = true; | |
| 330 | 2 | return; | |
| 331 | } | ||
| 332 | |||
| 333 | // Take this thread's delivery identity before the crossing commits to a callback, and give the edge up | ||
| 334 | // if it cannot be taken. Running the callback anyway would leave a control-plane release on another | ||
| 335 | // thread unable to see that this thread is inside consumer code, and that release promises its caller | ||
| 336 | // the opposite. Nothing consumer-visible has happened yet, so refusing here costs one edge; the entry | ||
| 337 | // count taken above is the only state to undo. Constructing under the gate mutex is what makes that | ||
| 338 | // undo a single decrement rather than a re-lock and a three-field rollback. | ||
| 339 | 22246 | DeliveryScope scope; | |
| 340 |
2/2✓ Branch 53 → 54 taken 4 times.
✓ Branch 53 → 58 taken 22242 times.
|
22246 | if (!scope.admitted()) |
| 341 | { | ||
| 342 |
2/2✓ Branch 54 → 55 taken 3 times.
✓ Branch 54 → 56 taken 1 time.
|
4 | if (active) |
| 343 | { | ||
| 344 | 3 | --active_entries; | |
| 345 | } | ||
| 346 | else | ||
| 347 | { | ||
| 348 | 1 | ++active_entries; | |
| 349 | } | ||
| 350 | 4 | return; | |
| 351 | } | ||
| 352 | |||
| 353 | 22242 | forwarded_active = active; | |
| 354 | 22242 | ++in_flight; | |
| 355 |
1/2✓ Branch 58 → 59 taken 22242 times.
✗ Branch 58 → 117 not taken.
|
22242 | lock.unlock(); |
| 356 | |||
| 357 | 22242 | std::exception_ptr err; | |
| 358 | try | ||
| 359 | { | ||
| 360 |
2/2✓ Branch 61 → 62 taken 22241 times.
✓ Branch 61 → 63 taken 1 time.
|
22242 | if (on_state_change) |
| 361 | { | ||
| 362 |
2/2✓ Branch 62 → 63 taken 22237 times.
✓ Branch 62 → 99 taken 2 times.
|
22241 | on_state_change(active); |
| 363 | } | ||
| 364 | } | ||
| 365 | 2 | catch (...) | |
| 366 | { | ||
| 367 | 2 | err = std::current_exception(); | |
| 368 |
1/2✓ Branch 104 → 63 taken 2 times.
✗ Branch 104 → 115 not taken.
|
2 | } |
| 369 | |||
| 370 | // Claim deferred_final in the final slot-release critical section. | ||
| 371 | // Separate decisions can let two exits miss the deferred false edge. | ||
| 372 | // Keep the claimed slot through the deferred callback. | ||
| 373 | // A release() or retire() waiter must not observe false quiescence | ||
| 374 | // (proof: GateRaceProbe.HoldGateDeferredFinalClaimSharesLastSlotRelease). | ||
| 375 | 22240 | bool emit_deferred = false; | |
| 376 | { | ||
| 377 |
1/2✓ Branch 63 → 64 taken 22230 times.
✗ Branch 63 → 105 not taken.
|
22240 | std::lock_guard<std::mutex> bookkeeping(mutex); |
| 378 | 22230 | --in_flight; | |
| 379 |
3/4✓ Branch 64 → 65 taken 22231 times.
✗ Branch 64 → 68 not taken.
✓ Branch 65 → 66 taken 372 times.
✓ Branch 65 → 68 taken 21859 times.
|
22230 | if (in_flight == 0 && deferred_final) |
| 380 | { | ||
| 381 | 372 | deferred_final = false; | |
| 382 | 372 | emit_deferred = forwarded_active; | |
| 383 | 372 | forwarded_active = false; | |
| 384 |
2/2✓ Branch 66 → 67 taken 6 times.
✓ Branch 66 → 68 taken 366 times.
|
372 | if (emit_deferred) |
| 385 | { | ||
| 386 | 6 | ++in_flight; | |
| 387 | } | ||
| 388 | } | ||
| 389 |
2/2✓ Branch 68 → 69 taken 22221 times.
✓ Branch 68 → 70 taken 9 times.
|
22230 | if (!emit_deferred) |
| 390 | { | ||
| 391 | 22221 | idle_cv.notify_all(); | |
| 392 | } | ||
| 393 | 22231 | } | |
| 394 | |||
| 395 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 396 |
2/2✓ Branch 71 → 72 taken 1 time.
✓ Branch 71 → 73 taken 22236 times.
|
22237 | if (after_bookkeeping_for_test != nullptr) |
| 397 | { | ||
| 398 | 1 | after_bookkeeping_for_test(after_bookkeeping_context_for_test); | |
| 399 | } | ||
| 400 | #endif | ||
| 401 | |||
| 402 | // A release() that could not block (self-release, or cross-binding release from inside another | ||
| 403 | // callback) deferred its balancing false to here; emit it now the callback has unwound. When the | ||
| 404 | // primary callback threw, swallow any secondary throw so the original exception is the one that | ||
| 405 | // surfaces to the poller; otherwise let it propagate to the poller's dispatch handler. | ||
| 406 |
2/2✓ Branch 73 → 74 taken 6 times.
✓ Branch 73 → 83 taken 22231 times.
|
22237 | if (emit_deferred) |
| 407 | { | ||
| 408 | 6 | InFlightSlot balancing_slot{this}; | |
| 409 |
1/2✓ Branch 76 → 77 taken 6 times.
✗ Branch 76 → 81 not taken.
|
6 | if (on_state_change) |
| 410 | { | ||
| 411 |
2/2✓ Branch 78 → 79 taken 1 time.
✓ Branch 78 → 80 taken 5 times.
|
6 | if (err) |
| 412 | { | ||
| 413 | try | ||
| 414 | { | ||
| 415 |
1/2✓ Branch 79 → 81 taken 1 time.
✗ Branch 79 → 106 not taken.
|
1 | on_state_change(false); |
| 416 | } | ||
| 417 | ✗ | catch (...) | |
| 418 | { | ||
| 419 | ✗ | } | |
| 420 | } | ||
| 421 | else | ||
| 422 | { | ||
| 423 |
1/2✓ Branch 80 → 81 taken 5 times.
✗ Branch 80 → 109 not taken.
|
5 | on_state_change(false); |
| 424 | } | ||
| 425 | } | ||
| 426 | 6 | } | |
| 427 |
2/2✓ Branch 84 → 85 taken 2 times.
✓ Branch 84 → 87 taken 22230 times.
|
22237 | if (err) |
| 428 | { | ||
| 429 | 4 | std::rethrow_exception(err); | |
| 430 | } | ||
| 431 |
4/4✓ Branch 90 → 91 taken 22233 times.
✓ Branch 90 → 92 taken 4 times.
✓ Branch 94 → 95 taken 22233 times.
✓ Branch 94 → 97 taken 15264 times.
|
37515 | } |
| 432 | |||
| 433 | /** | ||
| 434 | * @brief Guard teardown: stops further delivery and synthesizes one balancing false if still held. | ||
| 435 | * @details A control-plane release blocks until any in-flight delivery drains, then emits the balancing | ||
| 436 | * false unlocked. A release reached from inside a callback cannot block (it would deadlock), so it | ||
| 437 | * marks the gate released. If this gate has a delivery in flight, the balancing false is deferred | ||
| 438 | * to that delivery's unwind; otherwise it may run inline. A depth-zero return means the gate is | ||
| 439 | * quiesced, so a release that finds another teardown already claiming the gate waits for that one | ||
| 440 | * to finish instead of returning on the strength of its own no-op. | ||
| 441 | */ | ||
| 442 | 664 | void release() | |
| 443 | { | ||
| 444 |
1/2✓ Branch 2 → 3 taken 686 times.
✗ Branch 2 → 47 not taken.
|
664 | std::unique_lock<std::mutex> lock(mutex); |
| 445 |
2/2✓ Branch 3 → 4 taken 22 times.
✓ Branch 3 → 13 taken 664 times.
|
686 | if (released) |
| 446 | { | ||
| 447 | // The unload drain's retire(), or a repeated direct release, already owns this gate's consumer-code | ||
| 448 | // span and may be inside the consumer's balancing edge right now. Returning here would tell this | ||
| 449 | // caller it may destroy the state that callback captured while the callback is still reading it. | ||
| 450 | // Wait the claimant out. A release reached from inside a delivery must not wait (that is the | ||
| 451 | // ordering discipline at the top of this file) and need not: it is not a boundary where a caller | ||
| 452 | // destroys captured state, and the delivery it is nested in is the very thing a waiter would await. | ||
| 453 | // The claim's own thread is excused for the same reason and without needing the marker, which is | ||
| 454 | // what keeps a refused DeliveryScope from turning a self-release into a wait on this very frame. | ||
| 455 |
5/6✓ Branch 5 → 6 taken 12 times.
✓ Branch 5 → 9 taken 11 times.
✓ Branch 7 → 8 taken 12 times.
✗ Branch 7 → 9 not taken.
✓ Branch 10 → 11 taken 12 times.
✓ Branch 10 → 12 taken 11 times.
|
22 | if (!current_thread_in_delivery() && teardown_owner != current_native_thread_id()) |
| 456 | { | ||
| 457 |
5/6✓ Branch 2 → 3 taken 13 times.
✓ Branch 2 → 5 taken 4 times.
✓ Branch 3 → 4 taken 12 times.
✓ Branch 3 → 5 taken 1 time.
✓ Branch 11 → 12 taken 12 times.
✗ Branch 11 → 45 not taken.
|
29 | idle_cv.wait(lock, [this] { return !teardown_active && in_flight == 0; }); |
| 458 | } | ||
| 459 | 23 | return; | |
| 460 | } | ||
| 461 | 664 | released = true; | |
| 462 | // Claimed before the wait below drops the mutex. Setting it together with `released` is what closes the | ||
| 463 | // window: any later release() now sees a claim rather than a merely-released gate. | ||
| 464 | 664 | teardown_active = true; | |
| 465 | 664 | teardown_owner = current_native_thread_id(); | |
| 466 |
2/2✓ Branch 14 → 15 taken 392 times.
✓ Branch 14 → 20 taken 265 times.
|
657 | if (in_flight > 0) |
| 467 | { | ||
| 468 |
2/2✓ Branch 16 → 17 taken 356 times.
✓ Branch 16 → 19 taken 33 times.
|
392 | if (current_thread_in_delivery()) |
| 469 | { | ||
| 470 | // The in-flight delivery emits the balancing false on its own unwind, inside its own in-flight | ||
| 471 | // slot, so the claim ends here and a waiter tracks that slot instead. | ||
| 472 | 356 | deferred_final = true; | |
| 473 | 356 | teardown_active = false; | |
| 474 | 356 | teardown_owner = 0; | |
| 475 | 356 | idle_cv.notify_all(); | |
| 476 | 361 | return; | |
| 477 | } | ||
| 478 |
1/2✓ Branch 19 → 20 taken 33 times.
✗ Branch 19 → 45 not taken.
|
99 | idle_cv.wait(lock, [this] { return in_flight == 0; }); |
| 479 | } | ||
| 480 | 294 | const bool emit_false = forwarded_active; | |
| 481 | 294 | forwarded_active = false; | |
| 482 |
6/6✓ Branch 20 → 21 taken 190 times.
✓ Branch 20 → 23 taken 104 times.
✓ Branch 22 → 23 taken 2 times.
✓ Branch 22 → 24 taken 188 times.
✓ Branch 25 → 26 taken 106 times.
✓ Branch 25 → 28 taken 188 times.
|
294 | if (!emit_false || !on_state_change) |
| 483 | { | ||
| 484 | 106 | teardown_active = false; | |
| 485 | 106 | teardown_owner = 0; | |
| 486 | 106 | idle_cv.notify_all(); | |
| 487 | 106 | return; | |
| 488 | } | ||
| 489 |
1/2✓ Branch 28 → 29 taken 188 times.
✗ Branch 28 → 45 not taken.
|
188 | lock.unlock(); |
| 490 | // Emit the balancing false UNWRAPPED: forwarded_active is already cleared, so a throw here cannot | ||
| 491 | // strand a stale true, and BindingGuard's composed teardown relies on the throw propagating (it runs | ||
| 492 | // its consume-suppression clear even when the balancing edge throws). The noexcept facade release | ||
| 493 | // catches it. The balancing edge is mandatory consumer code, so a MandatoryDeliveryScope brackets it: | ||
| 494 | // a nested release from this callback still defers, and it does so even when the TLS depth store is | ||
| 495 | // refused, which is the composition where two threads each running a cross-gate teardown callback | ||
| 496 | // would otherwise both read as control plane and wait on each other's claim. | ||
| 497 | // The claim outlives the call, including a throw out of it, so retire() cannot move and destroy the | ||
| 498 | // callable mid-invocation and a concurrent release() cannot report the gate quiesced. | ||
| 499 | 188 | TeardownScope teardown_slot{this}; | |
| 500 | 188 | MandatoryDeliveryScope scope; | |
| 501 |
2/2✓ Branch 31 → 32 taken 187 times.
✓ Branch 31 → 41 taken 1 time.
|
188 | on_state_change(false); |
| 502 |
2/2✓ Branch 36 → 37 taken 187 times.
✓ Branch 36 → 39 taken 493 times.
|
680 | } |
| 503 | |||
| 504 | /** | ||
| 505 | * @copydoc BindingGate::retire | ||
| 506 | * @details Takes the owner under the mutex. The false edge and destruction run on this thread after unlock. | ||
| 507 | */ | ||
| 508 | 24 | [[nodiscard]] bool retire(std::chrono::steady_clock::time_point deadline) override | |
| 509 | { | ||
| 510 | 24 | GateCallbackSlot<void(bool)>::Owner callback; | |
| 511 | 24 | bool emit_false = false; | |
| 512 | { | ||
| 513 |
1/2✓ Branch 2 → 3 taken 24 times.
✗ Branch 2 → 45 not taken.
|
24 | std::unique_lock<std::mutex> lock(mutex); |
| 514 | // A concurrent release() runs its balancing edge unlocked under a teardown claim. Retiring through | ||
| 515 | // that claim would move and destroy the callable the other thread is still invoking, so the claim | ||
| 516 | // is part of the quiescence this deadline is waiting for. A claim this thread holds is the one | ||
| 517 | // exception: waiting for it would be waiting for the frame doing the waiting. | ||
| 518 |
1/2✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 24 times.
|
24 | if (teardown_owner == current_native_thread_id()) |
| 519 | { | ||
| 520 | ✗ | return false; | |
| 521 | } | ||
| 522 |
7/8✓ Branch 2 → 3 taken 24 times.
✓ Branch 2 → 5 taken 2 times.
✓ Branch 3 → 4 taken 22 times.
✓ Branch 3 → 5 taken 2 times.
✓ Branch 6 → 7 taken 24 times.
✗ Branch 6 → 43 not taken.
✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 9 taken 22 times.
|
50 | if (!idle_cv.wait_until(lock, deadline, [this] { return in_flight == 0 && !teardown_active; })) |
| 523 | { | ||
| 524 | 2 | return false; | |
| 525 | } | ||
| 526 | 22 | released = true; | |
| 527 | // Publish inactivity before callback transfer. A retained guard must fail closed during disposal. | ||
| 528 |
2/2✓ Branch 10 → 11 taken 19 times.
✓ Branch 10 → 13 taken 3 times.
|
22 | if (enabled) |
| 529 | { | ||
| 530 | 19 | enabled->store(false, std::memory_order_release); | |
| 531 | } | ||
| 532 | 22 | emit_false = forwarded_active; | |
| 533 | 22 | forwarded_active = false; | |
| 534 | 22 | callback = on_state_change.take(); | |
| 535 |
2/2✓ Branch 17 → 18 taken 19 times.
✓ Branch 17 → 20 taken 3 times.
|
22 | if (callback) |
| 536 | { | ||
| 537 | 19 | teardown_active = true; | |
| 538 | 19 | teardown_owner = current_native_thread_id(); | |
| 539 | } | ||
| 540 |
2/2✓ Branch 22 → 23 taken 22 times.
✓ Branch 22 → 26 taken 2 times.
|
24 | } |
| 541 | |||
| 542 | // Keep invocation and destruction inside both teardown scopes. A capture destructor can release this | ||
| 543 | // gate or another gate. | ||
| 544 |
2/2✓ Branch 25 → 27 taken 19 times.
✓ Branch 25 → 39 taken 3 times.
|
22 | if (callback) |
| 545 | { | ||
| 546 | 19 | TeardownScope teardown_slot{this}; | |
| 547 | 19 | MandatoryDeliveryScope scope; | |
| 548 | 19 | GateCallbackSlot<void(bool)>::Owner owned = std::move(callback); | |
| 549 |
2/2✓ Branch 32 → 33 taken 8 times.
✓ Branch 32 → 35 taken 11 times.
|
19 | if (emit_false) |
| 550 | { | ||
| 551 |
1/2✓ Branch 34 → 35 taken 8 times.
✗ Branch 34 → 46 not taken.
|
8 | (*owned)(false); |
| 552 | } | ||
| 553 | 19 | } | |
| 554 | 22 | return true; | |
| 555 | 24 | } | |
| 556 | }; | ||
| 557 | |||
| 558 | /** | ||
| 559 | * @struct PressGate | ||
| 560 | * @brief Per-binding teardown gate that runs down an in-flight press callback before the guard releases. | ||
| 561 | * @details A press has no lingering state to balance, but shares the hold's teardown hazard: the poll thread | ||
| 562 | * can be executing on_press the instant the guard is released. A control-plane release blocks until | ||
| 563 | * on_press has finished, so the caller may destroy state the callback captured the moment release() | ||
| 564 | * returns. A one-shot press that destroys its own guard, or a callback that releases a second | ||
| 565 | * binding's guard, releases from inside the delivery and so cannot block; it marks the gate released | ||
| 566 | * and returns, and the in-flight callback observes released on its own. | ||
| 567 | */ | ||
| 568 | struct PressGate : BindingGate | ||
| 569 | { | ||
| 570 | std::mutex mutex; | ||
| 571 | std::condition_variable idle_cv; | ||
| 572 | std::shared_ptr<std::atomic<bool>> enabled; | ||
| 573 | std::shared_ptr<BindingLifecycle> lifecycle; | ||
| 574 | GateCallbackSlot<void()> on_press; | ||
| 575 | bool released = false; | ||
| 576 | int in_flight = 0; | ||
| 577 | // Native thread inside retire()'s callable disposal, which is counted in `in_flight` because it runs | ||
| 578 | // consumer destructors. Serves the same purpose as HoldGate::teardown_owner: the same-gate recursion | ||
| 579 | // guard, naming the one claim a waiter must not wait for. | ||
| 580 | std::uint32_t teardown_owner = 0; | ||
| 581 | |||
| 582 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 583 | // See HoldGate::before_idle_wait_for_test for the test-seam lifetime rule. | ||
| 584 | void (*before_idle_wait_for_test)(void *) noexcept = nullptr; | ||
| 585 | void *before_idle_wait_context_for_test = nullptr; | ||
| 586 | #endif | ||
| 587 | |||
| 588 | /** | ||
| 589 | * @brief Wrapper the poller invokes on each press edge; forwards to the user callback outside the mutex. | ||
| 590 | */ | ||
| 591 | 18 | void deliver() | |
| 592 | { | ||
| 593 |
1/2✓ Branch 2 → 3 taken 18 times.
✗ Branch 2 → 91 not taken.
|
18 | std::unique_lock<std::mutex> lock(mutex); |
| 594 |
2/2✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 5 taken 14 times.
|
18 | if (released) |
| 595 | { | ||
| 596 | 4 | return; | |
| 597 | } | ||
| 598 |
6/6✓ Branch 6 → 7 taken 10 times.
✓ Branch 6 → 11 taken 4 times.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 11 taken 9 times.
✓ Branch 12 → 13 taken 1 time.
✓ Branch 12 → 14 taken 13 times.
|
14 | if (enabled && !enabled->load(std::memory_order_acquire)) |
| 599 | { | ||
| 600 | 1 | return; | |
| 601 | } | ||
| 602 |
4/6✓ Branch 15 → 16 taken 5 times.
✓ Branch 15 → 20 taken 8 times.
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 5 times.
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 23 taken 13 times.
|
13 | if (lifecycle && lifecycle->tombstoned()) |
| 603 | { | ||
| 604 | ✗ | return; | |
| 605 | } | ||
| 606 |
1/2✓ Branch 24 → 25 taken 13 times.
✗ Branch 24 → 48 not taken.
|
13 | if (!current_thread_in_delivery()) |
| 607 | { | ||
| 608 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 609 |
2/2✓ Branch 25 → 26 taken 2 times.
✓ Branch 25 → 27 taken 11 times.
|
13 | if (before_idle_wait_for_test != nullptr) |
| 610 | { | ||
| 611 | 2 | before_idle_wait_for_test(before_idle_wait_context_for_test); | |
| 612 | } | ||
| 613 | #endif | ||
| 614 |
1/2✓ Branch 27 → 28 taken 13 times.
✗ Branch 27 → 89 not taken.
|
27 | idle_cv.wait(lock, [this] { return in_flight == 0; }); |
| 615 |
1/2✗ Branch 28 → 29 not taken.
✓ Branch 28 → 30 taken 13 times.
|
13 | if (released) |
| 616 | { | ||
| 617 | ✗ | return; | |
| 618 | } | ||
| 619 | // Recheck enabled after the wait | ||
| 620 | // (proof: GateRaceProbe.PressGateDisableDuringParkedDeliveryEmitsNoStaleCallback). | ||
| 621 |
6/6✓ Branch 31 → 32 taken 9 times.
✓ Branch 31 → 36 taken 4 times.
✓ Branch 34 → 35 taken 1 time.
✓ Branch 34 → 36 taken 8 times.
✓ Branch 37 → 38 taken 1 time.
✓ Branch 37 → 39 taken 12 times.
|
13 | if (enabled && !enabled->load(std::memory_order_acquire)) |
| 622 | { | ||
| 623 | 1 | return; | |
| 624 | } | ||
| 625 |
4/6✓ Branch 40 → 41 taken 5 times.
✓ Branch 40 → 45 taken 7 times.
✗ Branch 43 → 44 not taken.
✓ Branch 43 → 45 taken 5 times.
✗ Branch 46 → 47 not taken.
✓ Branch 46 → 48 taken 12 times.
|
12 | if (lifecycle && lifecycle->tombstoned()) |
| 626 | { | ||
| 627 | ✗ | return; | |
| 628 | } | ||
| 629 | } | ||
| 630 | // Same admission rule as HoldGate::deliver, and cheaper to undo: a press commits no gate state before | ||
| 631 | // this point, so a refused frame simply drops the edge. | ||
| 632 | 12 | DeliveryScope scope; | |
| 633 |
2/2✓ Branch 50 → 51 taken 2 times.
✓ Branch 50 → 52 taken 10 times.
|
12 | if (!scope.admitted()) |
| 634 | { | ||
| 635 | 2 | return; | |
| 636 | } | ||
| 637 | 10 | ++in_flight; | |
| 638 |
1/2✓ Branch 52 → 53 taken 10 times.
✗ Branch 52 → 87 not taken.
|
10 | lock.unlock(); |
| 639 | |||
| 640 | 10 | std::exception_ptr err; | |
| 641 | try | ||
| 642 | { | ||
| 643 |
1/2✓ Branch 55 → 56 taken 10 times.
✗ Branch 55 → 57 not taken.
|
10 | if (on_press) |
| 644 | { | ||
| 645 |
1/2✓ Branch 56 → 57 taken 10 times.
✗ Branch 56 → 76 not taken.
|
10 | on_press(); |
| 646 | } | ||
| 647 | } | ||
| 648 | ✗ | catch (...) | |
| 649 | { | ||
| 650 | ✗ | err = std::current_exception(); | |
| 651 | ✗ | } | |
| 652 | |||
| 653 |
1/2✓ Branch 57 → 58 taken 10 times.
✗ Branch 57 → 85 not taken.
|
10 | lock.lock(); |
| 654 | 10 | --in_flight; | |
| 655 | 10 | idle_cv.notify_all(); | |
| 656 |
1/2✓ Branch 59 → 60 taken 10 times.
✗ Branch 59 → 85 not taken.
|
10 | lock.unlock(); |
| 657 |
1/2✗ Branch 61 → 62 not taken.
✓ Branch 61 → 64 taken 10 times.
|
10 | if (err) |
| 658 | { | ||
| 659 | ✗ | std::rethrow_exception(err); | |
| 660 | } | ||
| 661 |
4/4✓ Branch 67 → 68 taken 10 times.
✓ Branch 67 → 69 taken 2 times.
✓ Branch 71 → 72 taken 10 times.
✓ Branch 71 → 74 taken 8 times.
|
20 | } |
| 662 | |||
| 663 | /** | ||
| 664 | * @brief Guard teardown: marks the gate released and, off any callback, waits out any in-flight on_press. | ||
| 665 | */ | ||
| 666 | 994 | void release() | |
| 667 | { | ||
| 668 |
1/2✓ Branch 2 → 3 taken 995 times.
✗ Branch 2 → 16 not taken.
|
994 | std::unique_lock<std::mutex> lock(mutex); |
| 669 | 995 | released = true; | |
| 670 |
7/8✓ Branch 3 → 4 taken 9 times.
✓ Branch 3 → 9 taken 986 times.
✓ Branch 5 → 6 taken 2 times.
✓ Branch 5 → 9 taken 7 times.
✓ Branch 7 → 8 taken 2 times.
✗ Branch 7 → 9 not taken.
✓ Branch 10 → 11 taken 2 times.
✓ Branch 10 → 12 taken 993 times.
|
995 | if (in_flight > 0 && !current_thread_in_delivery() && teardown_owner != current_native_thread_id()) |
| 671 | { | ||
| 672 |
1/2✓ Branch 11 → 12 taken 2 times.
✗ Branch 11 → 14 not taken.
|
6 | idle_cv.wait(lock, [this] { return in_flight == 0; }); |
| 673 | } | ||
| 674 | 995 | } | |
| 675 | |||
| 676 | /** | ||
| 677 | * @copydoc BindingGate::retire | ||
| 678 | * @details A press has no lingering edge to balance, so retirement is the run-down plus handing the | ||
| 679 | * callback to this thread to destroy. Destruction happens unlocked: it runs the consumer's | ||
| 680 | * captured destructors, which must not observe the gate mutex held. It is still counted, because | ||
| 681 | * a concurrent release() promises its caller a quiesced gate and those destructors are consumer | ||
| 682 | * (Logic-DLL) code. | ||
| 683 | */ | ||
| 684 | 133 | [[nodiscard]] bool retire(std::chrono::steady_clock::time_point deadline) override | |
| 685 | { | ||
| 686 | 133 | GateCallbackSlot<void()>::Owner callback; | |
| 687 | { | ||
| 688 |
1/2✓ Branch 2 → 3 taken 133 times.
✗ Branch 2 → 40 not taken.
|
133 | std::unique_lock<std::mutex> lock(mutex); |
| 689 | // Retiring from inside this gate's own disposal would wait on the count that disposal holds. | ||
| 690 |
1/2✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 133 times.
|
133 | if (teardown_owner == current_native_thread_id()) |
| 691 | { | ||
| 692 | ✗ | return false; | |
| 693 | } | ||
| 694 |
3/4✓ Branch 6 → 7 taken 133 times.
✗ Branch 6 → 38 not taken.
✓ Branch 7 → 8 taken 4 times.
✓ Branch 7 → 9 taken 129 times.
|
272 | if (!idle_cv.wait_until(lock, deadline, [this] { return in_flight == 0; })) |
| 695 | { | ||
| 696 | 4 | return false; | |
| 697 | } | ||
| 698 | 129 | released = true; | |
| 699 | // Publish inactivity before callback transfer, as HoldGate::retire does. | ||
| 700 |
2/2✓ Branch 10 → 11 taken 124 times.
✓ Branch 10 → 13 taken 5 times.
|
129 | if (enabled) |
| 701 | { | ||
| 702 | 124 | enabled->store(false, std::memory_order_release); | |
| 703 | } | ||
| 704 | 129 | callback = on_press.take(); | |
| 705 |
2/2✓ Branch 17 → 18 taken 126 times.
✓ Branch 17 → 20 taken 3 times.
|
129 | if (callback) |
| 706 | { | ||
| 707 | 126 | ++in_flight; | |
| 708 | 126 | teardown_owner = current_native_thread_id(); | |
| 709 | } | ||
| 710 |
2/2✓ Branch 22 → 23 taken 129 times.
✓ Branch 22 → 26 taken 4 times.
|
133 | } |
| 711 | |||
| 712 |
2/2✓ Branch 25 → 27 taken 126 times.
✓ Branch 25 → 34 taken 3 times.
|
129 | if (callback) |
| 713 | { | ||
| 714 | // A capture destructor can release a gate, so keep disposal inside mandatory delivery identity. | ||
| 715 | 126 | MandatoryDeliveryScope scope; | |
| 716 | 126 | callback = nullptr; | |
| 717 |
1/2✓ Branch 29 → 30 taken 126 times.
✗ Branch 29 → 41 not taken.
|
126 | std::lock_guard<std::mutex> exit_lock(mutex); |
| 718 | 126 | --in_flight; | |
| 719 | 126 | teardown_owner = 0; | |
| 720 | 126 | idle_cv.notify_all(); | |
| 721 | 126 | } | |
| 722 | 129 | return true; | |
| 723 | 133 | } | |
| 724 | }; | ||
| 725 | } // namespace detail | ||
| 726 | } // namespace DetourModKit | ||
| 727 | |||
| 728 | #endif // DETOURMODKIT_INTERNAL_INPUT_BINDING_GATE_HPP | ||
| 729 |