GCC Code Coverage Report


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

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 255 std::mutex &get_reload_apply_mutex()
32 {
33
3/4
✓ Branch 2 → 3 taken 143 times.
✓ Branch 2 → 8 taken 112 times.
✓ Branch 4 → 5 taken 143 times.
✗ Branch 4 → 8 not taken.
255 static std::mutex s_mtx;
34 255 return s_mtx;
35 }
36
37 // This thread-local marker detects pass-lock re-entry without publication of a cross-thread owner id.
38 1825 bool &reload_apply_lock_slot() noexcept
39 {
40 thread_local bool s_held = false;
41 1825 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 670 std::atomic<std::uint64_t> &reload_lifecycle_state() noexcept
51 {
52 static std::atomic<std::uint64_t> s_state{0};
53 670 return s_state;
54 }
55
56 461 std::atomic<bool> &reload_drain_active() noexcept
57 {
58 static std::atomic<bool> s_active{false};
59 461 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 244747 std::atomic<int> &reload_in_flight_count() noexcept
65 {
66 static std::atomic<int> s_in_flight{0};
67 244747 return s_in_flight;
68 }
69
70 130 bool reloads_quiesced_now() noexcept
71 {
72 260 return reload_in_flight_count().load(std::memory_order_seq_cst) == 0;
73 }
74 } // anonymous namespace
75
76 namespace detail
77 {
78 257 ReloadApplyLock::ReloadApplyLock()
79 {
80
2/2
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 6 taken 255 times.
257 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 255 times.
✗ Branch 7 → 13 not taken.
255 m_lock = std::unique_lock<std::mutex>(get_reload_apply_mutex());
86 255 reload_apply_lock_slot() = true;
87 255 m_engaged = true;
88 }
89
90 257 ReloadApplyLock::~ReloadApplyLock() noexcept
91 {
92 257 unlock();
93 257 }
94
95 440 void ReloadApplyLock::unlock() noexcept
96 {
97
6/6
✓ Branch 2 → 3 taken 438 times.
✓ Branch 2 → 6 taken 2 times.
✓ Branch 4 → 5 taken 255 times.
✓ Branch 4 → 6 taken 183 times.
✓ Branch 7 → 8 taken 255 times.
✓ Branch 7 → 10 taken 185 times.
440 if (m_engaged && m_lock.owns_lock())
98 {
99 255 reload_apply_lock_slot() = false;
100 255 m_lock.unlock();
101 }
102 440 }
103
104 923 bool reload_apply_lock_held_by_current_thread() noexcept
105 {
106 923 return reload_apply_lock_slot();
107 }
108
109 60 std::uint64_t current_reload_lifecycle_epoch() noexcept
110 {
111 120 return reload_lifecycle_state().load(std::memory_order_seq_cst) & ~RELOADS_DISABLED_BIT;
112 }
113
114 184 bool background_reloads_disabled() noexcept
115 {
116 368 return (reload_lifecycle_state().load(std::memory_order_seq_cst) & RELOADS_DISABLED_BIT) != 0;
117 }
118
119 29 BackgroundReloadGuard::BackgroundReloadGuard(std::uint64_t expected_epoch) noexcept
120 29 : m_expected_epoch(expected_epoch)
121 {
122
2/2
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 5 taken 27 times.
29 if (!lifecycle_current())
123 {
124 2 return;
125 }
126 27 reload_in_flight_count().fetch_add(1, std::memory_order_seq_cst);
127
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 14 taken 27 times.
27 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 27 m_engaged = true;
134 }
135
136 29 BackgroundReloadGuard::~BackgroundReloadGuard() noexcept
137 {
138
2/2
✓ Branch 2 → 3 taken 27 times.
✓ Branch 2 → 7 taken 2 times.
29 if (m_engaged)
139 {
140 27 reload_in_flight_count().fetch_sub(1, std::memory_order_seq_cst);
141 }
142 29 }
143
144 36 bool BackgroundReloadGuard::current() const noexcept
145 {
146
3/4
✓ Branch 2 → 3 taken 36 times.
✗ Branch 2 → 6 not taken.
✓ Branch 4 → 5 taken 34 times.
✓ Branch 4 → 6 taken 2 times.
36 return m_engaged && lifecycle_current();
147 }
148
149 92 bool BackgroundReloadGuard::lifecycle_current() const noexcept
150 {
151 184 return reload_lifecycle_state().load(std::memory_order_seq_cst) == m_expected_epoch;
152 }
153
154 137 void disable_reloads_for_unload() noexcept
155 {
156 137 reload_lifecycle_state().fetch_or(RELOADS_DISABLED_BIT, std::memory_order_seq_cst);
157 137 }
158
159 135 ReloadDrainStatus begin_reload_drain() noexcept
160 {
161
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 135 times.
135 if (reload_apply_lock_slot())
162 {
163 return ReloadDrainStatus::SelfDelivery;
164 }
165
166 135 bool expected = false;
167
2/2
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 134 times.
135 if (!reload_drain_active().compare_exchange_strong(expected, true, std::memory_order_seq_cst))
168 {
169 1 return ReloadDrainStatus::InProgress;
170 }
171 134 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 134 times.
✗ Branch 11 → 16 not taken.
134 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 134 case WatchStopPoke::LockBusy:
181 case WatchStopPoke::Requested:
182 134 break;
183 }
184 134 return ReloadDrainStatus::Ready;
185 }
186
187 134 ReloadDrainStatus finish_reload_drain(std::chrono::steady_clock::time_point deadline) noexcept
188 {
189 134 WatchTeardown teardown;
190 while (true)
191 {
192 1930745 const WatchDrainState state = try_detach_watch_control(&reloads_quiesced_now, teardown);
193
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 8 taken 1930745 times.
1930745 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 130 times.
✓ Branch 8 → 10 taken 1930615 times.
1930745 if (state == WatchDrainState::Detached)
200 {
201 130 break;
202 }
203
2/2
✓ Branch 13 → 14 taken 4 times.
✓ Branch 13 → 17 taken 1930611 times.
1930615 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 1930611 std::this_thread::yield();
209 1930611 }
210
211
2/2
✓ Branch 20 → 21 taken 5 times.
✓ Branch 20 → 24 taken 125 times.
130 if (teardown.watcher)
212 {
213 5 teardown.watcher->stop();
214 5 teardown.watcher.reset();
215 }
216 130 dispose_reload_hotkey_guards(teardown.guards);
217 130 teardown.servicer.reset();
218 130 teardown.callback = nullptr;
219 130 config::clear();
220 130 reload_drain_active().store(false, std::memory_order_seq_cst);
221 130 return ReloadDrainStatus::Ready;
222 134 }
223
224 #if defined(DMK_ENABLE_TEST_SEAMS)
225 3 bool await_reloads_quiesced_for_test(std::chrono::milliseconds timeout) noexcept
226 {
227 3 const auto deadline = std::chrono::steady_clock::now() + timeout;
228
2/2
✓ Branch 19 → 5 taken 244561 times.
✓ Branch 19 → 20 taken 2 times.
489126 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 244560 times.
244561 if (std::chrono::steady_clock::now() >= deadline)
231 {
232 1 return false;
233 }
234 244560 std::this_thread::yield();
235 }
236 2 return true;
237 }
238 #endif
239
240 192 void rearm_reloads() noexcept
241 {
242
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 192 times.
192 if (reload_drain_active().load(std::memory_order_seq_cst))
243 {
244 5 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 192 std::uint64_t state = reload_lifecycle_state().load(std::memory_order_seq_cst);
249
2/2
✓ Branch 26 → 15 taken 5 times.
✓ Branch 26 → 27 taken 187 times.
192 while ((state & RELOADS_DISABLED_BIT) != 0)
250 {
251 5 const std::uint64_t next_epoch = state + 1;
252
1/2
✓ Branch 24 → 25 taken 5 times.
✗ Branch 24 → 26 not taken.
10 if (reload_lifecycle_state().compare_exchange_weak(state, next_epoch, std::memory_order_seq_cst))
253 {
254 5 return;
255 }
256 }
257 }
258 } // namespace detail
259 } // namespace config
260 } // namespace DetourModKit
261