src/input.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /** | ||
| 2 | * @file input.cpp | ||
| 3 | * @brief Implementation of the public input facade (input.hpp): Input, Scope, BindingGuard, register_combo. | ||
| 4 | * | ||
| 5 | * The facade owns the background poll engine (input_poller.hpp) and the instance-shared interception layer. It explodes | ||
| 6 | * a public ComboBinding into one engine entry per combo (OR logic under a shared name), wraps the user callback behind | ||
| 7 | * a guard-owned cancellation flag, and routes delivery through a guard-owned teardown gate so release can run down any | ||
| 8 | * in-flight callback before returning. | ||
| 9 | */ | ||
| 10 | |||
| 11 | #include "DetourModKit/input.hpp" | ||
| 12 | #include "DetourModKit/diagnostics.hpp" | ||
| 13 | #include "DetourModKit/logger.hpp" | ||
| 14 | |||
| 15 | #include "internal/drain_backoff.hpp" | ||
| 16 | #include "internal/input_binding_gate.hpp" | ||
| 17 | #include "internal/input_delivery_scope.hpp" | ||
| 18 | #include "internal/input_poller.hpp" | ||
| 19 | #include "internal/input_test_seams.hpp" | ||
| 20 | #include "internal/lifecycle_context.hpp" | ||
| 21 | #include "internal/lifecycle_reaper.hpp" | ||
| 22 | |||
| 23 | #include <algorithm> | ||
| 24 | #include <atomic> | ||
| 25 | #include <cstdint> | ||
| 26 | #include <functional> | ||
| 27 | #include <limits> | ||
| 28 | #include <memory> | ||
| 29 | #include <mutex> | ||
| 30 | #include <new> | ||
| 31 | #include <string> | ||
| 32 | #include <string_view> | ||
| 33 | #include <system_error> | ||
| 34 | #include <thread> | ||
| 35 | #include <utility> | ||
| 36 | #include <vector> | ||
| 37 | |||
| 38 | namespace DetourModKit | ||
| 39 | { | ||
| 40 | namespace input | ||
| 41 | { | ||
| 42 | struct BindingGuard::Impl | ||
| 43 | { | ||
| 44 | // Shared cancellation flag the binding's callback wrapper gates on; release() clears it so subsequent | ||
| 45 | // events become no-ops. | ||
| 46 | std::shared_ptr<std::atomic<bool>> enabled; | ||
| 47 | // One-shot action run once by release(): runs down the per-binding gate and, for a consume binding, lifts | ||
| 48 | // passthrough suppression. | ||
| 49 | std::function<void()> on_release; | ||
| 50 | std::string name; | ||
| 51 | }; | ||
| 52 | |||
| 53 | 461 | BindingGuard::BindingGuard() noexcept = default; | |
| 54 | |||
| 55 | 2868 | BindingGuard::BindingGuard(std::unique_ptr<Impl> impl) noexcept : m_impl(std::move(impl)) {} | |
| 56 | |||
| 57 | 3817 | BindingGuard::~BindingGuard() noexcept | |
| 58 | { | ||
| 59 | 3817 | release(); | |
| 60 | 3817 | } | |
| 61 | |||
| 62 | 3854 | BindingGuard::BindingGuard(BindingGuard &&other) noexcept : m_impl(std::move(other.m_impl)) {} | |
| 63 | |||
| 64 | 455 | BindingGuard &BindingGuard::operator=(BindingGuard &&other) noexcept | |
| 65 | { | ||
| 66 |
2/2✓ Branch 2 → 3 taken 454 times.
✓ Branch 2 → 7 taken 1 time.
|
455 | if (this != &other) |
| 67 | { | ||
| 68 | // Fire this guard's own pending release before adopting the other's binding, so the binding this guard | ||
| 69 | // currently owns is not silently abandoned in a held state. | ||
| 70 | 454 | release(); | |
| 71 | 908 | m_impl = std::move(other.m_impl); | |
| 72 | } | ||
| 73 | 455 | return *this; | |
| 74 | } | ||
| 75 | |||
| 76 | 4853 | void BindingGuard::release() noexcept | |
| 77 | { | ||
| 78 |
2/2✓ Branch 3 → 4 taken 2844 times.
✓ Branch 3 → 5 taken 2000 times.
|
4853 | if (!m_impl) |
| 79 | { | ||
| 80 | 2844 | return; | |
| 81 | } | ||
| 82 |
2/2✓ Branch 7 → 8 taken 1402 times.
✓ Branch 7 → 13 taken 583 times.
|
2000 | if (m_impl->enabled) |
| 83 | { | ||
| 84 | 1402 | m_impl->enabled->store(false, std::memory_order_release); | |
| 85 | 1419 | m_impl->enabled.reset(); | |
| 86 | } | ||
| 87 | // Run the optional release action exactly once. std::exchange clears the member first so a repeated or | ||
| 88 | // re-entrant release() cannot double-fire it, and the catch keeps this noexcept teardown honest even though | ||
| 89 | // the action may invoke a user-supplied hold callback. | ||
| 90 |
2/2✓ Branch 15 → 16 taken 1398 times.
✓ Branch 15 → 21 taken 581 times.
|
1990 | if (m_impl->on_release) |
| 91 | { | ||
| 92 | 1398 | const std::function<void()> action = std::exchange(m_impl->on_release, nullptr); | |
| 93 | try | ||
| 94 | { | ||
| 95 |
1/2✓ Branch 18 → 19 taken 1417 times.
✗ Branch 18 → 22 not taken.
|
1398 | action(); |
| 96 | } | ||
| 97 | ✗ | catch (...) | |
| 98 | { | ||
| 99 | ✗ | (void)log().log_noexcept( | |
| 100 | LogLevel::Error, | ||
| 101 | ✗ | "BindingGuard: release action threw; suppressed in noexcept teardown" | |
| 102 | ); | ||
| 103 | ✗ | } | |
| 104 | 1417 | } | |
| 105 | } | ||
| 106 | |||
| 107 | 38 | bool BindingGuard::is_active() const noexcept | |
| 108 | { | ||
| 109 |
6/6✓ Branch 3 → 4 taken 30 times.
✓ Branch 3 → 12 taken 8 times.
✓ Branch 6 → 7 taken 21 times.
✓ Branch 6 → 12 taken 9 times.
✓ Branch 10 → 11 taken 20 times.
✓ Branch 10 → 12 taken 1 time.
|
38 | return m_impl && m_impl->enabled && m_impl->enabled->load(std::memory_order_acquire); |
| 110 | } | ||
| 111 | |||
| 112 | 5 | std::string_view BindingGuard::name() const noexcept | |
| 113 | { | ||
| 114 |
2/2✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 6 taken 1 time.
|
5 | return m_impl ? std::string_view{m_impl->name} : std::string_view{}; |
| 115 | } | ||
| 116 | |||
| 117 | 122 | void Scope::add(BindingGuard guard) | |
| 118 | { | ||
| 119 |
2/2✓ Branch 3 → 4 taken 119 times.
✓ Branch 3 → 8 taken 3 times.
|
122 | if (!m_guards) |
| 120 | { | ||
| 121 |
1/2✓ Branch 4 → 5 taken 119 times.
✗ Branch 4 → 13 not taken.
|
119 | m_guards = std::make_unique<std::vector<BindingGuard>>(); |
| 122 | } | ||
| 123 | 244 | m_guards->push_back(std::move(guard)); | |
| 124 | 122 | } | |
| 125 | |||
| 126 | 746 | void Scope::clear() noexcept | |
| 127 | { | ||
| 128 |
2/2✓ Branch 3 → 4 taken 517 times.
✓ Branch 3 → 5 taken 229 times.
|
746 | if (!m_guards) |
| 129 | { | ||
| 130 | 517 | return; | |
| 131 | } | ||
| 132 | // Consumer code can call add(), so detach the current batch before any release. | ||
| 133 | 458 | std::vector<BindingGuard> batch = std::move(*m_guards); | |
| 134 | 229 | m_guards->clear(); | |
| 135 |
2/2✓ Branch 17 → 12 taken 117 times.
✓ Branch 17 → 18 taken 229 times.
|
346 | for (auto it = batch.rbegin(); it != batch.rend(); ++it) |
| 136 | { | ||
| 137 | 117 | it->release(); | |
| 138 | } | ||
| 139 | 229 | } | |
| 140 | |||
| 141 | 5 | void Scope::abandon() noexcept | |
| 142 | { | ||
| 143 | // Retain the already-allocated container in place. Destroying the guards would destroy consumer callback | ||
| 144 | // captures inside DllMain even if release() itself were skipped, so logical abandonment must bypass both. | ||
| 145 |
2/2✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 5 taken 2 times.
|
5 | if (m_guards.release() != nullptr) |
| 146 | { | ||
| 147 | 3 | diagnostics::record_intentional_leak(diagnostics::LeakSubsystem::Input); | |
| 148 | } | ||
| 149 | 5 | } | |
| 150 | |||
| 151 | 1 | Scope &Scope::operator=(Scope &&other) noexcept | |
| 152 | { | ||
| 153 |
1/2✓ Branch 2 → 3 taken 1 time.
✗ Branch 2 → 7 not taken.
|
1 | if (this != &other) |
| 154 | { | ||
| 155 | 1 | clear(); | |
| 156 | 2 | m_guards = std::move(other.m_guards); | |
| 157 | } | ||
| 158 | 1 | return *this; | |
| 159 | } | ||
| 160 | |||
| 161 | struct Input::Impl | ||
| 162 | { | ||
| 163 | // The latch leaves m_impl unchanged, so an admitted facade call retains a stable pointee. | ||
| 164 | std::atomic<bool> m_vetoed_retained{false}; | ||
| 165 | mutable std::mutex m_mutex; | ||
| 166 | std::vector<detail::InputBinding> m_pending; | ||
| 167 | std::uint64_t m_start_revision{1}; | ||
| 168 | std::shared_ptr<detail::InputPoller> m_poller; | ||
| 169 | // Hot-path queries load a shared_ptr snapshot without m_mutex. The snapshot preserves poller lifetime | ||
| 170 | // across concurrent shutdown. input.hpp documents the shared ownership cost. | ||
| 171 | std::atomic<std::shared_ptr<detail::InputPoller>> m_active{}; | ||
| 172 | std::atomic<bool> m_running{false}; | ||
| 173 | std::atomic<bool> m_callback_drain_active{false}; | ||
| 174 | std::atomic<std::uint32_t> m_admission_commits_inflight{0}; | ||
| 175 | // Last-applied / pending engine settings. require_focus is live-mutable via set_require_focus; the gamepad | ||
| 176 | // knobs and poll interval are consumed when start() builds the poller. | ||
| 177 | Settings m_settings{}; | ||
| 178 | |||
| 179 | /// Advances the facade state token after a pending binding or start setting changes. | ||
| 180 | 1300 | void advance_start_revision() noexcept | |
| 181 | { | ||
| 182 | 1300 | m_start_revision = | |
| 183 |
1/2✓ Branch 3 → 4 taken 1300 times.
✗ Branch 3 → 5 not taken.
|
1300 | m_start_revision == (std::numeric_limits<std::uint64_t>::max)() ? 1 : m_start_revision + 1; |
| 184 | 1300 | } | |
| 185 | // A consume release action holds a weak facade token before it calls set_consume_by_owner. The action | ||
| 186 | // becomes a no-op after facade teardown. No other guard action reaches facade state. | ||
| 187 | std::shared_ptr<char> m_liveness{std::make_shared<char>()}; | ||
| 188 | }; | ||
| 189 | |||
| 190 | 340 | void Input::ImplDeleter::operator()(Impl *impl) const noexcept | |
| 191 | { | ||
| 192 |
5/6✓ Branch 2 → 3 taken 340 times.
✗ Branch 2 → 6 not taken.
✓ Branch 4 → 5 taken 226 times.
✓ Branch 4 → 6 taken 114 times.
✓ Branch 7 → 8 taken 226 times.
✓ Branch 7 → 11 taken 114 times.
|
340 | if (impl != nullptr && !impl->m_vetoed_retained.load(std::memory_order_acquire)) |
| 193 | { | ||
| 194 |
1/2✓ Branch 8 → 9 taken 226 times.
✗ Branch 8 → 11 not taken.
|
226 | delete impl; |
| 195 | } | ||
| 196 | 340 | } | |
| 197 | |||
| 198 | static_assert(sizeof(Input) == sizeof(void *), "Input must retain its pointer-sized public ABI"); | ||
| 199 | static_assert(alignof(Input) == alignof(void *), "Input must retain its pointer-aligned public ABI"); | ||
| 200 | static_assert(std::atomic<bool>::is_always_lock_free, "Input's loader-lock retention latch must be lock-free"); | ||
| 201 | |||
| 202 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 203 | namespace | ||
| 204 | { | ||
| 205 | std::atomic<detail::InputTestSeams::CallbackAdmissionCommitSeam> s_callback_admission_commit_seam{nullptr}; | ||
| 206 | // Input members cast this test-only retained-owner identity through void*. | ||
| 207 | std::atomic<void *> s_vetoed_retained_impl{nullptr}; | ||
| 208 | // The unlock seam uses the owner identity captured before a test veto. | ||
| 209 | void *s_test_locked_impl = nullptr; | ||
| 210 | } // namespace | ||
| 211 | #endif | ||
| 212 | |||
| 213 | namespace | ||
| 214 | { | ||
| 215 | /** | ||
| 216 | * @brief Counts one admitted registration or start operation until its commit point has passed. | ||
| 217 | */ | ||
| 218 | class AdmissionCommitLease | ||
| 219 | { | ||
| 220 | public: | ||
| 221 | 1650 | AdmissionCommitLease( | |
| 222 | std::atomic<bool> &drain_active, | ||
| 223 | std::atomic<std::uint32_t> &inflight, | ||
| 224 | bool require_staging_admission = true | ||
| 225 | ) noexcept | ||
| 226 | 1650 | : m_drain_active(drain_active), m_inflight(inflight) | |
| 227 | { | ||
| 228 |
7/8✓ Branch 3 → 4 taken 1650 times.
✗ Branch 3 → 9 not taken.
✓ Branch 5 → 6 taken 1647 times.
✓ Branch 5 → 9 taken 3 times.
✓ Branch 6 → 7 taken 1434 times.
✓ Branch 6 → 10 taken 213 times.
✓ Branch 11 → 12 taken 3 times.
✓ Branch 11 → 13 taken 1646 times.
|
3083 | if (m_drain_active.load(std::memory_order_seq_cst) || detail::input_callback_drain_pending() || |
| 229 |
1/2✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 1433 times.
|
1434 | (require_staging_admission && !detail::input_callback_admission_open())) |
| 230 | { | ||
| 231 | 3 | return; | |
| 232 | } | ||
| 233 | |||
| 234 | 1646 | m_inflight.fetch_add(1, std::memory_order_seq_cst); | |
| 235 |
5/8✓ Branch 16 → 17 taken 1647 times.
✗ Branch 16 → 22 not taken.
✓ Branch 18 → 19 taken 1646 times.
✗ Branch 18 → 22 not taken.
✓ Branch 19 → 20 taken 1433 times.
✓ Branch 19 → 23 taken 213 times.
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 28 taken 1647 times.
|
3080 | if (m_drain_active.load(std::memory_order_seq_cst) || detail::input_callback_drain_pending() || |
| 236 |
1/2✗ Branch 21 → 22 not taken.
✓ Branch 21 → 23 taken 1434 times.
|
1433 | (require_staging_admission && !detail::input_callback_admission_open())) |
| 237 | { | ||
| 238 | ✗ | m_inflight.fetch_sub(1, std::memory_order_seq_cst); | |
| 239 | ✗ | return; | |
| 240 | } | ||
| 241 | 1647 | m_engaged = true; | |
| 242 | } | ||
| 243 | |||
| 244 | 1650 | ~AdmissionCommitLease() noexcept | |
| 245 | { | ||
| 246 |
2/2✓ Branch 2 → 3 taken 1647 times.
✓ Branch 2 → 6 taken 3 times.
|
1650 | if (m_engaged) |
| 247 | { | ||
| 248 | 1647 | m_inflight.fetch_sub(1, std::memory_order_seq_cst); | |
| 249 | } | ||
| 250 | 1650 | } | |
| 251 | |||
| 252 | AdmissionCommitLease(const AdmissionCommitLease &) = delete; | ||
| 253 | AdmissionCommitLease &operator=(const AdmissionCommitLease &) = delete; | ||
| 254 | |||
| 255 | /// Returns whether this registration may commit. | ||
| 256 | 1650 | [[nodiscard]] bool engaged() const noexcept { return m_engaged; } | |
| 257 | |||
| 258 | private: | ||
| 259 | std::atomic<bool> &m_drain_active; | ||
| 260 | std::atomic<std::uint32_t> &m_inflight; | ||
| 261 | bool m_engaged{false}; | ||
| 262 | }; | ||
| 263 | |||
| 264 | 143 | [[nodiscard]] bool await_admission_commits( | |
| 265 | std::atomic<std::uint32_t> &inflight, | ||
| 266 | std::chrono::steady_clock::time_point deadline | ||
| 267 | ) noexcept | ||
| 268 | { | ||
| 269 | 143 | return detail::drain_until_zero( | |
| 270 | 832 | [&inflight]() noexcept { return inflight.load(std::memory_order_seq_cst); }, | |
| 271 | deadline | ||
| 272 | 143 | ); | |
| 273 | } | ||
| 274 | } // namespace | ||
| 275 | |||
| 276 | 342 | Input::Input() noexcept : m_impl(create_impl()) {} | |
| 277 | |||
| 278 | 342 | Input::ImplOwner Input::create_impl() noexcept | |
| 279 | { | ||
| 280 | // First-use allocation failure must not escape the noexcept instance() accessor. A null Impl is the inert | ||
| 281 | // state: no thread, no binding storage, and no partially built engine is ever published. It latches for the | ||
| 282 | // process generation because instance() constructs the singleton exactly once. | ||
| 283 | try | ||
| 284 | { | ||
| 285 |
4/8✓ Branch 2 → 3 taken 340 times.
✓ Branch 2 → 12 taken 2 times.
✓ Branch 3 → 4 taken 340 times.
✗ Branch 3 → 9 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 340 times.
✗ Branch 9 → 10 not taken.
✗ Branch 9 → 11 not taken.
|
342 | return ImplOwner{new Impl{}}; |
| 286 | } | ||
| 287 | 2 | catch (...) | |
| 288 | { | ||
| 289 | 2 | return nullptr; | |
| 290 | 2 | } | |
| 291 | } | ||
| 292 | |||
| 293 | 342 | Input::~Input() noexcept | |
| 294 | { | ||
| 295 | // shutdown() owns the B-100 boundary. | ||
| 296 | 342 | shutdown(); | |
| 297 | 342 | } | |
| 298 | |||
| 299 | 2324 | Input &Input::instance() noexcept | |
| 300 | { | ||
| 301 | // The constructor is noexcept, so this local static cannot throw out of a noexcept accessor and cannot be | ||
| 302 | // re-entered for a retry. Concurrent first callers serialize on the language's own initialization guard and | ||
| 303 | // all observe the same object, inert or live. | ||
| 304 |
4/4✓ Branch 2 → 3 taken 345 times.
✓ Branch 2 → 8 taken 1979 times.
✓ Branch 4 → 5 taken 342 times.
✓ Branch 4 → 8 taken 3 times.
|
2324 | static Input instance; |
| 305 | 2324 | return instance; | |
| 306 | } | ||
| 307 | |||
| 308 | 1439 | Result<BindingGuard> Input::register_combo(ComboBinding binding) noexcept | |
| 309 | { | ||
| 310 |
2/2✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 11 taken 1437 times.
|
1439 | if (is_inert()) |
| 311 | { | ||
| 312 |
1/2✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 2 times.
|
2 | const ErrorCode code = m_impl ? ErrorCode::ShutdownInProgress : ErrorCode::OutOfMemory; |
| 313 | 2 | return std::unexpected(Error{code, "input::register_combo"}); | |
| 314 | } | ||
| 315 | 1437 | AdmissionCommitLease registration{m_impl->m_callback_drain_active, m_impl->m_admission_commits_inflight}; | |
| 316 |
2/2✓ Branch 15 → 16 taken 3 times.
✓ Branch 15 → 19 taken 1434 times.
|
1437 | if (!registration.engaged()) |
| 317 | { | ||
| 318 | 3 | return std::unexpected(Error{ErrorCode::ShutdownInProgress, "input::register_combo"}); | |
| 319 | } | ||
| 320 | |||
| 321 | try | ||
| 322 | { | ||
| 323 |
1/2✓ Branch 19 → 20 taken 1434 times.
✗ Branch 19 → 239 not taken.
|
1434 | auto enabled = std::make_shared<std::atomic<bool>>(true); |
| 324 |
1/2✓ Branch 20 → 21 taken 1434 times.
✗ Branch 20 → 314 not taken.
|
1434 | auto impl = std::make_unique<BindingGuard::Impl>(); |
| 325 | 1434 | impl->enabled = enabled; | |
| 326 |
1/2✓ Branch 24 → 25 taken 1433 times.
✗ Branch 24 → 312 not taken.
|
1433 | impl->name = binding.name; |
| 327 | |||
| 328 | // One lifecycle shared by this registration's gate and every exploded engine entry: the gate reads its | ||
| 329 | // tombstone as a resurrection guard, and each entry carries it so a poll-cycle callback staged before a | ||
| 330 | // remove / clear / cardinality-changing rebind is refused at dispatch. Allocated once here so the gate | ||
| 331 | // and entries share one identity. This call also reserves the delivery marker's TLS slot, which has to | ||
| 332 | // happen on a control thread before the wrappers below can be dispatched to. | ||
| 333 |
1/2✓ Branch 25 → 26 taken 1431 times.
✗ Branch 25 → 312 not taken.
|
1433 | auto lifecycle = detail::make_binding_lifecycle(); |
| 334 | |||
| 335 | 1431 | const bool is_hold = binding.trigger == Trigger::Hold; | |
| 336 | |||
| 337 | // Unique identity for this registration, stamped on every exploded engine entry so the guard's teardown | ||
| 338 | // can clear the consume flag by identity rather than by name. A monotonic process-wide counter (never | ||
| 339 | // 0, which is the no-owner sentinel), so it cannot alias a freed binding the way a reused pointer | ||
| 340 | // address can. Relaxed suffices: the id only has to be unique, not ordered against any other state. | ||
| 341 | static std::atomic<std::uint64_t> s_next_consume_owner{1}; | ||
| 342 | 1431 | const std::uint64_t consume_owner = s_next_consume_owner.fetch_add(1, std::memory_order_relaxed); | |
| 343 | |||
| 344 | // Wrap the user callback behind a per-binding teardown gate that all of the binding's exploded combos | ||
| 345 | // share. The gate's release() is the one-shot action the guard runs on teardown: | ||
| 346 | // - HoldGate reference-counts the shared combos so a multi-combo hold forwards only the aggregate | ||
| 347 | // held/released transitions, and it synthesizes exactly one balancing on_state_change(false) for a | ||
| 348 | // still-held binding without re-entering the callback while it is on the stack. | ||
| 349 | // - PressGate serializes delivery against release() so a caller can destroy state the press callback | ||
| 350 | // captured the instant the guard is released, with no in-flight on_press still running through it. | ||
| 351 | 1431 | std::function<void()> press_wrapper; | |
| 352 | 1433 | std::function<void(bool)> hold_wrapper; | |
| 353 | 1432 | std::function<void()> gate_release; | |
| 354 | // Also handed to every exploded engine entry, so an unload drain can retire the gate directly instead | ||
| 355 | // of only dropping the wrappers that capture it. See InputBinding::gate. | ||
| 356 | 1434 | std::shared_ptr<detail::BindingGate> binding_gate; | |
| 357 |
2/2✓ Branch 31 → 32 taken 447 times.
✓ Branch 31 → 52 taken 987 times.
|
1434 | if (is_hold) |
| 358 | { | ||
| 359 |
1/2✓ Branch 32 → 33 taken 447 times.
✗ Branch 32 → 251 not taken.
|
447 | auto gate = std::make_shared<detail::HoldGate>(); |
| 360 | 447 | gate->enabled = enabled; | |
| 361 | 447 | gate->lifecycle = lifecycle; | |
| 362 |
1/2✓ Branch 41 → 42 taken 447 times.
✗ Branch 41 → 240 not taken.
|
894 | gate->on_state_change = std::move(binding.on_state_change); |
| 363 |
1/2✓ Branch 44 → 45 taken 447 times.
✗ Branch 44 → 243 not taken.
|
1280 | hold_wrapper = [gate](bool active) { gate->deliver(active); }; |
| 364 |
1/2✓ Branch 47 → 48 taken 447 times.
✗ Branch 47 → 246 not taken.
|
859 | gate_release = [gate]() { gate->release(); }; |
| 365 | 447 | binding_gate = gate; | |
| 366 | 447 | } | |
| 367 | else | ||
| 368 | { | ||
| 369 |
1/2✓ Branch 52 → 53 taken 985 times.
✗ Branch 52 → 263 not taken.
|
987 | auto gate = std::make_shared<detail::PressGate>(); |
| 370 | 985 | gate->enabled = enabled; | |
| 371 | 987 | gate->lifecycle = lifecycle; | |
| 372 |
1/2✓ Branch 61 → 62 taken 986 times.
✗ Branch 61 → 252 not taken.
|
1972 | gate->on_press = std::move(binding.on_press); |
| 373 |
1/2✓ Branch 64 → 65 taken 987 times.
✗ Branch 64 → 255 not taken.
|
991 | press_wrapper = [gate]() { gate->deliver(); }; |
| 374 |
1/2✓ Branch 67 → 68 taken 987 times.
✗ Branch 67 → 258 not taken.
|
1968 | gate_release = [gate]() { gate->release(); }; |
| 375 | 986 | binding_gate = gate; | |
| 376 | 986 | } | |
| 377 | |||
| 378 | // Callback disable does not clear consume suppression, which reads InputBinding::consume. Clear by | ||
| 379 | // owner identity because empty names do not enter the name index. The weak token rejects late release. | ||
| 380 | 1433 | std::function<void()> consume_release; | |
| 381 |
2/2✓ Branch 73 → 74 taken 113 times.
✓ Branch 73 → 83 taken 1320 times.
|
1433 | if (binding.consume) |
| 382 | { | ||
| 383 | 113 | const std::weak_ptr<char> facade_alive = m_impl->m_liveness; | |
| 384 | 113 | Input *const facade = this; | |
| 385 |
1/4✗ Branch 79 → 80 not taken.
✓ Branch 79 → 81 taken 113 times.
✗ Branch 266 → 267 not taken.
✗ Branch 266 → 268 not taken.
|
226 | consume_release = [facade_alive, facade, consume_owner]() |
| 386 | { | ||
| 387 |
1/2✓ Branch 4 → 5 taken 111 times.
✗ Branch 4 → 6 not taken.
|
111 | if (auto keep = facade_alive.lock()) |
| 388 | { | ||
| 389 | 111 | facade->set_consume_by_owner(consume_owner, false); | |
| 390 | 111 | } | |
| 391 |
1/2✓ Branch 77 → 78 taken 113 times.
✗ Branch 77 → 264 not taken.
|
113 | }; |
| 392 | 113 | } | |
| 393 | |||
| 394 | // Release the gate before the consume clear. If a Hold edge throws, run the clear before the exception | ||
| 395 | // resumes. | ||
| 396 |
2/2✓ Branch 84 → 85 taken 113 times.
✓ Branch 84 → 97 taken 1320 times.
|
1433 | if (consume_release) |
| 397 | { | ||
| 398 | 113 | impl->on_release = | |
| 399 |
1/4✗ Branch 94 → 95 not taken.
✓ Branch 94 → 96 taken 113 times.
✗ Branch 275 → 276 not taken.
✗ Branch 275 → 277 not taken.
|
339 | [gate_release = std::move(gate_release), consume_release = std::move(consume_release)]() |
| 400 | { | ||
| 401 | try | ||
| 402 | { | ||
| 403 |
1/2✓ Branch 2 → 3 taken 111 times.
✗ Branch 2 → 5 not taken.
|
111 | gate_release(); |
| 404 | } | ||
| 405 | ✗ | catch (...) | |
| 406 | { | ||
| 407 | ✗ | consume_release(); | |
| 408 | ✗ | throw; | |
| 409 | ✗ | } | |
| 410 | 111 | consume_release(); | |
| 411 |
1/2✓ Branch 92 → 93 taken 113 times.
✗ Branch 92 → 273 not taken.
|
224 | }; |
| 412 | } | ||
| 413 | else | ||
| 414 | { | ||
| 415 | 1320 | impl->on_release = std::move(gate_release); | |
| 416 | } | ||
| 417 | |||
| 418 | // Explode the combos into one engine entry per alternative, all sharing the name (OR logic). An empty | ||
| 419 | // list still registers a single inert sentinel so the name is addressable for a later rebind. | ||
| 420 | 1442 | const auto make_entry = [&](const std::vector<InputCode> &keys, | |
| 421 | const std::vector<InputCode> &modifiers) -> detail::InputBinding | ||
| 422 | { | ||
| 423 | 1442 | detail::InputBinding entry; | |
| 424 |
1/2✓ Branch 3 → 4 taken 1441 times.
✗ Branch 3 → 14 not taken.
|
1440 | entry.name = binding.name; |
| 425 |
1/2✓ Branch 4 → 5 taken 1440 times.
✗ Branch 4 → 14 not taken.
|
1441 | entry.keys = keys; |
| 426 |
1/2✓ Branch 5 → 6 taken 1440 times.
✗ Branch 5 → 14 not taken.
|
1440 | entry.modifiers = modifiers; |
| 427 | 1440 | entry.trigger = binding.trigger; | |
| 428 | 1440 | entry.consume = binding.consume; | |
| 429 | 1440 | entry.consume_owner = consume_owner; | |
| 430 | 1440 | entry.lifecycle = lifecycle; | |
| 431 | 1442 | entry.gate = binding_gate; | |
| 432 |
2/2✓ Branch 8 → 9 taken 450 times.
✓ Branch 8 → 11 taken 991 times.
|
1441 | if (is_hold) |
| 433 | { | ||
| 434 |
1/2✓ Branch 9 → 10 taken 450 times.
✗ Branch 9 → 14 not taken.
|
450 | entry.on_state_change = hold_wrapper; |
| 435 | // The gate deduplicates a released(false) with no live held(true), so a tombstoning reshape can | ||
| 436 | // publish this binding's balancing false without racing the state clear the poll loop commits | ||
| 437 | // for the cycle that staged the release. | ||
| 438 | 450 | entry.release_is_idempotent = true; | |
| 439 | } | ||
| 440 | else | ||
| 441 | { | ||
| 442 |
1/2✓ Branch 11 → 12 taken 991 times.
✗ Branch 11 → 14 not taken.
|
991 | entry.on_press = press_wrapper; |
| 443 | } | ||
| 444 | 1441 | return entry; | |
| 445 | ✗ | }; | |
| 446 | |||
| 447 | 1434 | std::vector<detail::InputBinding> entries; | |
| 448 |
2/2✓ Branch 102 → 103 taken 50 times.
✓ Branch 102 → 111 taken 1384 times.
|
1434 | if (binding.combos.empty()) |
| 449 | { | ||
| 450 |
2/4✓ Branch 105 → 106 taken 50 times.
✗ Branch 105 → 281 not taken.
✓ Branch 106 → 107 taken 50 times.
✗ Branch 106 → 279 not taken.
|
50 | entries.push_back(make_entry({}, {})); |
| 451 | } | ||
| 452 | else | ||
| 453 | { | ||
| 454 |
1/2✓ Branch 112 → 113 taken 1384 times.
✗ Branch 112 → 298 not taken.
|
1384 | entries.reserve(binding.combos.size()); |
| 455 |
2/2✓ Branch 129 → 115 taken 1391 times.
✓ Branch 129 → 130 taken 1384 times.
|
4159 | for (const auto &combo : binding.combos) |
| 456 | { | ||
| 457 |
2/4✓ Branch 117 → 118 taken 1391 times.
✗ Branch 117 → 290 not taken.
✓ Branch 118 → 119 taken 1392 times.
✗ Branch 118 → 288 not taken.
|
1391 | entries.push_back(make_entry(combo.keys, combo.modifiers)); |
| 458 | } | ||
| 459 | } | ||
| 460 | |||
| 461 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 462 |
2/2✓ Branch 132 → 133 taken 2 times.
✓ Branch 132 → 134 taken 1431 times.
|
1433 | if (const detail::InputTestSeams::CallbackAdmissionCommitSeam seam = |
| 463 | 1434 | s_callback_admission_commit_seam.load(std::memory_order_acquire); | |
| 464 | seam != nullptr) | ||
| 465 | { | ||
| 466 | 2 | seam(); | |
| 467 | } | ||
| 468 | #endif | ||
| 469 | |||
| 470 | // Register: forward each entry to the live poller, or stage it for the next start(). Forward outside | ||
| 471 | // m_mutex so the poller's exclusive binding lock cannot AB/BA against a caller holding m_mutex. | ||
| 472 | 1433 | std::shared_ptr<detail::InputPoller> live; | |
| 473 | { | ||
| 474 |
1/2✓ Branch 135 → 136 taken 1434 times.
✗ Branch 135 → 295 not taken.
|
1433 | std::lock_guard lock(m_impl->m_mutex); |
| 475 |
2/2✓ Branch 138 → 139 taken 710 times.
✓ Branch 138 → 141 taken 724 times.
|
1434 | if (m_impl->m_poller) |
| 476 | { | ||
| 477 | 710 | live = m_impl->m_poller; | |
| 478 | } | ||
| 479 | else | ||
| 480 | { | ||
| 481 | // Stage all-or-nothing. Reserve the whole batch up front so a mid-loop bad_alloc cannot leave a | ||
| 482 | // subset of a multi-combo registration staged (which then goes live half-registered at the | ||
| 483 | // next start()). The reserve is the only allocating step; InputBinding moves are noexcept, so | ||
| 484 | // once capacity is secured the push_backs cannot throw. | ||
| 485 |
1/2✓ Branch 145 → 146 taken 724 times.
✗ Branch 145 → 293 not taken.
|
724 | m_impl->m_pending.reserve(m_impl->m_pending.size() + entries.size()); |
| 486 |
2/2✓ Branch 163 → 148 taken 731 times.
✓ Branch 163 → 164 taken 724 times.
|
2179 | for (auto &entry : entries) |
| 487 | { | ||
| 488 |
1/2✓ Branch 153 → 154 taken 731 times.
✗ Branch 153 → 292 not taken.
|
1462 | m_impl->m_pending.push_back(std::move(entry)); |
| 489 | } | ||
| 490 | 724 | m_impl->advance_start_revision(); | |
| 491 | 724 | return BindingGuard{std::move(impl)}; | |
| 492 | } | ||
| 493 |
2/2✓ Branch 176 → 177 taken 710 times.
✓ Branch 176 → 181 taken 724 times.
|
1434 | } |
| 494 |
1/2✗ Branch 180 → 182 not taken.
✓ Branch 180 → 192 taken 710 times.
|
710 | if (!m_impl->m_running.load(std::memory_order_acquire)) |
| 495 | { | ||
| 496 | // shutdown() flips m_running false (under m_mutex) before it tears the captured poller down, so | ||
| 497 | // observing false here means a concurrent shutdown began after we captured the poller. Return a | ||
| 498 | // valid but inert guard, the same observable outcome as registering after shutdown. | ||
| 499 | ✗ | enabled->store(false, std::memory_order_release); | |
| 500 | ✗ | return BindingGuard{std::move(impl)}; | |
| 501 | } | ||
| 502 | |||
| 503 | // Add multi-combo bindings as one batch. A per-entry append can leave a partially-registered consume | ||
| 504 | // binding behind when a later append runs out of memory, and consume suppression is driven by the | ||
| 505 | // engine entry's consume flag rather than the guard's enabled flag. The single-entry path keeps the | ||
| 506 | // existing append primitive live; the multi-entry batch path either commits every combo or none. | ||
| 507 | 2837 | const bool added = (entries.size() == 1) ? live->add_binding(std::move(entries.front())) | |
| 508 |
6/6✓ Branch 193 → 194 taken 709 times.
✓ Branch 193 → 201 taken 1 time.
✓ Branch 207 → 208 taken 1 time.
✓ Branch 207 → 209 taken 709 times.
✓ Branch 209 → 210 taken 709 times.
✓ Branch 209 → 211 taken 1 time.
|
1420 | : live->add_bindings(std::move(entries)); |
| 509 |
1/2✗ Branch 211 → 212 not taken.
✓ Branch 211 → 217 taken 710 times.
|
710 | if (!added) |
| 510 | { | ||
| 511 | ✗ | enabled->store(false, std::memory_order_release); | |
| 512 | ✗ | return std::unexpected(Error{ErrorCode::OutOfMemory, "input::register_combo"}); | |
| 513 | } | ||
| 514 | 710 | return BindingGuard{std::move(impl)}; | |
| 515 | 1434 | } | |
| 516 | ✗ | catch (...) | |
| 517 | { | ||
| 518 | ✗ | return std::unexpected(Error{ErrorCode::OutOfMemory, "input::register_combo"}); | |
| 519 | ✗ | } | |
| 520 | 1437 | } | |
| 521 | |||
| 522 | 215 | Result<void> Input::start(Settings settings) noexcept | |
| 523 | { | ||
| 524 |
2/2✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 11 taken 213 times.
|
215 | if (is_inert()) |
| 525 | { | ||
| 526 |
1/2✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 2 times.
|
2 | const ErrorCode code = m_impl ? ErrorCode::ShutdownInProgress : ErrorCode::OutOfMemory; |
| 527 | 2 | return std::unexpected(Error{code, "input::start"}); | |
| 528 | } | ||
| 529 | AdmissionCommitLease start_admission{ | ||
| 530 | 213 | m_impl->m_callback_drain_active, | |
| 531 | 213 | m_impl->m_admission_commits_inflight, | |
| 532 | false | ||
| 533 | 213 | }; | |
| 534 |
1/2✗ Branch 15 → 16 not taken.
✓ Branch 15 → 19 taken 213 times.
|
213 | if (!start_admission.engaged()) |
| 535 | { | ||
| 536 | ✗ | return std::unexpected(Error{ErrorCode::ShutdownInProgress, "input::start"}); | |
| 537 | } | ||
| 538 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 539 |
2/2✓ Branch 20 → 21 taken 2 times.
✓ Branch 20 → 22 taken 211 times.
|
213 | if (const detail::InputTestSeams::CallbackAdmissionCommitSeam seam = |
| 540 | 213 | s_callback_admission_commit_seam.load(std::memory_order_acquire); | |
| 541 | seam != nullptr) | ||
| 542 | { | ||
| 543 | 2 | seam(); | |
| 544 | } | ||
| 545 | #endif | ||
| 546 | |||
| 547 | try | ||
| 548 | { | ||
| 549 |
1/2✓ Branch 23 → 24 taken 213 times.
✗ Branch 23 → 213 not taken.
|
213 | std::unique_lock lock(m_impl->m_mutex); |
| 550 | |||
| 551 |
5/6✓ Branch 26 → 27 taken 213 times.
✗ Branch 26 → 29 not taken.
✓ Branch 28 → 29 taken 1 time.
✓ Branch 28 → 30 taken 212 times.
✓ Branch 31 → 32 taken 1 time.
✓ Branch 31 → 35 taken 212 times.
|
426 | if (m_impl->m_callback_drain_active.load(std::memory_order_seq_cst) || |
| 552 | 213 | detail::input_callback_drain_pending()) | |
| 553 | { | ||
| 554 | 1 | return std::unexpected(Error{ErrorCode::ShutdownInProgress, "input::start"}); | |
| 555 | } | ||
| 556 |
1/2✗ Branch 36 → 37 not taken.
✓ Branch 36 → 40 taken 212 times.
|
212 | if (!detail::open_input_callback_admission()) |
| 557 | { | ||
| 558 | ✗ | return std::unexpected(Error{ErrorCode::ShutdownInProgress, "input::start"}); | |
| 559 | } | ||
| 560 | |||
| 561 |
2/2✓ Branch 42 → 43 taken 1 time.
✓ Branch 42 → 46 taken 211 times.
|
212 | if (m_impl->m_poller) |
| 562 | { | ||
| 563 |
1/2✓ Branch 44 → 45 taken 1 time.
✗ Branch 44 → 190 not taken.
|
1 | log().debug("input::Input: start() called while already running; no-op."); |
| 564 | 1 | return {}; | |
| 565 | } | ||
| 566 | |||
| 567 | 211 | m_impl->m_settings = settings; | |
| 568 | 211 | m_impl->advance_start_revision(); | |
| 569 | |||
| 570 |
2/2✓ Branch 51 → 52 taken 6 times.
✓ Branch 51 → 53 taken 205 times.
|
211 | if (m_impl->m_pending.empty()) |
| 571 | { | ||
| 572 | // No bindings to seed the engine with. Preserve the no-op; a later register_combo stages | ||
| 573 | // into pending and a subsequent start() builds the poller. | ||
| 574 | 6 | return {}; | |
| 575 | } | ||
| 576 | |||
| 577 | // Resolve the wheel backend before building the engine. An ExternalHost selection is validated against | ||
| 578 | // the C ABI here, once, so the poller only ever receives a known-good host. A required host that is | ||
| 579 | // missing or ABI-incompatible fails start() closed (InvalidArg); an optional one downgrades to the | ||
| 580 | // local MessageHook backend so a single-DLL consumer still captures the wheel. | ||
| 581 | 205 | Input::WheelBackend resolved_backend = settings.wheel_backend; | |
| 582 | 205 | const WheelHostTable *resolved_host = nullptr; | |
| 583 | // Reserved value 0 and every other unknown value are rejected at runtime. | ||
| 584 |
2/2✓ Branch 53 → 54 taken 121 times.
✓ Branch 53 → 58 taken 84 times.
|
205 | if (settings.wheel_backend != Input::WheelBackend::MessageHook && |
| 585 |
2/2✓ Branch 54 → 55 taken 2 times.
✓ Branch 54 → 58 taken 119 times.
|
121 | settings.wheel_backend != Input::WheelBackend::ExternalHost) |
| 586 | { | ||
| 587 | 2 | return std::unexpected(Error{ErrorCode::InvalidArg, "input::start"}); | |
| 588 | } | ||
| 589 |
2/2✓ Branch 58 → 59 taken 9 times.
✓ Branch 58 → 74 taken 194 times.
|
203 | if (settings.wheel_target_thread_id != 0) |
| 590 | { | ||
| 591 | // An explicit wheel target must belong to this process and be alive. | ||
| 592 | 18 | const HANDLE target = OpenThread( | |
| 593 | THREAD_QUERY_LIMITED_INFORMATION | SYNCHRONIZE, | ||
| 594 | FALSE, | ||
| 595 |
1/2✓ Branch 59 → 60 taken 9 times.
✗ Branch 59 → 211 not taken.
|
9 | settings.wheel_target_thread_id |
| 596 | ); | ||
| 597 | 8 | const bool target_valid = target != nullptr && | |
| 598 |
5/8✓ Branch 60 → 61 taken 8 times.
✓ Branch 60 → 67 taken 1 time.
✓ Branch 61 → 62 taken 8 times.
✗ Branch 61 → 211 not taken.
✓ Branch 62 → 63 taken 8 times.
✗ Branch 62 → 211 not taken.
✓ Branch 63 → 64 taken 8 times.
✗ Branch 63 → 67 not taken.
|
17 | GetProcessIdOfThread(target) == GetCurrentProcessId() && |
| 599 |
2/4✓ Branch 64 → 65 taken 8 times.
✗ Branch 64 → 211 not taken.
✓ Branch 65 → 66 taken 8 times.
✗ Branch 65 → 67 not taken.
|
8 | WaitForSingleObject(target, 0) != WAIT_OBJECT_0; |
| 600 |
2/2✓ Branch 68 → 69 taken 8 times.
✓ Branch 68 → 70 taken 1 time.
|
9 | if (target != nullptr) |
| 601 | { | ||
| 602 |
1/2✓ Branch 69 → 70 taken 8 times.
✗ Branch 69 → 211 not taken.
|
8 | CloseHandle(target); |
| 603 | } | ||
| 604 |
2/2✓ Branch 70 → 71 taken 1 time.
✓ Branch 70 → 74 taken 8 times.
|
9 | if (!target_valid) |
| 605 | { | ||
| 606 | 1 | return std::unexpected(Error{ErrorCode::InvalidArg, "input::start"}); | |
| 607 | } | ||
| 608 | } | ||
| 609 |
2/2✓ Branch 74 → 75 taken 119 times.
✓ Branch 74 → 100 taken 83 times.
|
202 | if (settings.wheel_backend == Input::WheelBackend::ExternalHost) |
| 610 | { | ||
| 611 | 119 | const WheelHostTable *host = settings.wheel_host; | |
| 612 | 119 | constexpr std::uint64_t REQUIRED_CAPABILITIES = DMK_WHEELHOST_CAP_VERTICAL | | |
| 613 | DMK_WHEELHOST_CAP_HORIZONTAL | | ||
| 614 | DMK_WHEELHOST_CAP_CONSUME | DMK_WHEELHOST_CAP_ROUTE; | ||
| 615 |
2/2✓ Branch 76 → 77 taken 116 times.
✓ Branch 76 → 88 taken 1 time.
|
117 | const bool host_valid = host != nullptr && host->struct_size >= sizeof(WheelHostTable) && |
| 616 |
2/2✓ Branch 77 → 78 taken 115 times.
✓ Branch 77 → 88 taken 1 time.
|
116 | host->abi_version == DMK_WHEELHOST_ABI_VERSION && |
| 617 |
2/2✓ Branch 78 → 79 taken 113 times.
✓ Branch 78 → 88 taken 2 times.
|
115 | (host->capability_bits & REQUIRED_CAPABILITIES) == REQUIRED_CAPABILITIES && |
| 618 |
3/4✓ Branch 79 → 80 taken 112 times.
✓ Branch 79 → 88 taken 1 time.
✓ Branch 80 → 81 taken 112 times.
✗ Branch 80 → 88 not taken.
|
113 | host->host_identity != 0 && host->host_context != nullptr && |
| 619 |
2/4✓ Branch 81 → 82 taken 112 times.
✗ Branch 81 → 88 not taken.
✓ Branch 82 → 83 taken 112 times.
✗ Branch 82 → 88 not taken.
|
112 | host->open_lease != nullptr && host->publish_capture != nullptr && |
| 620 |
2/4✓ Branch 83 → 84 taken 112 times.
✗ Branch 83 → 88 not taken.
✓ Branch 84 → 85 taken 112 times.
✗ Branch 84 → 88 not taken.
|
112 | host->drain_counts != nullptr && host->close_lease != nullptr && |
| 621 |
6/6✓ Branch 75 → 76 taken 117 times.
✓ Branch 75 → 88 taken 2 times.
✓ Branch 85 → 86 taken 111 times.
✓ Branch 85 → 88 taken 1 time.
✓ Branch 86 → 87 taken 110 times.
✓ Branch 86 → 88 taken 1 time.
|
236 | host->route_status != nullptr && host->retarget != nullptr; |
| 622 |
2/2✓ Branch 89 → 90 taken 110 times.
✓ Branch 89 → 91 taken 9 times.
|
119 | if (host_valid) |
| 623 | { | ||
| 624 | 110 | resolved_host = host; | |
| 625 | } | ||
| 626 |
2/2✓ Branch 91 → 92 taken 8 times.
✓ Branch 91 → 97 taken 1 time.
|
9 | else if (settings.wheel_host_required) |
| 627 | { | ||
| 628 | // Leave callback admission open, matching the other start() failure paths: the refusal is | ||
| 629 | // retryable once the loader supplies a valid host and the staged bindings remain. | ||
| 630 |
1/2✓ Branch 93 → 94 taken 8 times.
✗ Branch 93 → 191 not taken.
|
8 | log().error( |
| 631 | "input::Input: required wheel host is missing or ABI-incompatible; refusing start." | ||
| 632 | ); | ||
| 633 | 8 | return std::unexpected(Error{ErrorCode::InvalidArg, "input::start"}); | |
| 634 | } | ||
| 635 | else | ||
| 636 | { | ||
| 637 |
1/2✓ Branch 98 → 99 taken 1 time.
✗ Branch 98 → 192 not taken.
|
1 | log().warning( |
| 638 | "input::Input: optional wheel host unavailable; using the local MessageHook " | ||
| 639 | "backend." | ||
| 640 | ); | ||
| 641 | 1 | resolved_backend = Input::WheelBackend::MessageHook; | |
| 642 | } | ||
| 643 | } | ||
| 644 | |||
| 645 | 194 | Logger &logger = log(); | |
| 646 | ✗ | logger.info( | |
| 647 | "input::Input: Starting with {} binding(s), poll interval {}ms", | ||
| 648 |
1/2✓ Branch 104 → 105 taken 194 times.
✗ Branch 104 → 193 not taken.
|
194 | m_impl->m_pending.size(), |
| 649 | 194 | settings.poll_interval.count() | |
| 650 | ); | ||
| 651 |
2/2✓ Branch 122 → 108 taken 215 times.
✓ Branch 122 → 123 taken 194 times.
|
603 | for (const auto &binding : m_impl->m_pending) |
| 652 | { | ||
| 653 |
1/2✓ Branch 112 → 113 taken 215 times.
✗ Branch 112 → 196 not taken.
|
215 | logger.trace( |
| 654 | "input::Input: Registered {} binding \"{}\" with {} key(s)", | ||
| 655 | ✗ | to_string(binding.trigger), | |
| 656 | 215 | binding.name, | |
| 657 | 430 | binding.keys.size() | |
| 658 | ); | ||
| 659 | } | ||
| 660 | |||
| 661 | // Seed the engine with a COPY of the staged bindings and clear them only after start() succeeds. | ||
| 662 | // InputPoller::start() throws std::system_error when the poll thread or its module reference cannot be | ||
| 663 | // created, and the poller (sole owner of a moved-in vector) is destroyed during unwind. Moving | ||
| 664 | // m_pending in before that point destroys the staged set with it, so a later retry hits the | ||
| 665 | // empty-pending no-op above and silently loses the bindings. The copy is confined to this cold | ||
| 666 | // path. | ||
| 667 | auto poller = std::make_shared<detail::InputPoller>( | ||
| 668 | 194 | m_impl->m_pending, | |
| 669 | settings.poll_interval, | ||
| 670 | settings.require_focus, | ||
| 671 | settings.gamepad_index, | ||
| 672 | settings.trigger_threshold, | ||
| 673 | settings.stick_threshold, | ||
| 674 | resolved_backend, | ||
| 675 | resolved_host, | ||
| 676 | settings.wheel_target_thread_id | ||
| 677 |
1/2✓ Branch 124 → 125 taken 194 times.
✗ Branch 124 → 211 not taken.
|
194 | ); |
| 678 |
2/2✓ Branch 125 → 126 taken 110 times.
✓ Branch 125 → 165 taken 84 times.
|
194 | if (resolved_backend == Input::WheelBackend::ExternalHost) |
| 679 | { | ||
| 680 | 110 | const std::uint64_t candidate_revision = m_impl->m_start_revision; | |
| 681 | // The loader supplies this function pointer. B-101 requires the call outside the facade lock. | ||
| 682 |
1/2✓ Branch 127 → 128 taken 110 times.
✗ Branch 127 → 209 not taken.
|
110 | lock.unlock(); |
| 683 | 110 | const int32_t host_status = poller->prepare_wheel_source(); | |
| 684 |
2/2✓ Branch 130 → 131 taken 1 time.
✓ Branch 130 → 132 taken 109 times.
|
110 | if (host_status != DMK_WHEELHOST_OK) |
| 685 | { | ||
| 686 | 1 | poller.reset(); | |
| 687 | } | ||
| 688 |
1/2✓ Branch 132 → 133 taken 110 times.
✗ Branch 132 → 209 not taken.
|
110 | lock.lock(); |
| 689 |
3/4✓ Branch 137 → 138 taken 109 times.
✓ Branch 137 → 145 taken 1 time.
✓ Branch 140 → 141 taken 109 times.
✗ Branch 140 → 145 not taken.
|
329 | if (m_impl->m_poller || m_impl->m_start_revision != candidate_revision || |
| 690 |
1/2✓ Branch 142 → 143 taken 109 times.
✗ Branch 142 → 145 not taken.
|
218 | m_impl->m_callback_drain_active.load(std::memory_order_seq_cst) || |
| 691 |
4/6✓ Branch 135 → 136 taken 110 times.
✗ Branch 135 → 145 not taken.
✗ Branch 144 → 145 not taken.
✓ Branch 144 → 146 taken 109 times.
✓ Branch 147 → 148 taken 1 time.
✓ Branch 147 → 153 taken 109 times.
|
329 | detail::input_callback_drain_pending() || !detail::input_callback_admission_open()) |
| 692 | { | ||
| 693 |
1/2✓ Branch 148 → 149 taken 1 time.
✗ Branch 148 → 209 not taken.
|
1 | lock.unlock(); |
| 694 | 1 | poller.reset(); | |
| 695 | 1 | return std::unexpected(Error{ErrorCode::ShutdownInProgress, "input::start"}); | |
| 696 | } | ||
| 697 |
2/2✓ Branch 153 → 154 taken 1 time.
✓ Branch 153 → 165 taken 108 times.
|
109 | if (host_status != DMK_WHEELHOST_OK) |
| 698 | { | ||
| 699 |
1/2✓ Branch 154 → 155 taken 1 time.
✗ Branch 154 → 158 not taken.
|
1 | if (settings.wheel_host_required) |
| 700 | { | ||
| 701 | 1 | const std::int64_t signed_status = host_status; | |
| 702 | 1 | const auto detail = | |
| 703 | 1 | static_cast<std::uintptr_t>(signed_status < 0 ? -signed_status : signed_status); | |
| 704 | 1 | return std::unexpected(Error{ErrorCode::SystemCallFailed, "input::start", detail}); | |
| 705 | } | ||
| 706 | ✗ | log().warning( | |
| 707 | "input::Input: optional wheel host rejected the lease; using the local " | ||
| 708 | "MessageHook backend." | ||
| 709 | ); | ||
| 710 | ✗ | resolved_backend = Input::WheelBackend::MessageHook; | |
| 711 | ✗ | resolved_host = nullptr; | |
| 712 | ✗ | poller = std::make_shared<detail::InputPoller>( | |
| 713 | ✗ | m_impl->m_pending, | |
| 714 | settings.poll_interval, | ||
| 715 | settings.require_focus, | ||
| 716 | settings.gamepad_index, | ||
| 717 | settings.trigger_threshold, | ||
| 718 | settings.stick_threshold, | ||
| 719 | resolved_backend, | ||
| 720 | resolved_host, | ||
| 721 | settings.wheel_target_thread_id | ||
| 722 | ✗ | ); | |
| 723 | } | ||
| 724 | } | ||
| 725 | try | ||
| 726 | { | ||
| 727 |
1/2✓ Branch 166 → 167 taken 192 times.
✗ Branch 166 → 202 not taken.
|
192 | poller->start(); |
| 728 | } | ||
| 729 | ✗ | catch (...) | |
| 730 | { | ||
| 731 | ✗ | lock.unlock(); | |
| 732 | ✗ | poller.reset(); | |
| 733 | ✗ | throw; | |
| 734 | ✗ | } | |
| 735 | // Precommit the non-draining fallback before publishing the poller. A clean shutdown clears this | ||
| 736 | // cycle only after the join and rundown; every uncertain path leaves the complete owner reachable | ||
| 737 | // without allocating during teardown. | ||
| 738 | 192 | poller->retain_owner_for_abandonment(poller); | |
| 739 | 192 | m_impl->m_pending.clear(); | |
| 740 | 192 | m_impl->advance_start_revision(); | |
| 741 | 192 | m_impl->m_poller = poller; | |
| 742 | 192 | m_impl->m_active.store(poller, std::memory_order_release); | |
| 743 | 192 | m_impl->m_running.store(true, std::memory_order_release); | |
| 744 | 192 | return {}; | |
| 745 | 213 | } | |
| 746 | ✗ | catch (const std::system_error &e) | |
| 747 | { | ||
| 748 | ✗ | return std::unexpected( | |
| 749 | ✗ | Error{ErrorCode::SystemCallFailed, "input::start", static_cast<std::uintptr_t>(e.code().value())} | |
| 750 | ✗ | ); | |
| 751 | ✗ | } | |
| 752 | ✗ | catch (...) | |
| 753 | { | ||
| 754 | ✗ | return std::unexpected(Error{ErrorCode::OutOfMemory, "input::start"}); | |
| 755 | ✗ | } | |
| 756 | 213 | } | |
| 757 | |||
| 758 | 874 | void Input::shutdown() noexcept | |
| 759 | { | ||
| 760 |
2/2✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 5 taken 870 times.
|
874 | if (is_inert()) |
| 761 | { | ||
| 762 | 122 | return; | |
| 763 | } | ||
| 764 | |||
| 765 | // B-100 requires this gate before any m_mutex access. | ||
| 766 |
2/2✓ Branch 6 → 7 taken 118 times.
✓ Branch 6 → 25 taken 752 times.
|
870 | if (!detail::blocking_teardown_permitted()) |
| 767 | { | ||
| 768 | 118 | Impl *const impl = m_impl.get(); | |
| 769 | 118 | bool active = false; | |
| 770 |
1/2✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 118 times.
|
118 | if (!impl->m_vetoed_retained |
| 771 | 118 | .compare_exchange_strong(active, true, std::memory_order_acq_rel, std::memory_order_acquire)) | |
| 772 | { | ||
| 773 | ✗ | return; | |
| 774 | } | ||
| 775 | // Stop a running poll loop without a wait: the poller's own gate detaches instead of joining, so the | ||
| 776 | // retained owner keeps a stopped engine rather than a live callback source. Process exit skips the | ||
| 777 | // stop, because the OS already terminated the poll thread and no lock acquisition is safe there. | ||
| 778 |
2/2✓ Branch 13 → 14 taken 117 times.
✓ Branch 13 → 21 taken 1 time.
|
118 | if (detail::lifecycle().loader_context() != detail::LoaderContext::ProcessExit) |
| 779 | { | ||
| 780 |
2/2✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 19 taken 116 times.
|
117 | if (const std::shared_ptr<detail::InputPoller> poller = |
| 781 | 117 | impl->m_active.load(std::memory_order_acquire)) | |
| 782 | { | ||
| 783 | 1 | poller->shutdown(); | |
| 784 | 117 | } | |
| 785 | } | ||
| 786 | 118 | diagnostics::record_intentional_leak(diagnostics::LeakSubsystem::Input); | |
| 787 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 788 | 118 | s_vetoed_retained_impl.store(impl, std::memory_order_release); | |
| 789 | #endif | ||
| 790 | 118 | return; | |
| 791 | } | ||
| 792 | |||
| 793 | 752 | std::shared_ptr<detail::InputPoller> local_poller; | |
| 794 | 752 | std::vector<detail::InputBinding> retired; | |
| 795 | |||
| 796 | { | ||
| 797 | 752 | std::lock_guard lock(m_impl->m_mutex); | |
| 798 | // Clear the atomic shared_ptr before releasing the poller so a concurrent is_active() caller holds a | ||
| 799 | // valid shared_ptr. | ||
| 800 | 752 | m_impl->m_active.store(nullptr, std::memory_order_release); | |
| 801 | 752 | m_impl->m_running.store(false, std::memory_order_release); | |
| 802 | 1504 | local_poller = std::move(m_impl->m_poller); | |
| 803 |
2/2✓ Branch 39 → 40 taken 83 times.
✓ Branch 39 → 42 taken 669 times.
|
752 | if (!m_impl->m_pending.empty()) |
| 804 | { | ||
| 805 | 83 | m_impl->advance_start_revision(); | |
| 806 | } | ||
| 807 | 752 | retired.swap(m_impl->m_pending); | |
| 808 | 752 | } | |
| 809 | // Drop staged capture owners before poller shutdown. | ||
| 810 | 752 | retired.clear(); | |
| 811 | |||
| 812 |
2/2✓ Branch 47 → 48 taken 192 times.
✓ Branch 47 → 62 taken 560 times.
|
752 | if (local_poller) |
| 813 | { | ||
| 814 | 192 | local_poller->shutdown(); | |
| 815 | |||
| 816 |
2/2✓ Branch 52 → 53 taken 3 times.
✓ Branch 52 → 62 taken 189 times.
|
192 | if (local_poller->self_retiring()) |
| 817 | { | ||
| 818 | // shutdown() was reached from a binding callback, so this thread IS the poll thread. Its rundown | ||
| 819 | // (join, detour uninstall, final on_state_change(false)) must happen after the callback returns | ||
| 820 | // and off this thread. Hand the facade's reference to the process-lifetime reaper, which drops it | ||
| 821 | // once shutdown() has joined the body there; ~InputPoller then sees a completed rundown. | ||
| 822 | 3 | std::shared_ptr<void> owner = std::move(local_poller); | |
| 823 | 3 | const auto retire = [](void *raw_owner) noexcept -> bool | |
| 824 | { | ||
| 825 | 3 | auto *const poller = static_cast<detail::InputPoller *>(raw_owner); | |
| 826 | 3 | poller->shutdown(); | |
| 827 | 3 | return !poller->requires_abandonment(); | |
| 828 | }; | ||
| 829 |
1/2✗ Branch 58 → 59 not taken.
✓ Branch 58 → 60 taken 3 times.
|
3 | if (!detail::reap_shared_owner(owner, retire)) |
| 830 | { | ||
| 831 | // The precommitted self-keepalive retains the complete poller when no reaper can accept it. | ||
| 832 | // Stop was already requested, so the loop exits after this callback without losing its state. | ||
| 833 | ✗ | diagnostics::record_intentional_leak(diagnostics::LeakSubsystem::Input); | |
| 834 | } | ||
| 835 | 3 | } | |
| 836 | } | ||
| 837 | 752 | } | |
| 838 | |||
| 839 | 263 | bool Input::is_running() const noexcept | |
| 840 | { | ||
| 841 |
4/4✓ Branch 3 → 4 taken 257 times.
✓ Branch 3 → 8 taken 5 times.
✓ Branch 6 → 7 taken 234 times.
✓ Branch 6 → 8 taken 23 times.
|
263 | return !is_inert() && m_impl->m_running.load(std::memory_order_acquire); |
| 842 | } | ||
| 843 | |||
| 844 | 56588 | std::size_t Input::binding_count() const noexcept | |
| 845 | { | ||
| 846 |
2/2✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 5 taken 57088 times.
|
56588 | if (is_inert()) |
| 847 | { | ||
| 848 | 3 | return 0; | |
| 849 | } | ||
| 850 | |||
| 851 | 57088 | std::shared_ptr<detail::InputPoller> live_poller; | |
| 852 | { | ||
| 853 | 57088 | std::lock_guard lock(m_impl->m_mutex); | |
| 854 |
2/2✓ Branch 9 → 10 taken 294 times.
✓ Branch 9 → 13 taken 58108 times.
|
58402 | if (!m_impl->m_poller) |
| 855 | { | ||
| 856 | 294 | return m_impl->m_pending.size(); | |
| 857 | } | ||
| 858 | 58108 | live_poller = m_impl->m_poller; | |
| 859 |
2/2✓ Branch 17 → 18 taken 58081 times.
✓ Branch 17 → 21 taken 288 times.
|
58402 | } |
| 860 | 58081 | return live_poller->binding_count(); | |
| 861 | 58165 | } | |
| 862 | |||
| 863 | 57672 | bool Input::is_active(std::string_view name) const noexcept | |
| 864 | { | ||
| 865 | 57672 | auto active_poller = poller_snapshot(); | |
| 866 |
2/2✓ Branch 4 → 5 taken 57934 times.
✓ Branch 4 → 8 taken 208 times.
|
58136 | return active_poller ? active_poller->is_binding_active(name) : false; |
| 867 | 57178 | } | |
| 868 | |||
| 869 | 20 | BindingToken Input::acquire_token(std::string_view name) const noexcept | |
| 870 | { | ||
| 871 | 20 | auto active_poller = poller_snapshot(); | |
| 872 |
2/2✓ Branch 4 → 5 taken 15 times.
✓ Branch 4 → 7 taken 5 times.
|
20 | return active_poller ? active_poller->acquire_binding_token(name) : BindingToken{}; |
| 873 | 20 | } | |
| 874 | |||
| 875 | 6 | bool Input::is_active(const BindingToken &token) const noexcept | |
| 876 | { | ||
| 877 | 6 | auto active_poller = poller_snapshot(); | |
| 878 |
2/2✓ Branch 4 → 5 taken 5 times.
✓ Branch 4 → 8 taken 1 time.
|
6 | return active_poller ? active_poller->is_binding_active(token) : false; |
| 879 | 6 | } | |
| 880 | |||
| 881 | 32 | bool Input::token_current(const BindingToken &token) const noexcept | |
| 882 | { | ||
| 883 | 32 | auto active_poller = poller_snapshot(); | |
| 884 |
2/2✓ Branch 4 → 5 taken 29 times.
✓ Branch 4 → 8 taken 3 times.
|
32 | return active_poller ? active_poller->binding_token_current(token) : false; |
| 885 | 32 | } | |
| 886 | |||
| 887 | 3 | ConsumeCapacity Input::consume_capacity() const noexcept | |
| 888 | { | ||
| 889 | 3 | const auto active_poller = poller_snapshot(); | |
| 890 |
2/2✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 7 taken 2 times.
|
3 | return active_poller ? active_poller->consume_capacity() : ConsumeCapacity{}; |
| 891 | 3 | } | |
| 892 | |||
| 893 | 28 | Input::WheelSourceHealth Input::wheel_source_health() const noexcept | |
| 894 | { | ||
| 895 | 28 | const auto active_poller = poller_snapshot(); | |
| 896 |
2/2✓ Branch 4 → 5 taken 26 times.
✓ Branch 4 → 7 taken 2 times.
|
28 | return active_poller ? active_poller->wheel_source_health() : WheelSourceHealth::Inactive; |
| 897 | 28 | } | |
| 898 | |||
| 899 | 118583 | bool Input::is_inert() const noexcept | |
| 900 | { | ||
| 901 | 118583 | const Impl *const impl = m_impl.get(); | |
| 902 |
3/4✓ Branch 3 → 4 taken 119527 times.
✓ Branch 3 → 6 taken 193 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 119986 times.
|
119720 | return impl == nullptr || impl->m_vetoed_retained.load(std::memory_order_acquire); |
| 903 | } | ||
| 904 | |||
| 905 | 57734 | std::shared_ptr<detail::InputPoller> Input::poller_snapshot() const noexcept | |
| 906 | { | ||
| 907 |
2/2✓ Branch 3 → 4 taken 8 times.
✓ Branch 3 → 5 taken 57778 times.
|
57734 | return is_inert() ? nullptr : m_impl->m_active.load(std::memory_order_acquire); |
| 908 | } | ||
| 909 | |||
| 910 | 2059 | Result<void> Input::rebind(std::string_view name, KeyComboList combos) noexcept | |
| 911 | { | ||
| 912 |
2/2✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 7 taken 2057 times.
|
2059 | if (is_inert()) |
| 913 | { | ||
| 914 | 2 | return std::unexpected(Error{ErrorCode::InvalidArg, "input::rebind"}); | |
| 915 | } | ||
| 916 | |||
| 917 | 2057 | std::shared_ptr<detail::InputPoller> local_poller; | |
| 918 | 2057 | std::vector<detail::InputBinding> retired; | |
| 919 | 2057 | std::vector<detail::InputBinding> rebuilt; | |
| 920 | |||
| 921 | try | ||
| 922 | { | ||
| 923 |
1/2✓ Branch 8 → 9 taken 2057 times.
✗ Branch 8 → 234 not taken.
|
2057 | std::unique_lock lock(m_impl->m_mutex); |
| 924 |
2/2✓ Branch 11 → 12 taken 2007 times.
✓ Branch 11 → 14 taken 50 times.
|
2057 | if (m_impl->m_poller) |
| 925 | { | ||
| 926 | 2007 | local_poller = m_impl->m_poller; | |
| 927 | } | ||
| 928 | else | ||
| 929 | { | ||
| 930 | // Apply to pending bindings (the first INI load typically runs before start()). | ||
| 931 | 50 | std::vector<std::size_t> indices; | |
| 932 |
1/2✓ Branch 16 → 17 taken 50 times.
✗ Branch 16 → 229 not taken.
|
50 | indices.reserve(m_impl->m_pending.size()); |
| 933 |
2/2✓ Branch 27 → 18 taken 54 times.
✓ Branch 27 → 28 taken 50 times.
|
104 | for (std::size_t i = 0; i < m_impl->m_pending.size(); ++i) |
| 934 | { | ||
| 935 |
1/2✓ Branch 22 → 23 taken 54 times.
✗ Branch 22 → 24 not taken.
|
54 | if (m_impl->m_pending[i].name == name) |
| 936 | { | ||
| 937 |
1/2✓ Branch 23 → 24 taken 54 times.
✗ Branch 23 → 209 not taken.
|
54 | indices.push_back(i); |
| 938 | } | ||
| 939 | } | ||
| 940 |
2/2✓ Branch 29 → 30 taken 2 times.
✓ Branch 29 → 36 taken 48 times.
|
50 | if (indices.empty()) |
| 941 | { | ||
| 942 |
1/2✓ Branch 30 → 31 taken 2 times.
✗ Branch 30 → 229 not taken.
|
2 | lock.unlock(); |
| 943 | 2 | (void)log() | |
| 944 | 2 | .try_log(LogLevel::Debug, "input::Input: rebind(\"{}\") ignored: name not found", name); | |
| 945 | 2 | return std::unexpected(Error{ErrorCode::InvalidArg, "input::rebind"}); | |
| 946 | } | ||
| 947 | |||
| 948 |
2/2✓ Branch 38 → 39 taken 34 times.
✓ Branch 38 → 73 taken 14 times.
|
48 | if (indices.size() == combos.size()) |
| 949 | { | ||
| 950 | // Each replacement copies the entry's gate reference, so the locked overwrite destroys no | ||
| 951 | // consumer callable. | ||
| 952 | 34 | std::vector<detail::InputBinding> replacements; | |
| 953 |
1/2✓ Branch 40 → 41 taken 34 times.
✗ Branch 40 → 213 not taken.
|
34 | replacements.reserve(indices.size()); |
| 954 |
2/2✓ Branch 56 → 42 taken 34 times.
✓ Branch 56 → 57 taken 34 times.
|
68 | for (std::size_t i = 0; i < indices.size(); ++i) |
| 955 | { | ||
| 956 |
1/2✓ Branch 45 → 46 taken 34 times.
✗ Branch 45 → 212 not taken.
|
34 | detail::InputBinding binding = m_impl->m_pending[indices[i]]; |
| 957 |
1/2✓ Branch 47 → 48 taken 34 times.
✗ Branch 47 → 210 not taken.
|
34 | binding.keys = combos[i].keys; |
| 958 |
1/2✓ Branch 49 → 50 taken 34 times.
✗ Branch 49 → 210 not taken.
|
34 | binding.modifiers = combos[i].modifiers; |
| 959 |
1/2✓ Branch 52 → 53 taken 34 times.
✗ Branch 52 → 210 not taken.
|
34 | replacements.push_back(std::move(binding)); |
| 960 | 34 | } | |
| 961 |
2/2✓ Branch 67 → 58 taken 34 times.
✓ Branch 67 → 68 taken 34 times.
|
68 | for (std::size_t i = 0; i < indices.size(); ++i) |
| 962 | { | ||
| 963 | 68 | m_impl->m_pending[indices[i]] = std::move(replacements[i]); | |
| 964 | } | ||
| 965 | 34 | m_impl->advance_start_revision(); | |
| 966 | 34 | return {}; | |
| 967 | 34 | } | |
| 968 | |||
| 969 | // Cardinality change: rebuild the pending list. An empty replacement keeps one inert sentinel so | ||
| 970 | // the name stays addressable for a later non-empty update. | ||
| 971 |
1/2✓ Branch 76 → 77 taken 14 times.
✗ Branch 76 → 229 not taken.
|
14 | detail::InputBinding prototype = m_impl->m_pending[indices.front()]; |
| 972 |
1/2✓ Branch 79 → 80 taken 14 times.
✗ Branch 79 → 227 not taken.
|
14 | std::sort(indices.begin(), indices.end()); |
| 973 | |||
| 974 |
2/2✓ Branch 81 → 82 taken 7 times.
✓ Branch 81 → 83 taken 7 times.
|
14 | const std::size_t append_count = combos.empty() ? 1 : combos.size(); |
| 975 | 14 | std::vector<detail::InputBinding> appended; | |
| 976 |
1/2✓ Branch 84 → 85 taken 14 times.
✗ Branch 84 → 225 not taken.
|
14 | appended.reserve(append_count); |
| 977 |
2/2✓ Branch 86 → 87 taken 7 times.
✓ Branch 86 → 95 taken 7 times.
|
14 | if (combos.empty()) |
| 978 | { | ||
| 979 |
1/2✓ Branch 87 → 88 taken 7 times.
✗ Branch 87 → 218 not taken.
|
7 | detail::InputBinding sentinel = prototype; |
| 980 | 7 | sentinel.keys.clear(); | |
| 981 | 7 | sentinel.modifiers.clear(); | |
| 982 |
1/2✓ Branch 92 → 93 taken 7 times.
✗ Branch 92 → 216 not taken.
|
7 | appended.push_back(std::move(sentinel)); |
| 983 | 7 | } | |
| 984 | else | ||
| 985 | { | ||
| 986 |
2/2✓ Branch 115 → 97 taken 9 times.
✓ Branch 115 → 116 taken 7 times.
|
23 | for (const auto &combo : combos) |
| 987 | { | ||
| 988 |
1/2✓ Branch 99 → 100 taken 9 times.
✗ Branch 99 → 221 not taken.
|
9 | detail::InputBinding binding = prototype; |
| 989 |
1/2✓ Branch 100 → 101 taken 9 times.
✗ Branch 100 → 219 not taken.
|
9 | binding.keys = combo.keys; |
| 990 |
1/2✓ Branch 101 → 102 taken 9 times.
✗ Branch 101 → 219 not taken.
|
9 | binding.modifiers = combo.modifiers; |
| 991 |
1/2✓ Branch 104 → 105 taken 9 times.
✗ Branch 104 → 219 not taken.
|
9 | appended.push_back(std::move(binding)); |
| 992 | 9 | } | |
| 993 | } | ||
| 994 | |||
| 995 |
1/2✓ Branch 120 → 121 taken 14 times.
✗ Branch 120 → 225 not taken.
|
14 | rebuilt.reserve(m_impl->m_pending.size() - indices.size() + append_count); |
| 996 |
1/2✓ Branch 122 → 123 taken 14 times.
✗ Branch 122 → 225 not taken.
|
14 | retired.reserve(indices.size()); |
| 997 | 14 | std::size_t cursor = 0; | |
| 998 |
2/2✓ Branch 149 → 125 taken 20 times.
✓ Branch 149 → 150 taken 14 times.
|
48 | for (std::size_t skip : indices) |
| 999 | { | ||
| 1000 |
1/2✗ Branch 134 → 128 not taken.
✓ Branch 134 → 135 taken 20 times.
|
20 | for (std::size_t i = cursor; i < skip; ++i) |
| 1001 | { | ||
| 1002 | ✗ | rebuilt.push_back(std::move(m_impl->m_pending[i])); | |
| 1003 | } | ||
| 1004 |
1/2✓ Branch 139 → 140 taken 20 times.
✗ Branch 139 → 223 not taken.
|
40 | retired.push_back(std::move(m_impl->m_pending[skip])); |
| 1005 | 20 | cursor = skip + 1; | |
| 1006 | } | ||
| 1007 |
1/2✗ Branch 159 → 151 not taken.
✓ Branch 159 → 160 taken 14 times.
|
14 | for (std::size_t i = cursor; i < m_impl->m_pending.size(); ++i) |
| 1008 | { | ||
| 1009 | ✗ | rebuilt.push_back(std::move(m_impl->m_pending[i])); | |
| 1010 | } | ||
| 1011 |
2/2✓ Branch 176 → 162 taken 16 times.
✓ Branch 176 → 177 taken 14 times.
|
60 | for (auto &binding : appended) |
| 1012 | { | ||
| 1013 |
1/2✓ Branch 166 → 167 taken 16 times.
✗ Branch 166 → 224 not taken.
|
16 | rebuilt.push_back(std::move(binding)); |
| 1014 | } | ||
| 1015 | 14 | m_impl->m_pending.swap(rebuilt); | |
| 1016 | 14 | m_impl->advance_start_revision(); | |
| 1017 | 14 | return {}; | |
| 1018 | 50 | } | |
| 1019 |
2/2✓ Branch 188 → 189 taken 2007 times.
✓ Branch 188 → 193 taken 50 times.
|
2057 | } |
| 1020 | ✗ | catch (...) | |
| 1021 | { | ||
| 1022 | // rebind is noexcept-friendly via Result; on out-of-memory the pending bindings are left unchanged | ||
| 1023 | // (allocation precedes the move-commit). | ||
| 1024 | ✗ | (void)log().try_log(LogLevel::Error, "input::Input: out of memory in rebind; bindings unchanged"); | |
| 1025 | ✗ | return std::unexpected(Error{ErrorCode::OutOfMemory, "input::rebind"}); | |
| 1026 | ✗ | } | |
| 1027 | |||
| 1028 | // Forward to the live poller outside m_mutex. Preserve the caller and resource failure classes. | ||
| 1029 |
3/4✓ Branch 192 → 194 taken 2005 times.
✓ Branch 192 → 195 taken 1 time.
✓ Branch 192 → 198 taken 1 time.
✗ Branch 192 → 201 not taken.
|
2007 | switch (local_poller->update_combos(name, combos)) |
| 1030 | { | ||
| 1031 | 2005 | case detail::InputPoller::ComboUpdate::Updated: | |
| 1032 | 2005 | return {}; | |
| 1033 | 1 | case detail::InputPoller::ComboUpdate::NameAbsent: | |
| 1034 | 1 | return std::unexpected(Error{ErrorCode::InvalidArg, "input::rebind"}); | |
| 1035 | 1 | case detail::InputPoller::ComboUpdate::ResourceFailure: | |
| 1036 | 1 | return std::unexpected(Error{ErrorCode::OutOfMemory, "input::rebind"}); | |
| 1037 | } | ||
| 1038 | ✗ | return std::unexpected(Error{ErrorCode::InvalidArg, "input::rebind"}); | |
| 1039 | 2057 | } | |
| 1040 | |||
| 1041 | 17 | void Input::set_consume(std::string_view name, bool consume) noexcept | |
| 1042 | { | ||
| 1043 |
2/2✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 5 taken 15 times.
|
17 | if (is_inert()) |
| 1044 | { | ||
| 1045 | 10 | return; | |
| 1046 | } | ||
| 1047 | |||
| 1048 | 15 | std::shared_ptr<detail::InputPoller> live_poller; | |
| 1049 | |||
| 1050 | { | ||
| 1051 | 15 | std::lock_guard lock(m_impl->m_mutex); | |
| 1052 |
2/2✓ Branch 9 → 10 taken 7 times.
✓ Branch 9 → 12 taken 8 times.
|
15 | if (m_impl->m_poller) |
| 1053 | { | ||
| 1054 | 7 | live_poller = m_impl->m_poller; | |
| 1055 | } | ||
| 1056 | else | ||
| 1057 | { | ||
| 1058 | 8 | bool changed = false; | |
| 1059 |
2/2✓ Branch 34 → 15 taken 8 times.
✓ Branch 34 → 35 taken 8 times.
|
24 | for (auto &binding : m_impl->m_pending) |
| 1060 | { | ||
| 1061 |
6/6✓ Branch 19 → 20 taken 7 times.
✓ Branch 19 → 22 taken 1 time.
✓ Branch 20 → 21 taken 4 times.
✓ Branch 20 → 22 taken 3 times.
✓ Branch 23 → 24 taken 4 times.
✓ Branch 23 → 25 taken 4 times.
|
8 | if (binding.name == name && binding.consume != consume) |
| 1062 | { | ||
| 1063 | 4 | binding.consume = consume; | |
| 1064 | 4 | changed = true; | |
| 1065 | } | ||
| 1066 | } | ||
| 1067 |
2/2✓ Branch 35 → 36 taken 4 times.
✓ Branch 35 → 38 taken 4 times.
|
8 | if (changed) |
| 1068 | { | ||
| 1069 | 4 | m_impl->advance_start_revision(); | |
| 1070 | } | ||
| 1071 | 8 | return; | |
| 1072 | } | ||
| 1073 |
2/2✓ Branch 41 → 42 taken 7 times.
✓ Branch 41 → 46 taken 8 times.
|
15 | } |
| 1074 | |||
| 1075 | // Forward outside m_mutex so the poller's exclusive binding lock cannot deadlock against a caller holding | ||
| 1076 | // m_mutex (matches register_combo). | ||
| 1077 | 7 | live_poller->set_consume(name, consume); | |
| 1078 |
2/2✓ Branch 48 → 49 taken 7 times.
✓ Branch 48 → 51 taken 8 times.
|
15 | } |
| 1079 | |||
| 1080 | 111 | void Input::set_consume_by_owner(std::uint64_t owner, bool consume) noexcept | |
| 1081 | { | ||
| 1082 | // Identity-keyed counterpart to set_consume(name), used by a consume guard's teardown so an empty-name | ||
| 1083 | // binding (absent from the name index) still has its suppression lifted. Mirrors set_consume's live-vs- | ||
| 1084 | // pending routing: clear on the live poller if running, else on the staged bindings for the next start(). | ||
| 1085 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 111 times.
|
111 | if (is_inert()) |
| 1086 | { | ||
| 1087 | 1 | return; | |
| 1088 | } | ||
| 1089 | |||
| 1090 | 111 | std::shared_ptr<detail::InputPoller> live_poller; | |
| 1091 | |||
| 1092 | { | ||
| 1093 | 111 | std::lock_guard lock(m_impl->m_mutex); | |
| 1094 |
2/2✓ Branch 9 → 10 taken 110 times.
✓ Branch 9 → 12 taken 1 time.
|
111 | if (m_impl->m_poller) |
| 1095 | { | ||
| 1096 | 110 | live_poller = m_impl->m_poller; | |
| 1097 | } | ||
| 1098 | else | ||
| 1099 | { | ||
| 1100 | 1 | bool changed = false; | |
| 1101 |
1/2✓ Branch 12 → 13 taken 1 time.
✗ Branch 12 → 32 not taken.
|
1 | if (owner != 0) |
| 1102 | { | ||
| 1103 |
1/2✗ Branch 30 → 16 not taken.
✓ Branch 30 → 31 taken 1 time.
|
2 | for (auto &binding : m_impl->m_pending) |
| 1104 | { | ||
| 1105 | ✗ | if (binding.consume_owner == owner && binding.consume != consume) | |
| 1106 | { | ||
| 1107 | ✗ | binding.consume = consume; | |
| 1108 | ✗ | changed = true; | |
| 1109 | } | ||
| 1110 | } | ||
| 1111 | } | ||
| 1112 |
1/2✗ Branch 32 → 33 not taken.
✓ Branch 32 → 35 taken 1 time.
|
1 | if (changed) |
| 1113 | { | ||
| 1114 | ✗ | m_impl->advance_start_revision(); | |
| 1115 | } | ||
| 1116 | 1 | return; | |
| 1117 | } | ||
| 1118 |
2/2✓ Branch 38 → 39 taken 110 times.
✓ Branch 38 → 43 taken 1 time.
|
111 | } |
| 1119 | |||
| 1120 | // Forward outside m_mutex so the poller's exclusive binding lock cannot deadlock against a caller holding | ||
| 1121 | // m_mutex (matches register_combo). | ||
| 1122 | 110 | live_poller->set_consume_by_owner(owner, consume); | |
| 1123 |
2/2✓ Branch 45 → 46 taken 110 times.
✓ Branch 45 → 48 taken 1 time.
|
111 | } |
| 1124 | |||
| 1125 | 100 | void Input::set_require_focus(bool require_focus) noexcept | |
| 1126 | { | ||
| 1127 |
2/2✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 5 taken 98 times.
|
100 | if (is_inert()) |
| 1128 | { | ||
| 1129 | 2 | return; | |
| 1130 | } | ||
| 1131 | |||
| 1132 | 98 | std::lock_guard lock(m_impl->m_mutex); | |
| 1133 |
2/2✓ Branch 8 → 9 taken 18 times.
✓ Branch 8 → 12 taken 80 times.
|
98 | if (m_impl->m_settings.require_focus != require_focus) |
| 1134 | { | ||
| 1135 | 18 | m_impl->m_settings.require_focus = require_focus; | |
| 1136 | 18 | m_impl->advance_start_revision(); | |
| 1137 | } | ||
| 1138 |
2/2✓ Branch 14 → 15 taken 7 times.
✓ Branch 14 → 18 taken 91 times.
|
98 | if (m_impl->m_poller) |
| 1139 | { | ||
| 1140 | 7 | m_impl->m_poller->set_require_focus(require_focus); | |
| 1141 | } | ||
| 1142 | 98 | } | |
| 1143 | |||
| 1144 | 429 | std::size_t Input::remove_bindings_by_name(std::string_view name, bool invoke_callbacks) noexcept | |
| 1145 | { | ||
| 1146 |
2/2✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 5 taken 427 times.
|
429 | if (is_inert()) |
| 1147 | { | ||
| 1148 | 2 | return 0; | |
| 1149 | } | ||
| 1150 | |||
| 1151 | 427 | std::shared_ptr<detail::InputPoller> live_poller; | |
| 1152 | 427 | std::size_t removed_pending = 0; | |
| 1153 | 427 | std::vector<detail::InputBinding> retired; | |
| 1154 | 427 | std::vector<detail::InputBinding> staged; | |
| 1155 | |||
| 1156 | try | ||
| 1157 | { | ||
| 1158 |
1/2✓ Branch 6 → 7 taken 427 times.
✗ Branch 6 → 60 not taken.
|
427 | std::lock_guard lock(m_impl->m_mutex); |
| 1159 |
2/2✓ Branch 9 → 10 taken 409 times.
✓ Branch 9 → 12 taken 18 times.
|
427 | if (m_impl->m_poller) |
| 1160 | { | ||
| 1161 | 409 | live_poller = m_impl->m_poller; | |
| 1162 | } | ||
| 1163 | else | ||
| 1164 | { | ||
| 1165 | 54 | removed_pending = static_cast<std::size_t>(std::ranges::count_if( | |
| 1166 |
1/2✓ Branch 13 → 14 taken 18 times.
✗ Branch 13 → 58 not taken.
|
18 | m_impl->m_pending, |
| 1167 | 22 | [name](const detail::InputBinding &b) { return b.name == name; } | |
| 1168 | )); | ||
| 1169 |
2/2✓ Branch 14 → 15 taken 16 times.
✓ Branch 14 → 46 taken 2 times.
|
18 | if (removed_pending != 0) |
| 1170 | { | ||
| 1171 | // Before mutation, reserve both batches so allocation failure preserves the staged set. | ||
| 1172 |
2/2✓ Branch 15 → 16 taken 15 times.
✓ Branch 15 → 58 taken 1 time.
|
16 | retired.reserve(removed_pending); |
| 1173 |
2/2✓ Branch 18 → 19 taken 14 times.
✓ Branch 18 → 58 taken 1 time.
|
15 | staged.reserve(m_impl->m_pending.size() - removed_pending); |
| 1174 |
2/2✓ Branch 41 → 22 taken 18 times.
✓ Branch 41 → 42 taken 14 times.
|
46 | for (detail::InputBinding &entry : m_impl->m_pending) |
| 1175 | { | ||
| 1176 |
3/4✓ Branch 26 → 27 taken 14 times.
✓ Branch 26 → 28 taken 4 times.
✓ Branch 31 → 32 taken 18 times.
✗ Branch 31 → 57 not taken.
|
36 | (entry.name == name ? retired : staged).push_back(std::move(entry)); |
| 1177 | } | ||
| 1178 | 14 | m_impl->m_pending.swap(staged); | |
| 1179 | 14 | m_impl->advance_start_revision(); | |
| 1180 | } | ||
| 1181 | } | ||
| 1182 | 427 | } | |
| 1183 | 2 | catch (...) | |
| 1184 | { | ||
| 1185 | 2 | (void)log().try_log( | |
| 1186 | LogLevel::Error, | ||
| 1187 | "input::Input: out of memory in remove_bindings_by_name. Bindings unchanged" | ||
| 1188 | ); | ||
| 1189 | 2 | return 0; | |
| 1190 | 2 | } | |
| 1191 | |||
| 1192 |
2/2✓ Branch 48 → 49 taken 409 times.
✓ Branch 48 → 51 taken 16 times.
|
425 | if (live_poller) |
| 1193 | { | ||
| 1194 | 409 | return live_poller->remove_bindings_by_name(name, invoke_callbacks); | |
| 1195 | } | ||
| 1196 | 16 | return removed_pending; | |
| 1197 | 426 | } | |
| 1198 | |||
| 1199 | 128 | void Input::clear_bindings(bool invoke_callbacks) noexcept | |
| 1200 | { | ||
| 1201 |
2/2✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 5 taken 126 times.
|
128 | if (is_inert()) |
| 1202 | { | ||
| 1203 | 2 | return; | |
| 1204 | } | ||
| 1205 | |||
| 1206 | 126 | std::shared_ptr<detail::InputPoller> live_poller; | |
| 1207 | 126 | std::vector<detail::InputBinding> retired; | |
| 1208 | |||
| 1209 | { | ||
| 1210 | 126 | std::lock_guard lock(m_impl->m_mutex); | |
| 1211 |
2/2✓ Branch 9 → 10 taken 6 times.
✓ Branch 9 → 12 taken 120 times.
|
126 | if (!m_impl->m_pending.empty()) |
| 1212 | { | ||
| 1213 | 6 | m_impl->advance_start_revision(); | |
| 1214 | } | ||
| 1215 | 126 | retired.swap(m_impl->m_pending); | |
| 1216 |
2/2✓ Branch 16 → 17 taken 111 times.
✓ Branch 16 → 19 taken 15 times.
|
126 | if (m_impl->m_poller) |
| 1217 | { | ||
| 1218 | 111 | live_poller = m_impl->m_poller; | |
| 1219 | } | ||
| 1220 | 126 | } | |
| 1221 | 126 | retired.clear(); | |
| 1222 | |||
| 1223 |
2/2✓ Branch 22 → 23 taken 111 times.
✓ Branch 22 → 25 taken 15 times.
|
126 | if (live_poller) |
| 1224 | { | ||
| 1225 | 111 | live_poller->clear_bindings(invoke_callbacks); | |
| 1226 | } | ||
| 1227 | 126 | } | |
| 1228 | |||
| 1229 | 142 | bool Input::retire_gates_for_unload( | |
| 1230 | std::span<const std::string_view> binding_names, | ||
| 1231 | bool every_binding, | ||
| 1232 | std::chrono::steady_clock::time_point deadline | ||
| 1233 | ) noexcept | ||
| 1234 | { | ||
| 1235 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 142 times.
|
142 | if (is_inert()) |
| 1236 | { | ||
| 1237 | ✗ | return true; | |
| 1238 | } | ||
| 1239 | |||
| 1240 | 142 | std::shared_ptr<detail::InputPoller> live_poller; | |
| 1241 | 142 | std::vector<std::shared_ptr<detail::BindingGate>> pending_gates; | |
| 1242 | 142 | bool collected = true; | |
| 1243 | |||
| 1244 | { | ||
| 1245 | 142 | std::lock_guard lock(m_impl->m_mutex); | |
| 1246 | 142 | live_poller = m_impl->m_poller; | |
| 1247 |
2/2✓ Branch 10 → 11 taken 28 times.
✓ Branch 10 → 39 taken 114 times.
|
142 | if (!live_poller) |
| 1248 | { | ||
| 1249 | // Staged but never started: the gates exist and the guards are already handed out, so a pending | ||
| 1250 | // binding's callback outlives removal exactly as a live one does. | ||
| 1251 | try | ||
| 1252 | { | ||
| 1253 |
2/2✓ Branch 37 → 14 taken 13 times.
✓ Branch 37 → 38 taken 28 times.
|
69 | for (const detail::InputBinding &staged : m_impl->m_pending) |
| 1254 | { | ||
| 1255 | const bool selected = | ||
| 1256 |
5/6✓ Branch 16 → 17 taken 8 times.
✓ Branch 16 → 19 taken 5 times.
✓ Branch 17 → 18 taken 8 times.
✗ Branch 17 → 87 not taken.
✓ Branch 18 → 19 taken 6 times.
✓ Branch 18 → 20 taken 2 times.
|
13 | every_binding || std::ranges::any_of( |
| 1257 | binding_names, | ||
| 1258 | 6 | [&staged](std::string_view name) { return staged.name == name; } | |
| 1259 | 13 | ); | |
| 1260 |
5/6✓ Branch 21 → 22 taken 11 times.
✓ Branch 21 → 25 taken 2 times.
✓ Branch 23 → 24 taken 11 times.
✗ Branch 23 → 25 not taken.
✓ Branch 26 → 27 taken 11 times.
✓ Branch 26 → 28 taken 2 times.
|
13 | if (selected && staged.gate) |
| 1261 | { | ||
| 1262 |
1/2✓ Branch 27 → 28 taken 11 times.
✗ Branch 27 → 87 not taken.
|
11 | pending_gates.push_back(staged.gate); |
| 1263 | } | ||
| 1264 | } | ||
| 1265 | } | ||
| 1266 | ✗ | catch (...) | |
| 1267 | { | ||
| 1268 | ✗ | collected = false; | |
| 1269 | ✗ | } | |
| 1270 | } | ||
| 1271 | 142 | } | |
| 1272 | |||
| 1273 |
2/2✓ Branch 41 → 42 taken 114 times.
✓ Branch 41 → 64 taken 28 times.
|
142 | if (live_poller) |
| 1274 | { | ||
| 1275 |
2/2✓ Branch 42 → 43 taken 110 times.
✓ Branch 42 → 45 taken 4 times.
|
114 | if (every_binding) |
| 1276 | { | ||
| 1277 | 110 | return live_poller->retire_all_gates(deadline); | |
| 1278 | } | ||
| 1279 | 4 | bool retired_all = true; | |
| 1280 |
2/2✓ Branch 62 → 47 taken 4 times.
✓ Branch 62 → 63 taken 4 times.
|
12 | for (const std::string_view name : binding_names) |
| 1281 | { | ||
| 1282 |
2/2✓ Branch 51 → 52 taken 1 time.
✓ Branch 51 → 53 taken 3 times.
|
4 | if (!live_poller->retire_gates_by_name(name, deadline)) |
| 1283 | { | ||
| 1284 | 1 | retired_all = false; | |
| 1285 | } | ||
| 1286 | } | ||
| 1287 | 4 | return retired_all; | |
| 1288 | } | ||
| 1289 | |||
| 1290 | // Off m_mutex: a retired hold delivers its balancing edge, and that consumer code may call back into the | ||
| 1291 | // facade. The gates are kept alive by the copies taken above, so a concurrent clear cannot free them here. | ||
| 1292 |
2/2✓ Branch 81 → 66 taken 11 times.
✓ Branch 81 → 82 taken 28 times.
|
67 | for (const auto &gate : pending_gates) |
| 1293 | { | ||
| 1294 | try | ||
| 1295 | { | ||
| 1296 |
2/4✓ Branch 69 → 70 taken 11 times.
✗ Branch 69 → 91 not taken.
✗ Branch 70 → 71 not taken.
✓ Branch 70 → 72 taken 11 times.
|
11 | if (!gate->retire(deadline)) |
| 1297 | { | ||
| 1298 | ✗ | collected = false; | |
| 1299 | } | ||
| 1300 | } | ||
| 1301 | ✗ | catch (...) | |
| 1302 | { | ||
| 1303 | // retire() moved the callback out before invoking it, so it is destroyed even on this path and | ||
| 1304 | // only the consumer's balancing edge failed. | ||
| 1305 | ✗ | } | |
| 1306 | } | ||
| 1307 | 28 | return collected; | |
| 1308 | 142 | } | |
| 1309 | |||
| 1310 | 19 | CallbackDrainStatus Input::prepare_logic_dll_unload( | |
| 1311 | std::span<const std::string_view> binding_names, | ||
| 1312 | std::chrono::milliseconds timeout | ||
| 1313 | ) noexcept | ||
| 1314 | { | ||
| 1315 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 19 times.
|
19 | if (detail::current_thread_in_delivery()) |
| 1316 | { | ||
| 1317 | ✗ | return CallbackDrainStatus::SelfDelivery; | |
| 1318 | } | ||
| 1319 |
1/2✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 19 times.
|
19 | if (is_inert()) |
| 1320 | { | ||
| 1321 | ✗ | return CallbackDrainStatus::Drained; | |
| 1322 | } | ||
| 1323 |
1/2✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 19 times.
|
19 | if (m_impl->m_callback_drain_active.exchange(true, std::memory_order_seq_cst)) |
| 1324 | { | ||
| 1325 | ✗ | return CallbackDrainStatus::InProgress; | |
| 1326 | } | ||
| 1327 | |||
| 1328 | 19 | detail::mark_input_callback_drain_pending(); | |
| 1329 | 19 | const auto deadline = detail::drain_deadline(timeout); | |
| 1330 | |||
| 1331 | 19 | CallbackDrainStatus status = CallbackDrainStatus::Drained; | |
| 1332 | // Both refusals are the same outcome, so they share one branch and short-circuit order keeps the admission | ||
| 1333 | // check first. An unretired gate means either a selected binding was still delivering at the deadline, in | ||
| 1334 | // which case its callback is deliberately left alive because destroying a callable a poll thread is | ||
| 1335 | // executing would free the code out from under it, or the gate handles could not be collected at all under | ||
| 1336 | // memory pressure. Neither outcome has established that the callbacks are gone, so both refuse the unmap. | ||
| 1337 |
3/4✓ Branch 16 → 17 taken 19 times.
✗ Branch 16 → 19 not taken.
✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 23 taken 18 times.
|
38 | if (!await_admission_commits(m_impl->m_admission_commits_inflight, deadline) || |
| 1338 |
2/2✓ Branch 18 → 19 taken 1 time.
✓ Branch 18 → 20 taken 18 times.
|
19 | !retire_gates_for_unload(binding_names, false, deadline)) |
| 1339 | { | ||
| 1340 | 1 | status = CallbackDrainStatus::TimedOut; | |
| 1341 | } | ||
| 1342 | else | ||
| 1343 | { | ||
| 1344 | 18 | bool retire_failed = false; | |
| 1345 |
2/2✓ Branch 59 → 25 taken 10 times.
✓ Branch 59 → 60 taken 18 times.
|
46 | for (const std::string_view name : binding_names) |
| 1346 | { | ||
| 1347 | 10 | (void)remove_bindings_by_name(name, false); | |
| 1348 | |||
| 1349 | 10 | std::shared_ptr<detail::InputPoller> live_poller; | |
| 1350 | 10 | bool pending_match = false; | |
| 1351 | { | ||
| 1352 | 10 | std::lock_guard lock(m_impl->m_mutex); | |
| 1353 | 30 | pending_match = std::ranges::any_of( | |
| 1354 | 10 | m_impl->m_pending, | |
| 1355 | ✗ | [name](const detail::InputBinding &binding) { return binding.name == name; } | |
| 1356 | ); | ||
| 1357 | 10 | live_poller = m_impl->m_poller; | |
| 1358 | 10 | } | |
| 1359 |
5/8✓ Branch 35 → 36 taken 10 times.
✗ Branch 35 → 41 not taken.
✓ Branch 37 → 38 taken 3 times.
✓ Branch 37 → 42 taken 7 times.
✗ Branch 40 → 41 not taken.
✓ Branch 40 → 42 taken 3 times.
✗ Branch 43 → 44 not taken.
✓ Branch 43 → 45 taken 10 times.
|
10 | if (pending_match || (live_poller && live_poller->has_bindings_by_name(name))) |
| 1360 | { | ||
| 1361 | ✗ | retire_failed = true; | |
| 1362 | ✗ | break; | |
| 1363 | } | ||
| 1364 |
1/2✓ Branch 47 → 48 taken 10 times.
✗ Branch 47 → 52 not taken.
|
10 | } |
| 1365 | |||
| 1366 |
1/2✗ Branch 60 → 61 not taken.
✓ Branch 60 → 62 taken 18 times.
|
18 | if (retire_failed) |
| 1367 | { | ||
| 1368 | ✗ | status = CallbackDrainStatus::RetireFailed; | |
| 1369 | } | ||
| 1370 |
2/2✓ Branch 63 → 64 taken 1 time.
✓ Branch 63 → 65 taken 17 times.
|
18 | else if (!detail::await_staged_input_callbacks(deadline)) |
| 1371 | { | ||
| 1372 | 1 | status = CallbackDrainStatus::TimedOut; | |
| 1373 | } | ||
| 1374 | } | ||
| 1375 | |||
| 1376 |
2/2✓ Branch 65 → 66 taken 17 times.
✓ Branch 65 → 67 taken 2 times.
|
19 | if (status == CallbackDrainStatus::Drained) |
| 1377 | { | ||
| 1378 | 17 | detail::resolve_input_callback_drain(); | |
| 1379 | } | ||
| 1380 | |||
| 1381 | 19 | m_impl->m_callback_drain_active.store(false, std::memory_order_release); | |
| 1382 | 19 | return status; | |
| 1383 | } | ||
| 1384 | |||
| 1385 | 124 | CallbackDrainStatus Input::prepare_logic_dll_unload_all(std::chrono::milliseconds timeout) noexcept | |
| 1386 | { | ||
| 1387 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 124 times.
|
124 | if (detail::current_thread_in_delivery()) |
| 1388 | { | ||
| 1389 | ✗ | return CallbackDrainStatus::SelfDelivery; | |
| 1390 | } | ||
| 1391 |
1/2✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 124 times.
|
124 | if (is_inert()) |
| 1392 | { | ||
| 1393 | ✗ | return CallbackDrainStatus::Drained; | |
| 1394 | } | ||
| 1395 |
1/2✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 124 times.
|
124 | if (m_impl->m_callback_drain_active.exchange(true, std::memory_order_seq_cst)) |
| 1396 | { | ||
| 1397 | ✗ | return CallbackDrainStatus::InProgress; | |
| 1398 | } | ||
| 1399 | |||
| 1400 | 124 | detail::mark_input_callback_drain_pending(); | |
| 1401 | 124 | const auto deadline = detail::drain_deadline(timeout); | |
| 1402 | |||
| 1403 | 124 | CallbackDrainStatus status = CallbackDrainStatus::Drained; | |
| 1404 | // One branch for both refusals, for the reason given in prepare_logic_dll_unload. | ||
| 1405 |
2/2✓ Branch 16 → 17 taken 123 times.
✓ Branch 16 → 20 taken 1 time.
|
247 | if (!await_admission_commits(m_impl->m_admission_commits_inflight, deadline) || |
| 1406 |
4/4✓ Branch 19 → 20 taken 2 times.
✓ Branch 19 → 21 taken 121 times.
✓ Branch 22 → 23 taken 3 times.
✓ Branch 22 → 24 taken 121 times.
|
247 | !retire_gates_for_unload({}, true, deadline)) |
| 1407 | { | ||
| 1408 | 3 | status = CallbackDrainStatus::TimedOut; | |
| 1409 | } | ||
| 1410 | else | ||
| 1411 | { | ||
| 1412 | 121 | clear_bindings(false); | |
| 1413 |
1/2✗ Branch 26 → 27 not taken.
✓ Branch 26 → 28 taken 121 times.
|
121 | if (binding_count() != 0) |
| 1414 | { | ||
| 1415 | ✗ | status = CallbackDrainStatus::RetireFailed; | |
| 1416 | } | ||
| 1417 |
1/2✗ Branch 29 → 30 not taken.
✓ Branch 29 → 31 taken 121 times.
|
121 | else if (!detail::await_staged_input_callbacks(deadline)) |
| 1418 | { | ||
| 1419 | ✗ | status = CallbackDrainStatus::TimedOut; | |
| 1420 | } | ||
| 1421 | } | ||
| 1422 | |||
| 1423 |
2/2✓ Branch 31 → 32 taken 121 times.
✓ Branch 31 → 33 taken 3 times.
|
124 | if (status == CallbackDrainStatus::Drained) |
| 1424 | { | ||
| 1425 | 121 | detail::resolve_input_callback_drain(); | |
| 1426 | } | ||
| 1427 | |||
| 1428 | 124 | m_impl->m_callback_drain_active.store(false, std::memory_order_release); | |
| 1429 | 124 | return status; | |
| 1430 | } | ||
| 1431 | |||
| 1432 | // Free-function ergonomics | ||
| 1433 | |||
| 1434 | 814 | Result<BindingGuard> register_combo(ComboBinding binding) noexcept | |
| 1435 | { | ||
| 1436 | 1628 | return Input::instance().register_combo(std::move(binding)); | |
| 1437 | } | ||
| 1438 | |||
| 1439 | 1 | Scope &scope() noexcept | |
| 1440 | { | ||
| 1441 | alignas(Scope) static unsigned char storage[sizeof(Scope)]; | ||
| 1442 |
3/6✓ Branch 2 → 3 taken 1 time.
✗ Branch 2 → 10 not taken.
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 10 not taken.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 1 time.
|
1 | static Scope *const process_scope = ::new (static_cast<void *>(storage)) Scope(); |
| 1443 | 1 | return *process_scope; | |
| 1444 | } | ||
| 1445 | } // namespace input | ||
| 1446 | } // namespace DetourModKit | ||
| 1447 | |||
| 1448 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 1449 | namespace DetourModKit::detail | ||
| 1450 | { | ||
| 1451 | // Friend-accessor bodies live beside the facade state they reach. The unnamed-namespace seam objects above are | ||
| 1452 | // reachable through the input namespace by qualified lookup. | ||
| 1453 | 21 | void InputTestSeams::set_callback_admission_commit_seam_for_test(CallbackAdmissionCommitSeam seam) noexcept | |
| 1454 | { | ||
| 1455 | 21 | input::s_callback_admission_commit_seam.store(seam, std::memory_order_release); | |
| 1456 | 21 | } | |
| 1457 | |||
| 1458 | 2 | void InputTestSeams::lock_facade_mutex_for_test() noexcept | |
| 1459 | { | ||
| 1460 | 2 | input::Input::Impl *const impl = input::Input::instance().m_impl.get(); | |
| 1461 | 2 | input::s_test_locked_impl = impl; | |
| 1462 | 2 | impl->m_mutex.lock(); | |
| 1463 | 2 | } | |
| 1464 | |||
| 1465 | 1 | void InputTestSeams::unlock_facade_mutex_for_test() noexcept | |
| 1466 | { | ||
| 1467 | 1 | static_cast<input::Input::Impl *>(input::s_test_locked_impl)->m_mutex.unlock(); | |
| 1468 | 1 | input::s_test_locked_impl = nullptr; | |
| 1469 | 1 | } | |
| 1470 | |||
| 1471 | 4 | bool InputTestSeams::reclaim_vetoed_impl_for_test() noexcept | |
| 1472 | { | ||
| 1473 | 4 | void *const retained = input::s_vetoed_retained_impl.exchange(nullptr, std::memory_order_acq_rel); | |
| 1474 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 4 times.
|
4 | if (retained == nullptr) |
| 1475 | { | ||
| 1476 | ✗ | return false; | |
| 1477 | } | ||
| 1478 | 4 | input::Input &self = input::Input::instance(); | |
| 1479 | 4 | auto *const impl = static_cast<input::Input::Impl *>(retained); | |
| 1480 |
1/2✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 4 times.
|
4 | if (self.m_impl.get() != impl) |
| 1481 | { | ||
| 1482 | ✗ | return false; | |
| 1483 | } | ||
| 1484 | 4 | bool vetoed = true; | |
| 1485 | return impl->m_vetoed_retained | ||
| 1486 | 4 | .compare_exchange_strong(vetoed, false, std::memory_order_acq_rel, std::memory_order_acquire); | |
| 1487 | } | ||
| 1488 | |||
| 1489 | 8 | bool InputTestSeams::adopt_intercept_owner_for_test() noexcept | |
| 1490 | { | ||
| 1491 | 8 | input::Input &self = input::Input::instance(); | |
| 1492 | 8 | std::shared_ptr<InputPoller> live_poller; | |
| 1493 |
1/2✓ Branch 4 → 5 taken 8 times.
✗ Branch 4 → 11 not taken.
|
8 | if (!self.is_inert()) |
| 1494 | { | ||
| 1495 | 8 | std::lock_guard lock(self.m_impl->m_mutex); | |
| 1496 | 8 | live_poller = self.m_impl->m_poller; | |
| 1497 | 8 | } | |
| 1498 |
1/2✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 8 times.
|
8 | if (!live_poller) |
| 1499 | { | ||
| 1500 | ✗ | return false; | |
| 1501 | } | ||
| 1502 |
1/2✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 8 times.
|
8 | if (!adopt_owner_for_test(live_poller->intercept_owner_for_test())) |
| 1503 | { | ||
| 1504 | ✗ | return false; | |
| 1505 | } | ||
| 1506 | 8 | live_poller->publish_consume_rules_for_test(); | |
| 1507 | 8 | return true; | |
| 1508 | 8 | } | |
| 1509 | } // namespace DetourModKit::detail | ||
| 1510 | #endif // DMK_ENABLE_TEST_SEAMS | ||
| 1511 |