./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
97 / 106
91.5%
Branches:
39 / 49
79.6%
Functions:
21 / 21
100.0%

src/internal/config_reload.cpp
Line Branch Exec Source
1 /**
2 * @file config_reload.cpp
3 * @brief This TU owns the reload control plane: the pass lock, the background-reload lifecycle gate, and the drain.
4 *
5 * The data-plane pass lives in config.cpp and the watcher control plane in config_watch.cpp. Both reach this state
6 * through internal/config_reload_lifecycle.hpp. The session TU reaches the drain through
7 * internal/config_reload_gate.hpp.
8 */
9
10 #include "internal/config_reload_gate.hpp"
11 #include "internal/config_reload_lifecycle.hpp"
12 #include "internal/config_watch_control.hpp"
13
14 #include "DetourModKit/config.hpp"
15
16 #include <atomic>
17 #include <chrono>
18 #include <cstdint>
19 #include <mutex>
20 #include <thread>
21
22 namespace DetourModKit
23 {
24 namespace config
25 {
26 namespace
27 {
28 // Serializes an entire reload/load pass (read + content-hash decision + deferred-setter application).
29 // Setters run after the config mutex is released, so this separate lock prevents stale pass reorder.
30 // Two reload drivers can otherwise advance the cached hash before an older pass applies its stale snapshot.
31 310 std::mutex &get_reload_apply_mutex()
32 {
33
3/4
✓ Branch 2 → 3 taken 182 times.
✓ Branch 2 → 8 taken 128 times.
✓ Branch 4 → 5 taken 182 times.
✗ Branch 4 → 8 not taken.
310 static std::mutex s_mtx;
34 310 return s_mtx;
35 }
36
37 // This thread-local marker detects pass-lock re-entry without publication of a cross-thread owner id.
38 7864 bool &reload_apply_lock_slot() noexcept
39 {
40 thread_local bool s_held = false;
41 7864 return s_held;
42 }
43
44 // The background-reload gate stops new passes through its latch and tracks an active pass through the
45 // in-flight count. Bit zero is the unload latch. The other even bits form the lifecycle epoch. Together
46 // they make an unload/rearm transition atomic, so no callback can observe a clear latch with the previous
47 // epoch. See internal/config_reload_gate.hpp.
48 inline constexpr std::uint64_t RELOADS_DISABLED_BIT = 1;
49
50 2259 std::atomic<std::uint64_t> &reload_lifecycle_state() noexcept
51 {
52 static std::atomic<std::uint64_t> s_state{0};
53 2259 return s_state;
54 }
55
56 3364 std::atomic<bool> &reload_drain_active() noexcept
57 {
58 static std::atomic<bool> s_active{false};
59 3364 return s_active;
60 }
61
62 // Counts background reload passes that execute consumer code. Safe-drain finalization reads this count
63 // after the latch store.
64 266300 std::atomic<int> &reload_in_flight_count() noexcept
65 {
66 static std::atomic<int> s_in_flight{0};
67 266300 return s_in_flight;
68 }
69
70 1559 bool reloads_quiesced_now() noexcept
71 {
72 3118 return reload_in_flight_count().load(std::memory_order_seq_cst) == 0;
73 }
74 } // anonymous namespace
75
76 namespace detail
77 {
78 312 ReloadApplyLock::ReloadApplyLock()
79 {
80
2/2
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 6 taken 310 times.
312 if (reload_apply_lock_slot())
81 {
82 // Same-thread re-entry causes a self-deadlock. Do NOT lock. Leave disengaged.
83 2 return;
84 }
85
1/2
✓ Branch 7 → 8 taken 310 times.
✗ Branch 7 → 13 not taken.
310 m_lock = std::unique_lock<std::mutex>(get_reload_apply_mutex());
86 310 reload_apply_lock_slot() = true;
87 310 m_engaged = true;
88 ✗ }
89
90 312 ReloadApplyLock::~ReloadApplyLock() noexcept
91 {
92 312 unlock();
93 312 }
94
95 539 void ReloadApplyLock::unlock() noexcept
96 {
97
6/6
✓ Branch 2 → 3 taken 537 times.
✓ Branch 2 → 6 taken 2 times.
✓ Branch 4 → 5 taken 310 times.
✓ Branch 4 → 6 taken 227 times.
✓ Branch 7 → 8 taken 310 times.
✓ Branch 7 → 10 taken 229 times.
539 if (m_engaged && m_lock.owns_lock())
98 {
99 310 reload_apply_lock_slot() = false;
100 310 m_lock.unlock();
101 }
102 539 }
103
104 5368 bool reload_apply_lock_held_by_current_thread() noexcept
105 {
106 5368 return reload_apply_lock_slot();
107 }
108
109 82 std::uint64_t current_reload_lifecycle_epoch() noexcept
110 {
111 164 return reload_lifecycle_state().load(std::memory_order_seq_cst) & ~RELOADS_DISABLED_BIT;
112 }
113
114 245 bool background_reloads_disabled() noexcept
115 {
116 490 return (reload_lifecycle_state().load(std::memory_order_seq_cst) & RELOADS_DISABLED_BIT) != 0;
117 }
118
119 37 BackgroundReloadGuard::BackgroundReloadGuard(std::uint64_t expected_epoch) noexcept
120 37 : m_expected_epoch(expected_epoch)
121 {
122
2/2
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 5 taken 35 times.
37 if (!lifecycle_current())
123 {
124 2 return;
125 }
126 35 reload_in_flight_count().fetch_add(1, std::memory_order_seq_cst);
127
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 14 taken 35 times.
35 if (!lifecycle_current())
128 {
129 // The unload state changed between the first check and admission. Back out.
130 ✗ reload_in_flight_count().fetch_sub(1, std::memory_order_seq_cst);
131 ✗ return;
132 }
133 35 m_engaged = true;
134 }
135
136 37 BackgroundReloadGuard::~BackgroundReloadGuard() noexcept
137 {
138
2/2
✓ Branch 2 → 3 taken 35 times.
✓ Branch 2 → 7 taken 2 times.
37 if (m_engaged)
139 {
140 35 reload_in_flight_count().fetch_sub(1, std::memory_order_seq_cst);
141 }
142 37 }
143
144 51 bool BackgroundReloadGuard::current() const noexcept
145 {
146
3/4
✓ Branch 2 → 3 taken 51 times.
✗ Branch 2 → 6 not taken.
✓ Branch 4 → 5 taken 49 times.
✓ Branch 4 → 6 taken 2 times.
51 return m_engaged && lifecycle_current();
147 }
148
149 123 bool BackgroundReloadGuard::lifecycle_current() const noexcept
150 {
151 246 return reload_lifecycle_state().load(std::memory_order_seq_cst) == m_expected_epoch;
152 }
153
154 1566 void disable_reloads_for_unload() noexcept
155 {
156 1566 reload_lifecycle_state().fetch_or(RELOADS_DISABLED_BIT, std::memory_order_seq_cst);
157 1566 }
158
159 1564 ReloadDrainStatus begin_reload_drain() noexcept
160 {
161
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 1564 times.
1564 if (reload_apply_lock_slot())
162 {
163 ✗ return ReloadDrainStatus::SelfDelivery;
164 }
165
166 1564 bool expected = false;
167
2/2
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 1563 times.
1564 if (!reload_drain_active().compare_exchange_strong(expected, true, std::memory_order_seq_cst))
168 {
169 1 return ReloadDrainStatus::InProgress;
170 }
171 1563 disable_reloads_for_unload();
172
173 // Never wait for the control-plane lock here: finish_reload_drain owns the caller's deadline, and
174 // the disabled lifecycle latch already prevents worker entry into consumer code.
175
1/3
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 15 taken 1563 times.
✗ Branch 11 → 16 not taken.
1563 switch (request_watch_stops_for_drain())
176 {
177 ✗ case WatchStopPoke::SelfDelivery:
178 ✗ reload_drain_active().store(false, std::memory_order_seq_cst);
179 ✗ return ReloadDrainStatus::SelfDelivery;
180 1563 case WatchStopPoke::LockBusy:
181 case WatchStopPoke::Requested:
182 1563 break;
183 }
184 1563 return ReloadDrainStatus::Ready;
185 }
186
187 1563 ReloadDrainStatus finish_reload_drain(std::chrono::steady_clock::time_point deadline) noexcept
188 {
189 1563 WatchTeardown teardown;
190 while (true)
191 {
192 2105132 const WatchDrainState state = try_detach_watch_control(&reloads_quiesced_now, teardown);
193
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 8 taken 2105132 times.
2105132 if (state == WatchDrainState::SelfDelivery)
194 {
195 // Control-mutex contention can hide this identity from begin_reload_drain.
196 ✗ reload_drain_active().store(false, std::memory_order_seq_cst);
197 ✗ return ReloadDrainStatus::SelfDelivery;
198 }
199
2/2
✓ Branch 8 → 9 taken 1559 times.
✓ Branch 8 → 10 taken 2103573 times.
2105132 if (state == WatchDrainState::Detached)
200 {
201 1559 break;
202 }
203
2/2
✓ Branch 13 → 14 taken 4 times.
✓ Branch 13 → 17 taken 2103569 times.
2103573 if (std::chrono::steady_clock::now() >= deadline)
204 {
205 4 reload_drain_active().store(false, std::memory_order_seq_cst);
206 4 return ReloadDrainStatus::TimedOut;
207 }
208 2103569 std::this_thread::yield();
209 2103569 }
210
211
2/2
✓ Branch 20 → 21 taken 6 times.
✓ Branch 20 → 24 taken 1553 times.
1559 if (teardown.watcher)
212 {
213 6 teardown.watcher->stop();
214 6 teardown.watcher.reset();
215 }
216 1559 dispose_reload_hotkey_guards(teardown.guards);
217 1559 teardown.servicer.reset();
218 1559 teardown.callback = nullptr;
219 1559 config::clear();
220 1559 reload_drain_active().store(false, std::memory_order_seq_cst);
221 1559 return ReloadDrainStatus::Ready;
222 1563 }
223
224 #if defined(DMK_ENABLE_TEST_SEAMS)
225 4 bool await_reloads_quiesced_for_test(std::chrono::milliseconds timeout) noexcept
226 {
227 4 const auto deadline = std::chrono::steady_clock::now() + timeout;
228
2/2
✓ Branch 19 → 5 taken 264669 times.
✓ Branch 19 → 20 taken 3 times.
529344 while (reload_in_flight_count().load(std::memory_order_seq_cst) != 0)
229 {
230
2/2
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 264668 times.
264669 if (std::chrono::steady_clock::now() >= deadline)
231 {
232 1 return false;
233 }
234 264668 std::this_thread::yield();
235 }
236 3 return true;
237 }
238 #endif
239
240 237 void rearm_reloads() noexcept
241 {
242
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 237 times.
237 if (reload_drain_active().load(std::memory_order_seq_cst))
243 {
244 6 return;
245 }
246 // Advance the epoch and clear a set latch in one compare-exchange. An ordinary load while already
247 // enabled changes nothing. The in-flight count balances its own admitted older passes.
248 237 std::uint64_t state = reload_lifecycle_state().load(std::memory_order_seq_cst);
249
2/2
✓ Branch 26 → 15 taken 6 times.
✓ Branch 26 → 27 taken 231 times.
237 while ((state & RELOADS_DISABLED_BIT) != 0)
250 {
251 6 const std::uint64_t next_epoch = state + 1;
252
1/2
✓ Branch 24 → 25 taken 6 times.
✗ Branch 24 → 26 not taken.
12 if (reload_lifecycle_state().compare_exchange_weak(state, next_epoch, std::memory_order_seq_cst))
253 {
254 6 return;
255 }
256 }
257 }
258 } // namespace detail
259 } // namespace config
260 } // namespace DetourModKit
261