GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 89.2% 91 / 0 / 102
Functions: 100.0% 6 / 0 / 6
Branches: 58.9% 53 / 0 / 90

src/worker.cpp
Line Branch Exec Source
1 #include "DetourModKit/detail/worker.hpp"
2 #include "DetourModKit/diagnostics.hpp"
3 #include "DetourModKit/logger.hpp"
4 #include "internal/lifecycle_context.hpp"
5 #include "internal/lifecycle_reaper.hpp"
6 #include "internal/worker_start_log.hpp"
7 #include "platform.hpp"
8
9 #include <atomic>
10 #include <memory>
11 #include <system_error>
12 #include <thread>
13 #include <utility>
14
15 namespace DetourModKit
16 {
17 #if defined(DMK_ENABLE_TEST_SEAMS)
18 namespace detail
19 {
20 // A throwing probe exercises construction failure after thread creation but before ownership publication.
21 void (*g_worker_post_thread_start_seam)() = nullptr;
22
23 // A throwing probe exercises shutdown's join/detach-failure containment after its safety guards pass.
24 void (*g_worker_join_fail_seam)() = nullptr;
25 } // namespace detail
26 #endif
27
28 234 StoppableWorker::StoppableWorker(std::string_view name, std::function<void(std::stop_token)> body)
29
3/6
✓ Branch 4 → 5 taken 234 times.
✗ Branch 4 → 48 not taken.
✓ Branch 7 → 8 taken 234 times.
✗ Branch 7 → 96 not taken.
✓ Branch 8 → 9 taken 234 times.
✗ Branch 8 → 51 not taken.
702 : m_name(name), m_state(std::make_shared<std::atomic<State>>(State::Starting))
30 {
31
2/2
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 16 taken 233 times.
234 if (!body)
32 {
33 1 (void)log().try_log(LogLevel::Error, "StoppableWorker '{}': empty body; no thread started.", m_name);
34 1 m_state->store(State::Stopped, std::memory_order_release);
35 1 return;
36 }
37
38 // Take the module reference before creating the thread: once std::jthread returns the new thread may
39 // already be running library code, so the keepalive must exist first.
40 233 const HMODULE self_ref = detail::acquire_module_ref(diagnostics::ModulePinReason::Worker);
41
1/2
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 22 taken 233 times.
233 if (self_ref == nullptr)
42 {
43 throw std::system_error(
44 static_cast<int>(GetLastError()),
45 std::system_category(),
46 "StoppableWorker: acquire_module_ref failed"
47 );
48 }
49
50 try
51 {
52 233 m_thread = std::make_unique<std::jthread>(
53
4/12
✓ Branch 25 → 26 taken 233 times.
✗ Branch 25 → 59 not taken.
✓ Branch 27 → 28 taken 233 times.
✗ Branch 27 → 54 not taken.
✗ Branch 31 → 32 not taken.
✓ Branch 31 → 33 taken 233 times.
✗ Branch 33 → 34 not taken.
✓ Branch 33 → 35 taken 233 times.
✗ Branch 56 → 57 not taken.
✗ Branch 56 → 58 not taken.
✗ Branch 59 → 60 not taken.
✗ Branch 59 → 61 not taken.
466 [fn = std::move(body), label = m_name, state = m_state](const std::stop_token &st)
54 {
55 // First act: mark Running, unless a racing shutdown() already claimed teardown.
56 233 State expected = State::Starting;
57 233 state->compare_exchange_strong(expected, State::Running, std::memory_order_acq_rel);
58 try
59 {
60
2/2
✓ Branch 5 → 6 taken 229 times.
✓ Branch 5 → 11 taken 2 times.
235 fn(st);
61 }
62
2/2
✓ Branch 14 → 15 taken 1 time.
✓ Branch 14 → 20 taken 1 time.
2 catch (const std::exception &e)
63 {
64 // try_log, not error(): a throw from the logger here would escape the thread function
65 // and terminate the process, defeating the very containment these handlers provide.
66 1 (void)log()
67 1 .try_log(LogLevel::Error, "StoppableWorker '{}': unhandled exception: {}", label, e.what());
68 1 }
69 1 catch (...)
70 {
71 1 (void)log()
72 1 .try_log(LogLevel::Error, "StoppableWorker '{}': unknown exception escaped body.", label);
73
1/2
✓ Branch 23 → 8 taken 1 time.
✗ Branch 23 → 24 not taken.
1 }
74 // Last act: mark Exited so a caller can distinguish a self-exited body from a live one,
75 // unless shutdown() already moved the state to Stopping/Stopped.
76 231 State running = State::Running;
77 231 state->compare_exchange_strong(running, State::Exited, std::memory_order_acq_rel);
78 231 }
79 233 );
80
81 #if defined(DMK_ENABLE_TEST_SEAMS)
82
2/2
✓ Branch 35 → 36 taken 4 times.
✓ Branch 35 → 37 taken 229 times.
233 if (auto *seam = detail::g_worker_post_thread_start_seam)
83 {
84
1/2
✗ Branch 36 → 37 not taken.
✓ Branch 36 → 64 taken 4 times.
4 seam();
85 }
86 #endif
87
88 229 m_stop_source = m_thread->get_stop_source();
89 229 m_self_ref = self_ref;
90
2/2
✓ Branch 43 → 44 taken 16 times.
✓ Branch 43 → 47 taken 213 times.
229 if (!detail::WorkerStartLogDeferral::defer_start(m_name, std::source_location::current()))
91 {
92 16 (void)log().try_log(LogLevel::Debug, "StoppableWorker '{}' started.", m_name);
93 }
94 }
95 4 catch (...)
96 {
97 // A throw after the thread was created (the test seam, or any would-be publication step). The thread can be
98 // running: request stop and join it (this runs on the constructing thread, never the worker's own and never
99 // under the loader lock), then release the module reference so acquire/release stay balanced. No thread is
100 // ever left behind, and m_self_ref was not yet set, so nothing is double-released.
101
3/6
✓ Branch 67 → 68 taken 4 times.
✗ Branch 67 → 72 not taken.
✓ Branch 70 → 71 taken 4 times.
✗ Branch 70 → 72 not taken.
✓ Branch 73 → 74 taken 4 times.
✗ Branch 73 → 78 not taken.
4 if (m_thread != nullptr && m_thread->joinable())
102 {
103 4 m_thread->request_stop();
104 try
105 {
106
1/2
✓ Branch 77 → 78 taken 4 times.
✗ Branch 77 → 80 not taken.
4 m_thread->join();
107 }
108 catch (...)
109 {
110 detail::reap_worker_thread(std::move(m_thread), self_ref, diagnostics::ModulePinReason::Worker);
111 throw;
112 }
113 }
114 4 detail::release_module_ref(self_ref, diagnostics::ModulePinReason::Worker);
115 4 throw;
116 4 }
117 16 }
118
119 221 StoppableWorker::~StoppableWorker() noexcept
120 {
121 221 shutdown();
122 221 }
123
124 228146 void StoppableWorker::request_stop() noexcept
125 {
126 // Signal the copied stop_source while the body may still observe it. Stopping/Stopped already requested stop;
127 // Exited has no body left to notify. Reading the shared state never touches m_thread, so this stays race-free
128 // against a concurrent shutdown() join/detach.
129 228146 const State s = m_state->load(std::memory_order_acquire);
130
3/4
✓ Branch 4 → 5 taken 231865 times.
✗ Branch 4 → 6 not taken.
✓ Branch 5 → 6 taken 106160 times.
✓ Branch 5 → 7 taken 125705 times.
227503 if (s == State::Starting || s == State::Running)
131 {
132 101798 m_stop_source.request_stop();
133 }
134 231870 }
135
136 129114 bool StoppableWorker::is_running() const noexcept
137 {
138 129114 const State s = m_state->load(std::memory_order_acquire);
139
3/4
✓ Branch 4 → 5 taken 126477 times.
✓ Branch 4 → 6 taken 3478 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 128732 times.
129955 return s == State::Starting || s == State::Running;
140 }
141
142 431 void StoppableWorker::shutdown() noexcept
143 {
144 // Single-entry: claim teardown by moving to Stopping. A second shutdown (or the destructor after an
145 // explicit shutdown) observes Stopping/Stopped and returns.
146 431 State prev = m_state->load(std::memory_order_acquire);
147 for (;;)
148 {
149
3/4
✓ Branch 5 → 6 taken 431 times.
✗ Branch 5 → 7 not taken.
✓ Branch 6 → 7 taken 204 times.
✓ Branch 6 → 8 taken 227 times.
431 if (prev == State::Stopping || prev == State::Stopped)
150 {
151 210 return;
152 }
153
1/2
✓ Branch 10 → 11 taken 227 times.
✗ Branch 10 → 12 not taken.
227 if (m_state->compare_exchange_weak(prev, State::Stopping, std::memory_order_acq_rel))
154 {
155 227 break;
156 }
157 }
158
159
3/6
✓ Branch 14 → 15 taken 227 times.
✗ Branch 14 → 18 not taken.
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 227 times.
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 24 taken 227 times.
227 if (m_thread == nullptr || !m_thread->joinable())
160 {
161 m_state->store(State::Stopped, std::memory_order_release);
162 return;
163 }
164
165
2/2
✓ Branch 25 → 26 taken 5 times.
✓ Branch 25 → 36 taken 222 times.
227 if (!detail::blocking_teardown_permitted())
166 {
167 // No authorization to block: either a loader callback is in progress or the fail-closed probe vetoed.
168 // Joining a worker that may itself await the loader lock deadlocks, and request_stop() can synchronously
169 // invoke an arbitrary callback that blocks. Detach without signalling and leave the module reference
170 // outstanding so the detached thread's code pages stay mapped.
171 10 std::unique_ptr<std::jthread> retained_thread = std::move(m_thread);
172 try
173 {
174
1/2
✓ Branch 30 → 31 taken 5 times.
✗ Branch 30 → 62 not taken.
5 retained_thread->detach();
175 }
176 catch (...)
177 {
178 // A joinable jthread cannot be destroyed safely after detach failed. Its heap cell and the
179 // module reference remain paired and reachable for the life of the running thread.
180 (void)retained_thread.release();
181 }
182 5 DetourModKit::diagnostics::record_intentional_leak(DetourModKit::diagnostics::LeakSubsystem::Worker);
183 5 m_self_ref = nullptr;
184 5 m_state->store(State::Stopped, std::memory_order_release);
185 5 return;
186 5 }
187
188 222 m_stop_source.request_stop();
189
190
2/2
✓ Branch 41 → 42 taken 1 time.
✓ Branch 41 → 50 taken 221 times.
222 if (m_thread->get_id() == std::this_thread::get_id())
191 {
192 // Self-shutdown reached from inside the body (e.g. a reload setter destroying the servicer whose
193 // worker runs it). A self-join would raise std::system_error out of this noexcept function. Hand
194 // the thread and module reference to the reaper: it joins once the body returns and then releases
195 // the reference, so nothing is permanently leaked.
196 2 detail::reap_worker_thread(std::move(m_thread), m_self_ref, diagnostics::ModulePinReason::Worker);
197 1 m_self_ref = nullptr;
198 1 m_state->store(State::Stopped, std::memory_order_release);
199 1 return;
200 }
201
202 try
203 {
204 #if defined(DMK_ENABLE_TEST_SEAMS)
205
2/2
✓ Branch 50 → 51 taken 2 times.
✓ Branch 50 → 52 taken 219 times.
221 if (auto *seam = detail::g_worker_join_fail_seam)
206 {
207
1/2
✗ Branch 51 → 52 not taken.
✓ Branch 51 → 66 taken 2 times.
2 seam();
208 }
209 #endif
210
1/2
✓ Branch 53 → 54 taken 219 times.
✗ Branch 53 → 66 not taken.
219 m_thread->join();
211 219 m_thread.reset();
212
213 // Joined off the loader lock: the worker's code has finished, so drop the reference taken before
214 // thread creation. Another reference on the module still exists (the caller is executing this
215 // module's code), so this release is never the terminal one that could unmap the module out from
216 // under us.
217 219 detail::release_module_ref(static_cast<HMODULE>(m_self_ref), diagnostics::ModulePinReason::Worker);
218 219 m_self_ref = nullptr;
219 }
220 2 catch (...)
221 {
222 // Contain a join failure (std::system_error) inside this noexcept function. The thread's completion
223 // is now uncertain, so detach it (both to abandon it safely and to stop the eventual ~jthread from
224 // re-attempting the join and re-throwing into a noexcept destructor) and leave the module reference
225 // outstanding rather than release it into a possibly-still-running thread. That leaves a safe,
226 // diagnosed Stopped state.
227 try
228 {
229 #if defined(DMK_ENABLE_TEST_SEAMS)
230
1/2
✓ Branch 68 → 69 taken 2 times.
✗ Branch 68 → 70 not taken.
2 if (auto *seam = detail::g_worker_join_fail_seam)
231 {
232
2/2
✓ Branch 69 → 70 taken 1 time.
✓ Branch 69 → 84 taken 1 time.
2 seam();
233 }
234 #endif
235
3/6
✓ Branch 71 → 72 taken 1 time.
✗ Branch 71 → 76 not taken.
✓ Branch 74 → 75 taken 1 time.
✗ Branch 74 → 76 not taken.
✓ Branch 77 → 78 taken 1 time.
✗ Branch 77 → 80 not taken.
1 if (m_thread != nullptr && m_thread->joinable())
236 {
237
1/2
✓ Branch 79 → 80 taken 1 time.
✗ Branch 79 → 84 not taken.
1 m_thread->detach();
238 }
239 }
240 1 catch (...)
241 {
242 // Retain a still-joinable jthread so its destructor cannot retry the failed operation.
243 1 (void)m_thread.release();
244 1 }
245 2 (void)log().try_log(
246 LogLevel::Error,
247 "StoppableWorker '{}': join failed; abandoning module reference to stay safe.",
248 2 m_name
249 );
250 2 DetourModKit::diagnostics::record_intentional_leak(DetourModKit::diagnostics::LeakSubsystem::Worker);
251 2 m_self_ref = nullptr;
252 2 }
253 221 m_state->store(State::Stopped, std::memory_order_release);
254 }
255 } // namespace DetourModKit
256