include/boost/corosio/native/detail/iocp/win_signals.hpp

98.8% Lines (319 / 323, 1 excl) 77.0% Branches (164 / 213) 100.0% Functions (29 / 29)
win_signals.hpp
f(x) Functions (29)
Function Calls Lines Branches Blocks
boost::corosio::detail::win_signals::destroy(boost::corosio::io_object::implementation*) :171 1693x 100.0% 100.0% 100.0% boost::corosio::detail::win_signals::reset_token_cancel(boost::corosio::detail::win_signal&) :247 102301x 100.0% – 100.0% corosio_signal_handler :315 360x 100.0% – 100.0% boost::corosio::detail::signal_op::signal_op() :330 1689x 100.0% – 100.0% boost::corosio::detail::signal_op::do_complete(void*, boost::corosio::detail::scheduler_op*, unsigned int, unsigned int) :333 362x 80.0% 40.0% 59.4% boost::corosio::detail::win_signal::win_signal(boost::corosio::detail::win_signals&) :374 1689x 100.0% – 100.0% boost::corosio::detail::win_signal::wait(boost::capy::continuation&, boost::capy::executor_ref, std::stop_token, std::error_code*, int*) :377 102362x 100.0% 75.0% 100.0% boost::corosio::detail::win_signal::add(int, boost::corosio::signal_set::flags_t) :425 1701x 100.0% – 100.0% boost::corosio::detail::win_signal::remove(int) :431 15x 100.0% – 100.0% boost::corosio::detail::win_signal::clear() :437 1708x 100.0% – 100.0% boost::corosio::detail::win_signal::cancel() :443 101704x 100.0% – 100.0% boost::corosio::detail::win_signals::win_signals(boost::capy::execution_context&) :452 282x 100.0% 100.0% 61.1% boost::corosio::detail::win_signals::~win_signals() :461 564x 100.0% – 100.0% boost::corosio::detail::win_signals::shutdown() :467 282x 100.0% 55.8% 81.4% boost::corosio::detail::win_signals::shutdown()::{lambda(boost::corosio::detail::win_signal*)#1}::operator()(boost::corosio::detail::win_signal*) const :484 1605x 100.0% 100.0% 100.0% boost::corosio::detail::win_signals::construct() :539 1693x 100.0% – 100.0% boost::corosio::detail::win_signal::retire() :545 1693x 100.0% – 100.0% boost::corosio::detail::win_signals::add_signal(boost::corosio::detail::win_signal&, int, boost::corosio::signal_set::flags_t) :551 1701x 100.0% 92.0% 81.1% boost::corosio::detail::win_signals::remove_signal(boost::corosio::detail::win_signal&, int) :612 15x 100.0% 88.5% 96.8% boost::corosio::detail::win_signals::clear_signals(boost::corosio::detail::win_signal&) :658 1708x 100.0% 90.9% 93.8% boost::corosio::detail::win_signals::cancel_wait(boost::corosio::detail::win_signal&) :699 101704x 100.0% 75.0% 100.0% boost::corosio::detail::win_signals::cancel_wait_token(boost::corosio::detail::win_signal&) :729 335x 100.0% 75.0% 100.0% boost::corosio::detail::win_signal::token_canceller::operator()() const :761 335x 100.0% – 100.0% boost::corosio::detail::win_signals::start_wait(boost::corosio::detail::win_signal&, boost::corosio::detail::signal_op*) :767 102301x 100.0% 78.9% 93.8% boost::corosio::detail::win_signals::deliver_signal(int) :833 360x 100.0% 80.0% 74.3% boost::corosio::detail::win_signals::work_finished() :892 211x 100.0% – 100.0% boost::corosio::detail::win_signals::post(boost::corosio::detail::signal_op*) :898 362x 100.0% – 100.0% boost::corosio::detail::win_signals::add_service(boost::corosio::detail::win_signals*) :904 282x 100.0% 100.0% 100.0% boost::corosio::detail::win_signals::remove_service(boost::corosio::detail::win_signals*) :917 282x 100.0% 92.9% 100.0%
Line Branch TLA Hits Source Code
1 //
2 // Copyright (c) 2025 Vinnie Falco ([email protected])
3 // Copyright (c) 2026 Steve Gerbino
4 // Copyright (c) 2026 Michael Vandeberg
5 //
6 // Distributed under the Boost Software License, Version 1.0. (See accompanying
7 // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
8 //
9 // Official repository: https://github.com/cppalliance/corosio
10 //
11
12 #ifndef BOOST_COROSIO_NATIVE_DETAIL_IOCP_WIN_SIGNALS_HPP
13 #define BOOST_COROSIO_NATIVE_DETAIL_IOCP_WIN_SIGNALS_HPP
14
15 #include <boost/corosio/detail/platform.hpp>
16
17 #if BOOST_COROSIO_HAS_IOCP
18
19 #include <boost/corosio/native/detail/iocp/win_signal.hpp>
20
21 #include <boost/corosio/detail/config.hpp>
22 #include <boost/corosio/detail/object_pool.hpp>
23 #include <boost/corosio/detail/object_ref.hpp>
24 #include <boost/capy/continuation.hpp>
25 #include <boost/capy/ex/abandon.hpp>
26 #include <boost/capy/ex/execution_context.hpp>
27 #include <boost/corosio/native/detail/iocp/win_mutex.hpp>
28 #include <boost/corosio/native/detail/iocp/win_scheduler.hpp>
29 #include <boost/corosio/detail/dispatch_coro.hpp>
30 #include <boost/capy/error.hpp>
31
32 #include <csignal>
33 #include <coroutine>
34 #include <mutex>
35 #include <tuple>
36 #include <utility>
37 #include <vector>
38
39 #include <signal.h>
40
41 /*
42 Windows Signal Implementation - Header
43 ======================================
44
45 This header declares the internal types for Windows signal handling.
46
47 Key Differences from POSIX:
48 - Uses C runtime signal() instead of sigaction() (Windows has no sigaction)
49 - Only `none` and `dont_care` flags are supported; other flags return
50 `operation_not_supported` (Windows has no equivalent to SA_* flags)
51 - Windows resets handler to SIG_DFL after each signal, so we must re-register
52 - Only supports: SIGINT, SIGTERM, SIGABRT, SIGFPE, SIGILL, SIGSEGV
53 - max_signal_number is 32 (vs 64 on Linux)
54
55 The data structures mirror the POSIX implementation for consistency:
56 - signal_op, signal_registration, win_signal, win_signals
57
58 Threading note: Windows signal handling is synchronous (runs on faulting
59 thread), so we can safely acquire locks in the signal handler. This differs
60 from POSIX where the handler must be async-signal-safe.
61 */
62
63 /*
64 Windows Signal Handling Implementation
65 ======================================
66
67 This file implements POSIX-style signal handling on Windows, integrated with
68 the IOCP scheduler. Windows lacks native async signal support, so we use the
69 C standard library's signal() function and manually bridge signals into the
70 completion-based I/O model.
71
72 Architecture Overview
73 ---------------------
74
75 Three layers manage signal registrations:
76
77 1. signal_state (global singleton)
78 - Tracks the global service list and per-signal registration counts
79 - Owns the mutex that protects signal handler installation/removal
80 - Multiple execution_contexts share this; each gets a win_signals entry
81
82 2. win_signals (one per execution_context)
83 - Maintains registrations_[] table indexed by signal number
84 - Each slot is a doubly-linked list of all signal_registrations for that signal
85 - Also owns a recycling pool of all win_signal objects it owns
86
87 3. win_signal (one per signal_set)
88 - Owns a singly-linked list (sorted by signal number) of signal_registrations
89 - Contains the pending_op_ used for wait operations
90
91 The signal_registration struct links these together:
92 - next_in_set / (implicit via sorted order): links registrations within one signal_set
93 - prev_in_table / next_in_table: links registrations for the same signal across sets
94
95 Signal Delivery Flow
96 --------------------
97
98 1. corosio_signal_handler() (C handler, must be async-signal-safe)
99 - Called by the OS when a signal arrives
100 - Delegates to deliver_signal() and re-registers itself (Windows resets to SIG_DFL)
101
102 2. deliver_signal() broadcasts to all win_signals services:
103 - If a signal_set is waiting (impl->waiting_ == true), complete it immediately
104 by posting the signal_op to the scheduler
105 - Otherwise, increment reg->undelivered to queue the signal for later
106
107 3. start_wait() checks for queued signals first:
108 - If undelivered > 0, consume one and post immediate completion
109 - Otherwise, set waiting_ = true and call work_started() to keep context alive
110
111 Locking Protocol
112 ----------------
113
114 Two mutex levels exist (must be acquired in this order to avoid deadlock):
115 1. signal_state::mutex - protects handler registration and service list
116 2. win_signals::mutex_ - protects per-service registration tables and wait state
117
118 deliver_signal() acquires both locks because it iterates the global service list
119 and modifies per-service state.
120
121 Work Tracking
122 -------------
123
124 When waiting for a signal:
125 - start_wait() calls sched_.work_started() to keep io_context::run() alive
126 - signal_op::svc is set to point to the service
127 - signal_op::operator()() calls work_finished() after resuming the coroutine
128
129 If a signal was already queued (undelivered > 0), no work tracking is needed
130 because completion is posted immediately.
131
132 Signal Flags
133 ------------
134
135 Windows only supports `none` and `dont_care` flags. Any other flags
136 (restart, no_child_stop, etc.) return `operation_not_supported`. The
137 C runtime signal() function has no equivalent to sigaction() flags
138 like SA_RESTART or SA_NOCLDSTOP.
139 */
140
141 namespace boost::corosio::detail {
142
143 class win_scheduler;
144
145 /** Windows signal management service.
146
147 This service owns all signal set implementations and coordinates
148 their lifecycle. It provides:
149
150 - Signal implementation allocation and deallocation
151 - Signal registration via the C runtime signal() function
152 - Global signal state management
153 - Graceful shutdown - destroys all implementations when io_context stops
154
155 @par Thread Safety
156 All public member functions are thread-safe.
157
158 @note Only available on Windows platforms.
159 */
160 class BOOST_COROSIO_DECL win_signals final
161 : public capy::execution_context::service
162 , public io_object::io_service
163 {
164 friend class win_signal;
165
166 public:
167 using key_type = win_signals;
168
169 io_object::implementation* construct() override;
170
171 1693x void destroy(io_object::implementation* p) override
172 {
173 1693x auto& impl = static_cast<win_signal&>(*p);
174
1/1
✓ Branch 2 → 3 taken 1693 times.
1693x impl.clear();
175 1693x impl.disarm_stop();
176 1693x impl.cancel();
177 1693x release(&impl);
178 1693x }
179
180 /** Construct the signal service.
181
182 @param ctx Reference to the owning execution_context.
183 */
184 explicit win_signals(capy::execution_context& ctx);
185
186 /** Destroy the signal service. */
187 ~win_signals();
188
189 win_signals(win_signals const&) = delete;
190 win_signals& operator=(win_signals const&) = delete;
191
192 /** Shut down the service.
193
194 Gives every registration its live implementations still hold
195 back to the process-global table. The implementations are
196 freed when their handles release them, or when the service is
197 destroyed if a handle outlives it.
198 */
199 void shutdown() override;
200
201
202 /** Add a signal to a signal set.
203
204 @param impl The signal implementation to modify.
205 @param signal_number The signal to register.
206 @param flags The flags to apply (ignored on Windows).
207 @return Success, or an error.
208 */
209 std::error_code
210 add_signal(win_signal& impl, int signal_number, signal_set::flags_t flags);
211
212 /** Remove a signal from a signal set.
213
214 @param impl The signal implementation to modify.
215 @param signal_number The signal to unregister.
216 @return Success, or an error.
217 */
218 std::error_code remove_signal(win_signal& impl, int signal_number);
219
220 /** Remove all signals from a signal set.
221
222 @param impl The signal implementation to clear.
223 @return Success, or an error.
224 */
225 std::error_code clear_signals(win_signal& impl);
226
227 /** Cancel pending wait operations.
228
229 @param impl The signal implementation to cancel.
230 */
231 void cancel_wait(win_signal& impl) noexcept;
232
233 /** Cancel an in-flight wait on behalf of a stop token.
234
235 Identical to @ref cancel_wait except that it does not set the
236 sticky `cancelled_` latch: a stop token scopes to one operation,
237 so a request arriving after the wait completed must do nothing.
238 */
239 void cancel_wait_token(win_signal& impl) noexcept;
240
241 /** Clear the per-operation stop flag before a new wait arms.
242
243 Lives here rather than on the implementation because the mutex is
244 the service's; the service is a friend of `win_signal`, not the
245 reverse.
246 */
247 102301x void reset_token_cancel(win_signal& impl) noexcept
248 {
249 102301x std::lock_guard<win_mutex> lock(mutex_);
250 102301x impl.token_cancelled_ = false;
251 102301x }
252
253 /** Start a wait operation.
254
255 @param impl The signal implementation.
256 @param op The operation to start.
257 */
258 void start_wait(win_signal& impl, signal_op* op);
259
260 /** Deliver a signal to all registered handlers.
261
262 Called from the signal handler.
263
264 @param signal_number The signal that occurred.
265 */
266 static void deliver_signal(int signal_number);
267
268 /** Notify scheduler of pending work. */
269 void work_started() noexcept;
270
271 /** Notify scheduler that work completed. */
272 void work_finished() noexcept;
273
274 /** Post an operation for completion. */
275 void post(signal_op* op);
276
277 private:
278 static void add_service(win_signals* service);
279 static void remove_service(win_signals* service);
280
281 win_scheduler& sched_;
282 BOOST_COROSIO_MSVC_WARNING_PUSH
283 BOOST_COROSIO_MSVC_WARNING_DISABLE(4251) // detail:: members, dll-interface
284 win_mutex mutex_;
285 object_pool<win_signal> pool_;
286 BOOST_COROSIO_MSVC_WARNING_POP
287
288 // Per-signal registration table for this service
289 signal_registration* registrations_[max_signal_number];
290
291 // Linked list of services for global signal delivery
292 win_signals* next_ = nullptr;
293 win_signals* prev_ = nullptr;
294 };
295
296 //
297 // Global signal state
298 //
299
300 namespace signal_detail {
301
302 struct signal_state
303 {
304 std::mutex mutex;
305 win_signals* service_list = nullptr;
306 std::size_t registration_count[max_signal_number] = {};
307 };
308
309 BOOST_COROSIO_DECL signal_state* get_signal_state();
310
311 // C signal handler. Note: On POSIX this would need to be async-signal-safe,
312 // but Windows signal handling is synchronous (runs on the faulting thread)
313 // so we can safely acquire locks here.
314 extern "C" inline void
315 360x corosio_signal_handler(int signal_number)
316 {
317 360x win_signals::deliver_signal(signal_number);
318
319 // Windows uses "one-shot" semantics: the handler reverts to SIG_DFL
320 // after each delivery. Re-register to maintain our handler.
321 360x ::signal(signal_number, corosio_signal_handler);
322 360x }
323
324 } // namespace signal_detail
325
326 //
327 // signal_op
328 //
329
330 1689x inline signal_op::signal_op() noexcept : scheduler_op(&do_complete) {}
331
332 inline void
333 362x signal_op::do_complete(
334 void* owner,
335 scheduler_op* base,
336 std::uint32_t /*bytes*/,
337 std::uint32_t /*error*/)
338 {
339 362x auto* op = static_cast<signal_op*>(base);
340 // Embedded in win_signal: copied out below before the queued
341 // reference can drop, and nothing touches op afterwards.
342 362x auto keep = std::move(op->keep);
343
344
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 14 taken 362 times.
362x if (!owner)
345 {
346 // The held reference outlives the destroy: the coroutine may
347 // own the set this op is embedded in.
348 ✗ auto* dc = std::exchange(op->cont, nullptr);
349
0/6
✗ Branch 6 → 7 not taken.
✗ Branch 6 → 10 not taken.
✗ Branch 8 → 9 not taken.
✗ Branch 8 → 10 not taken.
✗ Branch 11 → 12 not taken.
✗ Branch 11 → 13 not taken.
✗ if (dc && dc->h)
350 ✗ capy::abandon(*dc);
351 ✗ return;
352 }
353
354
1/2
✓ Branch 14 → 15 taken 362 times.
✗ Branch 14 → 17 not taken.
362x if (op->ec_out)
355 362x *op->ec_out = {};
356
1/2
✓ Branch 17 → 18 taken 362 times.
✗ Branch 17 → 19 not taken.
362x if (op->signal_out)
357 362x *op->signal_out = op->signal_number;
358
359 362x auto* service = op->svc;
360 362x op->svc = nullptr;
361 362x auto* c = op->cont;
362 362x auto ex = op->d;
363
364
1/1
✓ Branch 20 → 21 taken 362 times.
362x dispatch_coro(ex, *c).resume();
365
366
2/2
✓ Branch 21 → 22 taken 211 times.
✓ Branch 21 → 23 taken 151 times.
362x if (service)
367 211x service->work_finished();
368 362x }
369
370 //
371 // win_signal
372 //
373
374 1689x inline win_signal::win_signal(win_signals& svc) noexcept : svc_(svc) {}
375
376 inline std::coroutine_handle<>
377 102362x win_signal::wait(
378 capy::continuation& cont,
379 capy::executor_ref d,
380 std::stop_token token,
381 std::error_code* ec,
382 int* signal_out)
383 {
384 102362x pending_op_.cont = &cont;
385 102362x pending_op_.d = d;
386 102362x pending_op_.ec_out = ec;
387 102362x pending_op_.signal_out = signal_out;
388 102362x pending_op_.signal_number = 0;
389
390 // Disarm any callback left over from a previous wait before doing
391 // anything else, including the early return below: otherwise that
392 // path leaves this object owning a callback it no longer uses.
393 // Outside start_wait's lock on purpose: ~stop_callback blocks until a
394 // concurrently running callback returns, and that callback takes the
395 // service mutex.
396 102362x stop_cb_.reset();
397
398 // Check for immediate cancellation
399
2/2
✓ Branch 4 → 5 taken 61 times.
✓ Branch 4 → 13 taken 102301 times.
102362x if (token.stop_requested())
400 {
401
1/2
✓ Branch 5 → 6 taken 61 times.
✗ Branch 5 → 7 not taken.
61x if (ec)
402 61x *ec = make_error_code(capy::error::canceled);
403
1/2
✓ Branch 7 → 8 taken 61 times.
✗ Branch 7 → 9 not taken.
61x if (signal_out)
404 61x *signal_out = 0;
405 // Posted, never resumed inline: this is the caller's own
406 // suspend, and a loop of such waits would nest frames.
407 61x d.post(*pending_op_.cont);
408 61x return std::noop_coroutine();
409 }
410
411 // Clearing the flag before arming is load-bearing: reset_token_cancel
412 // must run immediately before emplace, not before the early return
413 // above.
414 102301x svc_.reset_token_cancel(*this);
415
2/2
✓ Branch 15 → 16 taken 1929 times.
✓ Branch 15 → 18 taken 100372 times.
102301x if (token.stop_possible())
416 1929x stop_cb_.emplace(token, token_canceller{this});
417
418 102301x svc_.start_wait(*this, &pending_op_);
419 // Every completion -- delivery, cancel() or a stop request -- is
420 // posted, so the waiter never resumes inline from here.
421 102301x return std::noop_coroutine();
422 }
423
424 inline std::error_code
425 1701x win_signal::add(int signal_number, signal_set::flags_t flags)
426 {
427 1701x return svc_.add_signal(*this, signal_number, flags);
428 }
429
430 inline std::error_code
431 15x win_signal::remove(int signal_number)
432 {
433 15x return svc_.remove_signal(*this, signal_number);
434 }
435
436 inline std::error_code
437 1708x win_signal::clear()
438 {
439 1708x return svc_.clear_signals(*this);
440 }
441
442 inline void
443 101704x win_signal::cancel() noexcept
444 {
445 101704x svc_.cancel_wait(*this);
446 101704x }
447
448 //
449 // win_signals
450 //
451
452 282x inline win_signals::win_signals(capy::execution_context& ctx)
453
2/2
✓ Branch 4 → 5 taken 282 times.
✓ Branch 5 → 6 taken 282 times.
282x : sched_(ctx.use_service<win_scheduler>())
454 {
455
2/2
✓ Branch 9 → 8 taken 9024 times.
✓ Branch 9 → 10 taken 282 times.
9306x for (int i = 0; i < max_signal_number; ++i)
456 9024x registrations_[i] = nullptr;
457
458
1/1
✓ Branch 10 → 11 taken 282 times.
282x add_service(this);
459 282x }
460
461 564x inline win_signals::~win_signals()
462 {
463 282x remove_service(this);
464 564x }
465
466 inline void
467 282x win_signals::shutdown()
468 {
469 // Each visited impl is pinned with acquire() so a handle
470 // destroy() racing this walk cannot free it mid-walk. The
471 // matching release() runs after the locks drop: if that handle's
472 // destroy() raced in, this release() is the last one, and
473 // ~win_signal destroys an armed stop_cb_ whose ~stop_callback
474 // waits on a token_canceller that takes mutex_.
475 282x std::vector<win_signal*> doomed;
476 282x std::vector<capy::continuation> parked;
477
478 {
479
1/1
✓ Branch 2 → 3 taken 276 times.
282x signal_detail::signal_state* state = signal_detail::get_signal_state();
480
1/1
✓ Branch 3 → 4 taken 282 times.
282x std::lock_guard<std::mutex> state_lock(state->mutex);
481 282x std::lock_guard<win_mutex> lock(mutex_);
482
483 282x pool_.shutdown(
484
1/1
✓ Branch 5 → 6 taken 282 times.
282x [&](win_signal* impl)
485 {
486 1605x acquire(impl);
487 1605x doomed.push_back(impl);
488 1605x });
489
3/4
✓ Branch 26 → 8 taken 1605 times.
✓ Branch 26 → 27 taken 275 times.
✗ Branch 27 → 8 not taken.
✓ Branch 27 → 28 taken 7 times.
1887x for (auto* impl : doomed)
490 {
491 // The parked wait's coroutine is abandoned once the locks
492 // drop: its unwind can destroy the set, which takes them.
493
1/3
✓ Branch 10 → 11 taken 1605 times.
✗ Branch 10 → 12 not taken.
✗ Branch 10 → 16 not taken.
1605x if (std::exchange(impl->waiting_, false))
494 {
495
1/4
✓ Branch 12 → 13 taken 1605 times.
✗ Branch 12 → 16 not taken.
✗ Branch 13 → 11 not taken.
✗ Branch 13 → 14 not taken.
1605x if (auto* c = std::exchange(impl->pending_op_.cont, nullptr))
496 {
497
1/4
✓ Branch 14 → 15 taken 1605 times.
✗ Branch 14 → 16 not taken.
✗ Branch 15 → 11 not taken.
✗ Branch 15 → 16 not taken.
1605x if (c->h)
498
1/2
✓ Branch 15 → 16 taken 1605 times.
✗ Branch 16 → 11 not taken.
1605x parked.push_back(*c);
499 }
500 }
501
502
2/4
✓ Branch 16 → 17 taken 1605 times.
✓ Branch 16 → 24 taken 1605 times.
✗ Branch 17 → 18 not taken.
✗ Branch 17 → 25 not taken.
3210x while (auto* reg = impl->signals_)
503 {
504 1605x int const signal_number = reg->signal_number;
505
506 // The registration table outlives every io_context, so a set
507 // still registered here has to give its count and handler
508 // back the way clear() would: otherwise the handler stays
509 // installed for a signal no set owns any more. The per-node
510 // table unlink clear() also does is skipped in favour of the
511 // wholesale null-out below.
512
2/4
✓ Branch 17 → 18 taken 204 times.
✓ Branch 17 → 21 taken 1401 times.
✗ Branch 18 → 19 not taken.
✗ Branch 18 → 22 not taken.
1605x if (state->registration_count[signal_number] == 1)
513 204x std::ignore = ::signal(signal_number, SIG_DFL);
514
515 1605x --state->registration_count[signal_number];
516
517 1605x impl->signals_ = reg->next_in_set;
518
1/4
✓ Branch 21 → 22 taken 1605 times.
✗ Branch 21 → 23 not taken.
✗ Branch 22 → 23 not taken.
✗ Branch 22 → 24 not taken.
1605x delete reg;
519 1605x }
520 }
521
522 // Every live registration hung off an implementation this pool
523 // owns, so the whole table goes stale at once and can be dropped
524 // wholesale rather than node by node. It has to be dropped:
525 // deliver_signal() walks this service until the destructor
526 // unlinks it from the global list.
527
4/4
✓ Branch 29 → 28 taken 8800 times.
✓ Branch 29 → 30 taken 275 times.
✓ Branch 30 → 29 taken 224 times.
✓ Branch 30 → 31 taken 7 times.
9306x for (int i = 0; i < max_signal_number; ++i)
528 9024x registrations_[i] = nullptr;
529 282x }
530
531
3/4
✓ Branch 38 → 34 taken 1605 times.
✓ Branch 38 → 39 taken 275 times.
✗ Branch 39 → 35 not taken.
✓ Branch 39 → 40 taken 7 times.
1887x for (auto& c : parked)
532 1605x capy::abandon(c);
533
534
3/4
✓ Branch 45 → 41 taken 1605 times.
✓ Branch 45 → 46 taken 275 times.
✗ Branch 46 → 42 not taken.
✓ Branch 46 → 47 taken 7 times.
1887x for (auto* impl : doomed)
535 1605x release(impl);
536 282x }
537
538 inline io_object::implementation*
539 1693x win_signals::construct()
540 {
541 1693x return pool_.acquire(*this);
542 }
543
544 inline void
545 1693x win_signal::retire() noexcept
546 {
547 1693x svc_.pool_.recycle(this);
548 1693x }
549
550 inline std::error_code
551 1701x win_signals::add_signal(
552 win_signal& impl, int signal_number, signal_set::flags_t flags)
553 {
554
4/4
✓ Branch 2 → 3 taken 1700 times.
✓ Branch 2 → 4 taken 1 time.
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 1699 times.
1701x if (signal_number < 0 || signal_number >= max_signal_number)
555 2x return make_error_code(std::errc::invalid_argument);
556
557 // Windows only supports none and dont_care flags
558 1699x constexpr auto supported = signal_set::none | signal_set::dont_care;
559
2/2
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 1698 times.
1699x if ((flags & ~supported) != signal_set::none)
560 1x return make_error_code(std::errc::operation_not_supported);
561
562
1/1
✓ Branch 9 → 10 taken 1688 times.
1698x signal_detail::signal_state* state = signal_detail::get_signal_state();
563
1/1
✓ Branch 10 → 11 taken 1698 times.
1698x std::lock_guard<std::mutex> state_lock(state->mutex);
564 1698x std::lock_guard<win_mutex> lock(mutex_);
565
566 // Check if already registered in this set
567 1698x signal_registration** insertion_point = &impl.signals_;
568 1698x signal_registration* reg = impl.signals_;
569
4/4
✓ Branch 14 → 15 taken 15 times.
✓ Branch 14 → 16 taken 1697 times.
✓ Branch 15 → 13 taken 14 times.
✓ Branch 15 → 16 taken 1 time.
1712x while (reg && reg->signal_number < signal_number)
570 {
571 14x insertion_point = &reg->next_in_set;
572 14x reg = reg->next_in_set;
573 }
574
575
3/4
✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 19 taken 1697 times.
✓ Branch 17 → 18 taken 1 time.
✗ Branch 17 → 19 not taken.
1698x if (reg && reg->signal_number == signal_number)
576 1x return {}; // Already registered
577
578 // Create new registration
579
1/1
✓ Branch 19 → 20 taken 1697 times.
1697x auto* new_reg = new signal_registration;
580 1697x new_reg->signal_number = signal_number;
581 1697x new_reg->owner = &impl;
582 1697x new_reg->undelivered = 0;
583
584 // Register signal handler if first registration
585
2/2
✓ Branch 21 → 22 taken 287 times.
✓ Branch 21 → 27 taken 1410 times.
1697x if (state->registration_count[signal_number] == 0)
586 {
587
2/2
✓ Branch 23 → 24 taken 2 times.
✓ Branch 23 → 27 taken 285 times.
287x if (::signal(signal_number, signal_detail::corosio_signal_handler) ==
588 SIG_ERR)
589 {
590
1/2
✓ Branch 24 → 25 taken 2 times.
✗ Branch 24 → 26 not taken.
2x delete new_reg;
591 2x return make_error_code(std::errc::invalid_argument);
592 }
593 }
594
595 // Insert into set's registration list (sorted by signal number)
596 1695x new_reg->next_in_set = reg;
597 1695x *insertion_point = new_reg;
598
599 // Insert into service's registration table
600 1695x new_reg->next_in_table = registrations_[signal_number];
601 1695x new_reg->prev_in_table = nullptr;
602
2/2
✓ Branch 27 → 28 taken 1407 times.
✓ Branch 27 → 29 taken 288 times.
1695x if (registrations_[signal_number])
603 1407x registrations_[signal_number]->prev_in_table = new_reg;
604 1695x registrations_[signal_number] = new_reg;
605
606 1695x ++state->registration_count[signal_number];
607
608 1695x return {};
609 1698x }
610
611 inline std::error_code
612 15x win_signals::remove_signal(win_signal& impl, int signal_number)
613 {
614
3/4
✓ Branch 2 → 3 taken 14 times.
✓ Branch 2 → 4 taken 1 time.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 14 times.
15x if (signal_number < 0 || signal_number >= max_signal_number)
615 1x return make_error_code(std::errc::invalid_argument);
616
617
1/1
✓ Branch 5 → 6 taken 10 times.
14x signal_detail::signal_state* state = signal_detail::get_signal_state();
618
1/1
✓ Branch 6 → 7 taken 14 times.
14x std::lock_guard<std::mutex> state_lock(state->mutex);
619 14x std::lock_guard<win_mutex> lock(mutex_);
620
621 // Find the registration in the set
622 14x signal_registration** deletion_point = &impl.signals_;
623 14x signal_registration* reg = impl.signals_;
624
4/4
✓ Branch 10 → 11 taken 13 times.
✓ Branch 10 → 12 taken 2 times.
✓ Branch 11 → 9 taken 1 time.
✓ Branch 11 → 12 taken 12 times.
15x while (reg && reg->signal_number < signal_number)
625 {
626 1x deletion_point = &reg->next_in_set;
627 1x reg = reg->next_in_set;
628 }
629
630
3/4
✓ Branch 12 → 13 taken 12 times.
✓ Branch 12 → 14 taken 2 times.
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 12 times.
14x if (!reg || reg->signal_number != signal_number)
631 2x return {}; // Not found, no-op
632
633 // Restore default handler if last registration
634
2/2
✓ Branch 15 → 16 taken 10 times.
✓ Branch 15 → 19 taken 2 times.
12x if (state->registration_count[signal_number] == 1)
635 {
636
2/2
✓ Branch 17 → 18 taken 2 times.
✓ Branch 17 → 19 taken 8 times.
10x if (::signal(signal_number, SIG_DFL) == SIG_ERR)
637 2x return make_error_code(std::errc::invalid_argument);
638 }
639
640 // Remove from set's list
641 10x *deletion_point = reg->next_in_set;
642
643 // Remove from service's registration table
644
2/2
✓ Branch 19 → 20 taken 9 times.
✓ Branch 19 → 21 taken 1 time.
10x if (registrations_[signal_number] == reg)
645 9x registrations_[signal_number] = reg->next_in_table;
646
2/2
✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 23 taken 9 times.
10x if (reg->prev_in_table)
647 1x reg->prev_in_table->next_in_table = reg->next_in_table;
648
2/2
✓ Branch 23 → 24 taken 1 time.
✓ Branch 23 → 25 taken 9 times.
10x if (reg->next_in_table)
649 1x reg->next_in_table->prev_in_table = reg->prev_in_table;
650
651 10x --state->registration_count[signal_number];
652
653
1/2
✓ Branch 25 → 26 taken 10 times.
✗ Branch 25 → 27 not taken.
10x delete reg;
654 10x return {};
655 14x }
656
657 inline std::error_code
658 1708x win_signals::clear_signals(win_signal& impl)
659 {
660
1/1
✓ Branch 2 → 3 taken 1692 times.
1708x signal_detail::signal_state* state = signal_detail::get_signal_state();
661
1/1
✓ Branch 3 → 4 taken 1708 times.
1708x std::lock_guard<std::mutex> state_lock(state->mutex);
662 1708x std::lock_guard<win_mutex> lock(mutex_);
663
664 1708x std::error_code first_error;
665
666
2/2
✓ Branch 6 → 7 taken 80 times.
✓ Branch 6 → 25 taken 1708 times.
1788x while (signal_registration* reg = impl.signals_)
667 {
668 80x int signal_number = reg->signal_number;
669
670 // Restore default handler if last registration
671
2/2
✓ Branch 7 → 8 taken 73 times.
✓ Branch 7 → 16 taken 7 times.
80x if (state->registration_count[signal_number] == 1)
672 {
673
5/6
✓ Branch 9 → 10 taken 2 times.
✓ Branch 9 → 13 taken 71 times.
✓ Branch 11 → 12 taken 2 times.
✗ Branch 11 → 13 not taken.
✓ Branch 14 → 15 taken 2 times.
✓ Branch 14 → 16 taken 71 times.
73x if (::signal(signal_number, SIG_DFL) == SIG_ERR && !first_error)
674 2x first_error = make_error_code(std::errc::invalid_argument);
675 }
676
677 // Remove from set's list
678 80x impl.signals_ = reg->next_in_set;
679
680 // Remove from service's registration table
681
2/2
✓ Branch 16 → 17 taken 78 times.
✓ Branch 16 → 18 taken 2 times.
80x if (registrations_[signal_number] == reg)
682 78x registrations_[signal_number] = reg->next_in_table;
683
2/2
✓ Branch 18 → 19 taken 2 times.
✓ Branch 18 → 20 taken 78 times.
80x if (reg->prev_in_table)
684 2x reg->prev_in_table->next_in_table = reg->next_in_table;
685
2/2
✓ Branch 20 → 21 taken 3 times.
✓ Branch 20 → 22 taken 77 times.
80x if (reg->next_in_table)
686 3x reg->next_in_table->prev_in_table = reg->prev_in_table;
687
688 80x --state->registration_count[signal_number];
689
690
1/2
✓ Branch 22 → 23 taken 80 times.
✗ Branch 22 → 24 not taken.
80x delete reg;
691 80x }
692
693
2/2
✓ Branch 26 → 27 taken 2 times.
✓ Branch 26 → 28 taken 1706 times.
1708x if (first_error)
694 2x return first_error;
695 1706x return {};
696 1708x }
697
698 inline void
699 101704x win_signals::cancel_wait(win_signal& impl) noexcept
700 {
701 101704x bool was_waiting = false;
702 101704x signal_op* op = nullptr;
703
704 {
705 101704x std::lock_guard<win_mutex> lock(mutex_);
706 101704x impl.cancelled_ = true;
707
2/2
✓ Branch 3 → 4 taken 7 times.
✓ Branch 3 → 5 taken 101697 times.
101704x if (impl.waiting_)
708 {
709 7x was_waiting = true;
710 7x impl.waiting_ = false;
711 7x op = &impl.pending_op_;
712 7x sched_.cancel_posting_begin();
713 }
714 101704x }
715
716
2/2
✓ Branch 6 → 7 taken 7 times.
✓ Branch 6 → 14 taken 101697 times.
101704x if (was_waiting)
717 {
718
1/2
✓ Branch 7 → 8 taken 7 times.
✗ Branch 7 → 9 not taken.
7x if (op->ec_out)
719 7x *op->ec_out = make_error_code(capy::error::canceled);
720
1/2
✓ Branch 9 → 10 taken 7 times.
✗ Branch 9 → 11 not taken.
7x if (op->signal_out)
721 7x *op->signal_out = 0;
722 7x op->d.post(*op->cont);
723 7x sched_.work_finished();
724 7x sched_.cancel_posting_end();
725 }
726 101704x }
727
728 inline void
729 335x win_signals::cancel_wait_token(win_signal& impl) noexcept
730 {
731 335x bool was_waiting = false;
732 335x signal_op* op = nullptr;
733
734 {
735 335x std::lock_guard<win_mutex> lock(mutex_);
736 // Persist the request even when no wait is parked yet; start_wait
737 // consumes this flag.
738 335x impl.token_cancelled_ = true;
739
2/2
✓ Branch 3 → 4 taken 267 times.
✓ Branch 3 → 5 taken 68 times.
335x if (impl.waiting_)
740 {
741 267x was_waiting = true;
742 267x impl.waiting_ = false;
743 267x op = &impl.pending_op_;
744 267x sched_.cancel_posting_begin();
745 }
746 335x }
747
748
2/2
✓ Branch 6 → 7 taken 267 times.
✓ Branch 6 → 14 taken 68 times.
335x if (was_waiting)
749 {
750
1/2
✓ Branch 7 → 8 taken 267 times.
✗ Branch 7 → 9 not taken.
267x if (op->ec_out)
751 267x *op->ec_out = make_error_code(capy::error::canceled);
752
1/2
✓ Branch 9 → 10 taken 267 times.
✗ Branch 9 → 11 not taken.
267x if (op->signal_out)
753 267x *op->signal_out = 0;
754 267x op->d.post(*op->cont);
755 267x sched_.work_finished();
756 267x sched_.cancel_posting_end();
757 }
758 335x }
759
760 inline void
761 335x win_signal::token_canceller::operator()() const noexcept
762 {
763 335x self->svc_.cancel_wait_token(*self);
764 335x }
765
766 inline void
767 102301x win_signals::start_wait(win_signal& impl, signal_op* op)
768 {
769 102301x bool was_cancelled = false;
770
771 {
772 102301x std::lock_guard<win_mutex> lock(mutex_);
773
774 // Check if cancel() was called before this wait started
775
2/2
✓ Branch 3 → 4 taken 100001 times.
✓ Branch 3 → 8 taken 2300 times.
102301x if (impl.cancelled_)
776 {
777 100001x was_cancelled = true;
778 100001x impl.cancelled_ = false;
779
1/2
✓ Branch 4 → 5 taken 100001 times.
✗ Branch 4 → 6 not taken.
100001x if (op->ec_out)
780 100001x *op->ec_out = make_error_code(capy::error::canceled);
781
1/2
✓ Branch 6 → 7 taken 100001 times.
✗ Branch 6 → 23 not taken.
100001x if (op->signal_out)
782 100001x *op->signal_out = 0;
783 }
784
2/2
✓ Branch 8 → 9 taken 59 times.
✓ Branch 8 → 13 taken 2241 times.
2300x else if (impl.token_cancelled_)
785 {
786 // A stop request that arrived between wait() arming the callback
787 // and this lock: complete now rather than parking forever. Filled
788 // in under the lock and dispatched below, outside it, like the
789 // cancelled_ branch above -- see the comment after this block.
790 59x was_cancelled = true;
791 59x impl.token_cancelled_ = false;
792
1/2
✓ Branch 9 → 10 taken 59 times.
✗ Branch 9 → 11 not taken.
59x if (op->ec_out)
793 59x *op->ec_out = make_error_code(capy::error::canceled);
794
1/2
✓ Branch 11 → 12 taken 59 times.
✗ Branch 11 → 23 not taken.
59x if (op->signal_out)
795 59x *op->signal_out = 0;
796 }
797 else
798 {
799 // Check for queued signals first
800 2241x signal_registration* reg = impl.signals_;
801
2/2
✓ Branch 21 → 14 taken 2244 times.
✓ Branch 21 → 22 taken 2090 times.
4334x while (reg)
802 {
803
2/2
✓ Branch 14 → 15 taken 151 times.
✓ Branch 14 → 20 taken 2093 times.
2244x if (reg->undelivered > 0)
804 {
805 151x --reg->undelivered;
806 151x op->signal_number = reg->signal_number;
807 151x op->svc = nullptr; // No extra work_finished needed
808 // The caller's wait() holds the handle's reference.
809 151x op->keep = detail::object_ref(&impl);
810 // Post for immediate completion - post() handles work tracking
811
1/1
✓ Branch 18 → 19 taken 151 times.
151x post(op);
812 151x return;
813 }
814 2093x reg = reg->next_in_set;
815 }
816
817 // No queued signals, wait for delivery
818 // We call work_started() to keep io_context alive while waiting.
819 // Set svc so signal_op::operator() will call work_finished().
820 2090x impl.waiting_ = true;
821 2090x op->svc = this;
822 2090x sched_.work_started();
823 }
824 102301x }
825
826 // Posted outside the lock: this runs inside the caller's own
827 // suspend, so resuming it here would nest frames.
828
2/2
✓ Branch 27 → 29 taken 100060 times.
✓ Branch 27 → 30 taken 2090 times.
102150x if (was_cancelled)
829 100060x op->d.post(*op->cont);
830 }
831
832 inline void
833 360x win_signals::deliver_signal(int signal_number)
834 {
835
2/4
✓ Branch 2 → 3 taken 360 times.
✗ Branch 2 → 4 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 360 times.
360x if (signal_number < 0 ||
836 signal_number >=
837 max_signal_number) // LCOV_EXCL_LINE OS never delivers out-of-range
838 − return; // LCOV_EXCL_LINE OS never delivers out-of-range
839
840
1/1
✓ Branch 5 → 6 taken 360 times.
360x signal_detail::signal_state* state = signal_detail::get_signal_state();
841
1/1
✓ Branch 6 → 7 taken 360 times.
360x std::lock_guard<std::mutex> lock(state->mutex);
842
843 // Deliver to all services. We hold state->mutex while iterating, and
844 // acquire each service's mutex_ inside (matching the lock order used by
845 // add_signal/remove_signal) to safely read and modify registration state.
846 360x win_signals* service = state->service_list;
847
2/2
✓ Branch 24 → 8 taken 360 times.
✓ Branch 24 → 25 taken 360 times.
720x while (service)
848 {
849 360x std::lock_guard<win_mutex> svc_lock(service->mutex_);
850
851 // Find registrations for this signal
852 360x signal_registration* reg = service->registrations_[signal_number];
853
2/2
✓ Branch 21 → 10 taken 362 times.
✓ Branch 21 → 22 taken 360 times.
722x while (reg)
854 {
855 362x win_signal* impl = reg->owner;
856
857
2/2
✓ Branch 10 → 11 taken 211 times.
✓ Branch 10 → 19 taken 151 times.
362x if (impl->waiting_)
858 {
859 // Reached through the registry, not a held reference.
860 // A set whose count reached zero is being destroyed,
861 // and that destruction completes its wait itself.
862 211x auto keep = detail::object_ref::try_from(impl);
863
1/2
✓ Branch 13 → 14 taken 211 times.
✗ Branch 13 → 17 not taken.
211x if (keep)
864 {
865 211x impl->waiting_ = false;
866 211x impl->pending_op_.signal_number = signal_number;
867 211x impl->pending_op_.keep = std::move(keep);
868
1/1
✓ Branch 16 → 17 taken 211 times.
211x service->post(&impl->pending_op_);
869 }
870 211x }
871 else
872 {
873 // No waiter yet; increment undelivered so start_wait() will
874 // find this signal immediately without blocking
875 151x ++reg->undelivered;
876 }
877
878 362x reg = reg->next_in_table;
879 }
880
881 360x service = service->next_;
882 360x }
883 360x }
884
885 inline void
886 win_signals::work_started() noexcept
887 {
888 sched_.work_started();
889 }
890
891 inline void
892 211x win_signals::work_finished() noexcept
893 {
894 211x sched_.work_finished();
895 211x }
896
897 inline void
898 362x win_signals::post(signal_op* op)
899 {
900 362x sched_.post(op);
901 362x }
902
903 inline void
904 282x win_signals::add_service(win_signals* service)
905 {
906 282x signal_detail::signal_state* state = signal_detail::get_signal_state();
907
1/1
✓ Branch 3 → 4 taken 282 times.
282x std::lock_guard<std::mutex> lock(state->mutex);
908
909 282x service->next_ = state->service_list;
910 282x service->prev_ = nullptr;
911
2/2
✓ Branch 4 → 5 taken 4 times.
✓ Branch 4 → 6 taken 278 times.
282x if (state->service_list)
912 4x state->service_list->prev_ = service;
913 282x state->service_list = service;
914 282x }
915
916 inline void
917 282x win_signals::remove_service(win_signals* service)
918 {
919
1/1
✓ Branch 2 → 3 taken 276 times.
282x signal_detail::signal_state* state = signal_detail::get_signal_state();
920
1/1
✓ Branch 3 → 4 taken 282 times.
282x std::lock_guard<std::mutex> lock(state->mutex);
921
922
5/6
✓ Branch 4 → 5 taken 279 times.
✓ Branch 4 → 7 taken 3 times.
✓ Branch 5 → 6 taken 278 times.
✓ Branch 5 → 7 taken 1 time.
✓ Branch 6 → 7 taken 278 times.
✗ Branch 6 → 14 not taken.
282x if (service->next_ || service->prev_ || state->service_list == service)
923 {
924
2/2
✓ Branch 7 → 8 taken 281 times.
✓ Branch 7 → 9 taken 1 time.
282x if (state->service_list == service)
925 281x state->service_list = service->next_;
926
2/2
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 11 taken 281 times.
282x if (service->prev_)
927 1x service->prev_->next_ = service->next_;
928
2/2
✓ Branch 11 → 12 taken 3 times.
✓ Branch 11 → 13 taken 279 times.
282x if (service->next_)
929 3x service->next_->prev_ = service->prev_;
930 282x service->next_ = nullptr;
931 282x service->prev_ = nullptr;
932 }
933 282x }
934
935 } // namespace boost::corosio::detail
936
937 #endif // BOOST_COROSIO_HAS_IOCP
938
939 #endif // BOOST_COROSIO_NATIVE_DETAIL_IOCP_WIN_SIGNALS_HPP
940