include/boost/corosio/native/detail/posix/posix_signal_service.hpp

90.3% Lines (326/361) 96.9% List of functions (31/32) 64.5% Branches (129/200)
posix_signal_service.hpp
f(x) Functions (32)
Function Calls Lines Branches Blocks
boost::corosio::detail::posix_signal_service::destroy(boost::corosio::io_object::implementation*) :166 112x 100.0% 100.0% boost::corosio::detail::posix_signal_detail::flags_supported(boost::corosio::signal_set::flags_t) :270 114x 100.0% 100.0% boost::corosio::detail::posix_signal_detail::to_sigaction_flags(boost::corosio::signal_set::flags_t) :282 96x 76.9% 70.0% 72.0% boost::corosio::detail::posix_signal_detail::flags_compatible(boost::corosio::signal_set::flags_t, boost::corosio::signal_set::flags_t) :302 18x 100.0% 100.0% 100.0% boost::corosio::detail::posix_signal_detail::open_signal_pipe(boost::corosio::detail::posix_signal_detail::signal_state*) :320 114x 76.5% 66.7% 85.0% boost::corosio::detail::posix_signal_detail::corosio_posix_signal_handler(int) :353 300x 100.0% 100.0% boost::corosio::detail::posix_signal_detail::drain_signal_pipe() :369 300x 100.0% 100.0% 100.0% boost::corosio::detail::signal_op::operator()() :385 302x 100.0% 50.0% 100.0% boost::corosio::detail::signal_op::destroy() :405 0 0.0% 0.0% boost::corosio::detail::posix_signal::posix_signal(boost::corosio::detail::posix_signal_service&) :412 224x 100.0% 100.0% boost::corosio::detail::posix_signal::wait(std::__1::coroutine_handle<void>, boost::capy::executor_ref, std::__1::stop_token, std::__1::error_code*, int*) :418 310x 100.0% 66.7% 100.0% boost::corosio::detail::posix_signal::add(int, boost::corosio::signal_set::flags_t) :449 118x 100.0% 100.0% boost::corosio::detail::posix_signal::remove(int) :455 6x 100.0% 100.0% boost::corosio::detail::posix_signal::clear() :461 116x 100.0% 100.0% boost::corosio::detail::posix_signal::cancel() :467 126x 100.0% 100.0% boost::corosio::detail::posix_signal_service::posix_signal_service(boost::capy::execution_context&, boost::corosio::detail::scheduler&) :474 2662x 100.0% 75.0% 100.0% boost::corosio::detail::posix_signal_service::~posix_signal_service() :486 3993x 100.0% 50.0% 100.0% boost::corosio::detail::posix_signal_service::shutdown() :492 1331x 40.0% 12.5% 25.0% boost::corosio::detail::posix_signal_service::construct() :509 112x 100.0% 100.0% boost::corosio::detail::posix_signal_service::destroy_impl(boost::corosio::detail::posix_signal&) :522 112x 100.0% 50.0% 100.0% boost::corosio::detail::posix_signal_service::add_signal(boost::corosio::detail::posix_signal&, int, boost::corosio::signal_set::flags_t) :533 118x 81.8% 70.0% 76.0% boost::corosio::detail::posix_signal_service::add_signal(boost::corosio::detail::posix_signal&, int, boost::corosio::signal_set::flags_t)::'lambda'()::operator()() const :560 82x 95.6% 73.5% 100.0% boost::corosio::detail::posix_signal_service::remove_signal(boost::corosio::detail::posix_signal&, int) :634 6x 85.7% 60.7% 75.0% boost::corosio::detail::posix_signal_service::clear_signals(boost::corosio::detail::posix_signal&) :687 116x 90.3% 59.1% 69.0% boost::corosio::detail::posix_signal_service::cancel_wait(boost::corosio::detail::posix_signal&) :735 126x 100.0% 75.0% 100.0% boost::corosio::detail::posix_signal_service::start_wait(boost::corosio::detail::posix_signal&, boost::corosio::detail::signal_op*) :764 308x 80.0% 57.1% 70.0% boost::corosio::detail::posix_signal_service::deliver_signal(int) :807 300x 90.9% 64.3% 70.0% boost::corosio::detail::posix_signal_service::work_finished() :851 302x 100.0% 100.0% boost::corosio::detail::posix_signal_service::post(boost::corosio::detail::signal_op*) :857 302x 100.0% 100.0% boost::corosio::detail::posix_signal_service::add_service(boost::corosio::detail::posix_signal_service*) :863 1331x 100.0% 100.0% 100.0% boost::corosio::detail::posix_signal_service::remove_service(boost::corosio::detail::posix_signal_service*) :877 1331x 93.3% 66.7% 90.0% boost::corosio::detail::get_signal_service(boost::capy::execution_context&, boost::corosio::detail::scheduler&) :899 1331x 100.0% 100.0%
Line Branch TLA Hits Source Code
1 //
2 // Copyright (c) 2026 Steve Gerbino
3 // Copyright (c) 2026 Michael Vandeberg
4 //
5 // Distributed under the Boost Software License, Version 1.0. (See accompanying
6 // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
7 //
8 // Official repository: https://github.com/cppalliance/corosio
9 //
10
11 #ifndef BOOST_COROSIO_NATIVE_DETAIL_POSIX_POSIX_SIGNAL_SERVICE_HPP
12 #define BOOST_COROSIO_NATIVE_DETAIL_POSIX_POSIX_SIGNAL_SERVICE_HPP
13
14 #include <boost/corosio/detail/platform.hpp>
15
16 #if BOOST_COROSIO_POSIX
17
18 #include <boost/corosio/native/detail/posix/posix_signal.hpp>
19
20 #include <boost/corosio/detail/config.hpp>
21 #include <boost/capy/ex/execution_context.hpp>
22 #include <boost/corosio/detail/scheduler.hpp>
23 #include <boost/capy/error.hpp>
24
25 #include <mutex>
26
27 #include <errno.h>
28 #include <fcntl.h>
29 #include <signal.h>
30 #include <unistd.h>
31
32 /*
33 POSIX Signal Service
34 ====================
35
36 Concrete signal service implementation for POSIX backends. Manages signal
37 registrations via sigaction() and dispatches completions through the
38 scheduler. One instance per execution_context, created by
39 get_signal_service().
40
41 See the block comment further down for the full architecture overview.
42 */
43
44 /*
45 POSIX Signal Implementation
46 ===========================
47
48 This file implements signal handling for POSIX systems using sigaction().
49 The implementation supports signal flags (SA_RESTART, etc.) and integrates
50 with any POSIX-compatible scheduler via the abstract scheduler interface.
51
52 Architecture Overview
53 ---------------------
54
55 Three layers manage signal registrations:
56
57 1. signal_state (global singleton)
58 - Tracks the global service list and per-signal registration counts
59 - Stores the flags used for first registration of each signal (for
60 conflict detection when multiple signal_sets register same signal)
61 - Owns the mutex that protects signal handler installation/removal
62
63 2. posix_signal_service (one per execution_context)
64 - Maintains registrations_[] table indexed by signal number
65 - Each slot is a doubly-linked list of signal_registrations for that signal
66 - Also maintains impl_list_ of all posix_signal objects it owns
67
68 3. posix_signal (one per signal_set)
69 - Owns a singly-linked list (sorted by signal number) of signal_registrations
70 - Contains the pending_op_ used for wait operations
71
72 Signal Delivery Flow
73 --------------------
74
75 Delivery uses the self-pipe trick so the signal handler itself performs
76 only async-signal-safe work (mirrors Boost.Asio):
77
78 1. Signal arrives -> corosio_posix_signal_handler(). The handler only
79 write()s the signal number to the global self-pipe (write_fd) and
80 restores errno. No locks, no allocation, no scheduler dispatch.
81
82 2. The read end of the pipe is watched by one backend's event loop
83 (registered via scheduler::register_signal_reader on the first
84 registration). When it becomes readable the backend drains it
85 (drain_signal_pipe) and calls deliver_signal() in normal context.
86
87 3. deliver_signal() iterates all posix_signal_service services:
88 - If a signal_set is waiting (impl->waiting_ == true), post the signal_op
89 to the scheduler for immediate completion
90 - Otherwise, increment reg->undelivered to queue the signal
91
92 4. When wait() is called via start_wait():
93 - First check for queued signals (undelivered > 0); if found, post
94 immediate completion without blocking
95 - Otherwise, set waiting_ = true and call work_started() to keep
96 the io_context alive
97
98 Locking Protocol
99 ----------------
100
101 Two mutex levels exist (MUST acquire in this order to avoid deadlock):
102 1. signal_state::mutex - protects handler registration and service list
103 2. posix_signal_service::mutex_ - protects per-service registration tables
104
105 Async-Signal-Safety
106 -------------------
107
108 The C signal handler (corosio_posix_signal_handler) performs only
109 async-signal-safe operations: it reads the single global write_fd and
110 calls write(), saving/restoring errno. It never locks a mutex, allocates
111 memory, or dispatches through the scheduler. All of that happens in
112 deliver_signal(), which runs in normal thread context from the backend
113 event loop after draining the self-pipe. There is therefore no
114 self-deadlock risk if a signal arrives while a thread holds state->mutex
115 or service->mutex_.
116
117 Flag Handling
118 -------------
119
120 - Flags are abstract values in the public API (signal_set::flags_t)
121 - flags_supported() validates that requested flags are available on
122 this platform; returns false if SA_NOCLDWAIT is unavailable and
123 no_child_wait is requested
124 - to_sigaction_flags() maps validated flags to actual SA_* constants
125 - First registration of a signal establishes the flags; subsequent
126 registrations must be compatible (same flags or dont_care)
127 - Requesting unavailable flags returns operation_not_supported
128
129 Work Tracking
130 -------------
131
132 When waiting for a signal:
133 - start_wait() calls sched_->work_started() to prevent io_context::run()
134 from returning while we wait
135 - signal_op::svc is set to point to the service
136 - signal_op::operator()() calls work_finished() after resuming the coroutine
137
138 If a signal was already queued (undelivered > 0), no work tracking is needed
139 because completion is posted immediately.
140 */
141
142 namespace boost::corosio {
143
144 namespace detail {
145
146 /** Signal service for POSIX backends.
147
148 Manages signal registrations via sigaction() and dispatches signal
149 completions through the scheduler. One instance per execution_context.
150 */
151 class BOOST_COROSIO_DECL posix_signal_service final
152 : public capy::execution_context::service
153 , public io_object::io_service
154 {
155 public:
156 using key_type = posix_signal_service;
157
158 posix_signal_service(capy::execution_context& ctx, scheduler& sched);
159 ~posix_signal_service() override;
160
161 posix_signal_service(posix_signal_service const&) = delete;
162 posix_signal_service& operator=(posix_signal_service const&) = delete;
163
164 io_object::implementation* construct() override;
165
166 112x void destroy(io_object::implementation* p) override
167 {
168 112x auto& impl = static_cast<posix_signal&>(*p);
169 112x [[maybe_unused]] auto n = impl.clear();
170 112x impl.cancel();
171 112x destroy_impl(impl);
172 112x }
173
174 void shutdown() override;
175
176 void destroy_impl(posix_signal& impl);
177
178 std::error_code add_signal(
179 posix_signal& impl, int signal_number, signal_set::flags_t flags);
180
181 std::error_code remove_signal(posix_signal& impl, int signal_number);
182
183 std::error_code clear_signals(posix_signal& impl);
184
185 void cancel_wait(posix_signal& impl);
186 void start_wait(posix_signal& impl, signal_op* op);
187
188 static void deliver_signal(int signal_number);
189
190 void work_started() noexcept;
191 void work_finished() noexcept;
192 void post(signal_op* op);
193
194 private:
195 static void add_service(posix_signal_service* service);
196 static void remove_service(posix_signal_service* service);
197
198 scheduler* sched_;
199 std::mutex mutex_;
200
201 // Registers the signal self-pipe's read end with sched_ exactly once per
202 // service, so every io_context that waits on a signal can drain the pipe.
203 // A once_flag (not a bool under mutex_) because registration must run
204 // without holding mutex_ or the signal-state mutex — see add_signal.
205 std::once_flag reader_once_;
206
207 intrusive_list<posix_signal> impl_list_;
208
209 // Per-signal registration table
210 signal_registration* registrations_[max_signal_number];
211
212 // Registration counts for each signal
213 std::size_t registration_count_[max_signal_number];
214
215 // Linked list of all posix_signal_service services for signal delivery
216 1331x posix_signal_service* next_ = nullptr;
217 1331x posix_signal_service* prev_ = nullptr;
218 };
219
220 /** Get or create the signal service for the given context.
221
222 This function is called by the concrete scheduler during initialization
223 to create the signal service with a reference to itself.
224
225 @param ctx Reference to the owning execution_context.
226 @param sched Reference to the scheduler for posting completions.
227 @return Reference to the signal service.
228 */
229 posix_signal_service&
230 get_signal_service(capy::execution_context& ctx, scheduler& sched);
231
232 } // namespace detail
233
234 } // namespace boost::corosio
235
236 // ---------------------------------------------------------------------------
237 // Inline implementation
238 // ---------------------------------------------------------------------------
239
240 namespace boost::corosio {
241
242 namespace detail {
243
244 namespace posix_signal_detail {
245
246 struct signal_state
247 {
248 std::mutex mutex;
249 posix_signal_service* service_list = nullptr;
250 std::size_t registration_count[max_signal_number] = {};
251 signal_set::flags_t registered_flags[max_signal_number] = {};
252
253 // Self-pipe used to defer signal delivery out of handler context.
254 // The C handler writes the signal number to write_fd (async-signal-
255 // safe); a backend event loop drains read_fd and calls deliver_signal()
256 // in normal context. Created once (on the first signal registration) and
257 // kept for the process lifetime. Each posix_signal_service registers the
258 // read end with its own scheduler (see reader_once_) so every running
259 // io_context can drain it; multiple readers on one pipe are safe because
260 // each signal is a fixed sizeof(int) record read atomically.
261 int read_fd = -1;
262 int write_fd = -1;
263 };
264
265 BOOST_COROSIO_DECL signal_state* get_signal_state();
266
267 // Check if requested flags are supported on this platform.
268 // Returns true if all flags are supported, false otherwise.
269 inline bool
270 114x flags_supported([[maybe_unused]] signal_set::flags_t flags)
271 {
272 #ifndef SA_NOCLDWAIT
273 if (flags & signal_set::no_child_wait)
274 return false;
275 #endif
276 114x return true;
277 }
278
279 // Map abstract flags to sigaction() flags.
280 // Caller must ensure flags_supported() returns true first.
281 inline int
282 96x to_sigaction_flags(signal_set::flags_t flags)
283 {
284 96x int sa_flags = 0;
285
2/2
✓ Branch 0 taken 78 times.
✓ Branch 1 taken 18 times.
96x if (flags & signal_set::restart)
286 18x sa_flags |= SA_RESTART;
287
1/2
✓ Branch 0 taken 96 times.
✗ Branch 1 not taken.
96x if (flags & signal_set::no_child_stop)
288 sa_flags |= SA_NOCLDSTOP;
289 #ifdef SA_NOCLDWAIT
290
1/2
✓ Branch 0 taken 96 times.
✗ Branch 1 not taken.
96x if (flags & signal_set::no_child_wait)
291 sa_flags |= SA_NOCLDWAIT;
292 #endif
293
2/2
✓ Branch 0 taken 94 times.
✓ Branch 1 taken 2 times.
96x if (flags & signal_set::no_defer)
294 2x sa_flags |= SA_NODEFER;
295
1/2
✓ Branch 0 taken 96 times.
✗ Branch 1 not taken.
96x if (flags & signal_set::reset_handler)
296 sa_flags |= SA_RESETHAND;
297 96x return sa_flags;
298 }
299
300 // Check if two flag values are compatible
301 inline bool
302 18x flags_compatible(signal_set::flags_t existing, signal_set::flags_t requested)
303 {
304 // dont_care is always compatible
305
4/4
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 12 times.
18x if ((existing & signal_set::dont_care) ||
306 16x (requested & signal_set::dont_care))
307 6x return true;
308
309 // Mask out dont_care bit for comparison
310 12x constexpr auto mask = ~signal_set::dont_care;
311 12x return (existing & mask) == (requested & mask);
312 18x }
313
314 // Lazily create the global signal self-pipe. Idempotent; call under
315 // state->mutex before installing the first signal handler so write_fd is
316 // valid by the time the handler can fire. Both ends are non-blocking and
317 // close-on-exec (mirrors the reactor self-pipe setup in select_scheduler).
318 // Returns false and leaves the fds at -1 if creation fails.
319 inline bool
320 114x open_signal_pipe(signal_state* state)
321 {
322
2/2
✓ Branch 0 taken 110 times.
✓ Branch 1 taken 4 times.
114x if (state->read_fd >= 0)
323 110x return true;
324
325 int fds[2];
326
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4x if (::pipe(fds) < 0)
327 return false;
328
329
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 4 times.
12x for (int i = 0; i < 2; ++i)
330 {
331 8x int fl = ::fcntl(fds[i], F_GETFL, 0);
332
3/6
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 8 times.
8x if (fl == -1 || ::fcntl(fds[i], F_SETFL, fl | O_NONBLOCK) == -1 ||
333 8x ::fcntl(fds[i], F_SETFD, FD_CLOEXEC) == -1)
334 {
335 ::close(fds[0]);
336 ::close(fds[1]);
337 return false;
338 }
339 8x }
340
341 4x state->read_fd = fds[0];
342 4x state->write_fd = fds[1];
343 4x return true;
344 114x }
345
346 // C signal handler. Async-signal-safe: it touches only the single global
347 // write_fd (an int set before any handler is installed) and calls write(),
348 // which POSIX lists as async-signal-safe. errno is saved and restored so an
349 // interrupted foreground syscall is unaffected. A full pipe (write returns
350 // EAGAIN) or a short write is intentionally dropped — the reactor still
351 // coalesces because deliver_signal reports the signal to every waiting set.
352 inline void
353 300x corosio_posix_signal_handler(int signal_number)
354 {
355 300x int saved_errno = errno;
356 300x signal_state* state = get_signal_state();
357 300x [[maybe_unused]] ssize_t r =
358 300x ::write(state->write_fd, &signal_number, sizeof(int));
359 300x errno = saved_errno;
360 // With sigaction(), the handler persists automatically (unlike some
361 // signal() implementations that reset to SIG_DFL).
362 300x }
363
364 // Drain the signal self-pipe and deliver each pending signal. Runs in normal
365 // thread context from the backend event loop, so deliver_signal()'s mutex
366 // locking and scheduler post are safe here. Reads until EAGAIN (edge-
367 // triggered backends require a full drain per readiness event).
368 inline void
369 300x drain_signal_pipe()
370 {
371 300x signal_state* state = get_signal_state();
372 int signal_number;
373
2/2
✓ Branch 0 taken 300 times.
✓ Branch 1 taken 300 times.
600x while (::read(state->read_fd, &signal_number, sizeof(int)) ==
374 static_cast<ssize_t>(sizeof(int)))
375 {
376 300x posix_signal_service::deliver_signal(signal_number);
377 }
378 300x }
379
380 } // namespace posix_signal_detail
381
382 // signal_op implementation
383
384 inline void
385 302x signal_op::operator()()
386 {
387
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 302 times.
302x if (ec_out)
388 302x *ec_out = {};
389
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 302 times.
302x if (signal_out)
390 302x *signal_out = signal_number;
391
392 // Capture svc before resuming (coro may destroy us)
393 302x auto* service = svc;
394 302x svc = nullptr;
395
396 302x cont.h = h;
397 302x d.post(cont);
398
399 // Balance the work_started() from start_wait
400
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 302 times.
302x if (service)
401 302x service->work_finished();
402 302x }
403
404 inline void
405 signal_op::destroy()
406 {
407 // No-op: signal_op is embedded in posix_signal
408 }
409
410 // posix_signal implementation
411
412 336x inline posix_signal::posix_signal(posix_signal_service& svc) noexcept
413 112x : svc_(svc)
414 336x {
415 112x }
416
417 inline std::coroutine_handle<>
418 310x posix_signal::wait(
419 std::coroutine_handle<> h,
420 capy::executor_ref d,
421 std::stop_token token,
422 std::error_code* ec,
423 int* signal_out)
424 {
425 310x pending_op_.h = h;
426 310x pending_op_.d = d;
427 310x pending_op_.ec_out = ec;
428 310x pending_op_.signal_out = signal_out;
429 310x pending_op_.signal_number = 0;
430
431
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 308 times.
310x if (token.stop_requested())
432 {
433
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2x if (ec)
434 2x *ec = make_error_code(capy::error::canceled);
435
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2x if (signal_out)
436 2x *signal_out = 0;
437 2x pending_op_.cont.h = h;
438 2x d.post(pending_op_.cont);
439 // completion is always posted to scheduler queue, never inline.
440 2x return std::noop_coroutine();
441 }
442
443 308x svc_.start_wait(*this, &pending_op_);
444 // completion is always posted to scheduler queue, never inline.
445 308x return std::noop_coroutine();
446 310x }
447
448 inline std::error_code
449 118x posix_signal::add(int signal_number, signal_set::flags_t flags)
450 {
451 118x return svc_.add_signal(*this, signal_number, flags);
452 }
453
454 inline std::error_code
455 6x posix_signal::remove(int signal_number)
456 {
457 6x return svc_.remove_signal(*this, signal_number);
458 }
459
460 inline std::error_code
461 116x posix_signal::clear()
462 {
463 116x return svc_.clear_signals(*this);
464 }
465
466 inline void
467 126x posix_signal::cancel()
468 {
469 126x svc_.cancel_wait(*this);
470 126x }
471
472 // posix_signal_service implementation
473
474 5324x inline posix_signal_service::posix_signal_service(
475 capy::execution_context&, scheduler& sched)
476 1331x : sched_(&sched)
477 3993x {
478
2/2
✓ Branch 0 taken 85184 times.
✓ Branch 1 taken 1331 times.
86515x for (int i = 0; i < max_signal_number; ++i)
479 {
480 85184x registrations_[i] = nullptr;
481 85184x registration_count_[i] = 0;
482 85184x }
483
1/2
✓ Branch 0 taken 1331 times.
✗ Branch 1 not taken.
1331x add_service(this);
484 2662x }
485
486 3993x inline posix_signal_service::~posix_signal_service()
487 2662x {
488
1/2
✓ Branch 0 taken 1331 times.
✗ Branch 1 not taken.
1331x remove_service(this);
489 3993x }
490
491 inline void
492 1331x posix_signal_service::shutdown()
493 {
494 1331x std::lock_guard lock(mutex_);
495
496
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1331 times.
1331x for (auto* impl = impl_list_.pop_front(); impl != nullptr;
497 impl = impl_list_.pop_front())
498 {
499 while (auto* reg = impl->signals_)
500 {
501 impl->signals_ = reg->next_in_set;
502 delete reg;
503 }
504 delete impl;
505 }
506 1331x }
507
508 inline io_object::implementation*
509 112x posix_signal_service::construct()
510 {
511 112x auto* impl = new posix_signal(*this);
512
513 {
514 112x std::lock_guard lock(mutex_);
515 112x impl_list_.push_back(impl);
516 112x }
517
518 112x return impl;
519 }
520
521 inline void
522 112x posix_signal_service::destroy_impl(posix_signal& impl)
523 {
524 {
525 112x std::lock_guard lock(mutex_);
526 112x impl_list_.remove(&impl);
527 112x }
528
529
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 112 times.
112x delete &impl;
530 112x }
531
532 inline std::error_code
533 118x posix_signal_service::add_signal(
534 posix_signal& impl, int signal_number, signal_set::flags_t flags)
535 {
536
4/4
✓ Branch 0 taken 116 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 114 times.
118x if (signal_number < 0 || signal_number >= max_signal_number)
537 4x return make_error_code(std::errc::invalid_argument);
538
539 // Validate that requested flags are supported on this platform
540 // (e.g., SA_NOCLDWAIT may not be available on all POSIX systems)
541
1/2
✓ Branch 0 taken 114 times.
✗ Branch 1 not taken.
114x if (!posix_signal_detail::flags_supported(flags))
542 return make_error_code(std::errc::operation_not_supported);
543
544 114x posix_signal_detail::signal_state* state =
545 114x posix_signal_detail::get_signal_state();
546
547 // Ensure the global self-pipe exists and this service's scheduler is
548 // watching its read end, BEFORE taking the registration locks. The
549 // reactor drain path locks the descriptor mutex and then the signal-state
550 // and service mutexes; register_signal_reader locks the descriptor mutex
551 // (via register_descriptor), so it must run holding neither of those or
552 // the lock order would invert (a real deadlock, caught by TSan). call_once
553 // makes the once-per-service registration safe when two signal_sets on
554 // this context race add() from different threads.
555 {
556 114x std::lock_guard state_lock(state->mutex);
557
2/4
✓ Branch 0 taken 114 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 114 times.
✗ Branch 3 not taken.
114x if (!posix_signal_detail::open_signal_pipe(state))
558 return make_error_code(std::errc::io_error);
559 114x }
560 196x std::call_once(reader_once_, [this, state] {
561 82x sched_->register_signal_reader(state->read_fd);
562 82x });
563
564 114x std::lock_guard state_lock(state->mutex);
565
1/2
✓ Branch 0 taken 114 times.
✗ Branch 1 not taken.
114x std::lock_guard lock(mutex_);
566
567 // Find insertion point (list is sorted by signal number)
568 114x signal_registration** insertion_point = &impl.signals_;
569 114x signal_registration* reg = impl.signals_;
570
4/4
✓ Branch 0 taken 102 times.
✓ Branch 1 taken 26 times.
✓ Branch 2 taken 14 times.
✓ Branch 3 taken 114 times.
128x while (reg && reg->signal_number < signal_number)
571 {
572 14x insertion_point = &reg->next_in_set;
573 14x reg = reg->next_in_set;
574 }
575
576 // Already registered in this set - check flag compatibility
577 // (same signal_set adding same signal twice with different flags)
578
4/4
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 102 times.
✓ Branch 2 taken 10 times.
✓ Branch 3 taken 2 times.
114x if (reg && reg->signal_number == signal_number)
579 {
580
3/4
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✓ Branch 3 taken 2 times.
10x if (!posix_signal_detail::flags_compatible(reg->flags, flags))
581 2x return make_error_code(std::errc::invalid_argument);
582 8x return {};
583 }
584
585 // Check flag compatibility with global registration
586 // (different signal_set already registered this signal with different flags)
587
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 96 times.
104x if (state->registration_count[signal_number] > 0)
588 {
589
3/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 2 times.
8x if (!posix_signal_detail::flags_compatible(
590 8x state->registered_flags[signal_number], flags))
591 2x return make_error_code(std::errc::invalid_argument);
592 6x }
593
594
1/2
✓ Branch 0 taken 102 times.
✗ Branch 1 not taken.
102x auto* new_reg = new signal_registration;
595 102x new_reg->signal_number = signal_number;
596 102x new_reg->flags = flags;
597 102x new_reg->owner = &impl;
598 102x new_reg->undelivered = 0;
599
600 // Install signal handler on first global registration
601
2/2
✓ Branch 0 taken 96 times.
✓ Branch 1 taken 6 times.
102x if (state->registration_count[signal_number] == 0)
602 {
603 96x struct sigaction sa = {};
604 96x sa.sa_handler = posix_signal_detail::corosio_posix_signal_handler;
605 96x sigemptyset(&sa.sa_mask);
606
1/2
✓ Branch 0 taken 96 times.
✗ Branch 1 not taken.
96x sa.sa_flags = posix_signal_detail::to_sigaction_flags(flags);
607
608
2/4
✓ Branch 0 taken 96 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 96 times.
✗ Branch 3 not taken.
96x if (::sigaction(signal_number, &sa, nullptr) < 0)
609 {
610 delete new_reg;
611 return make_error_code(std::errc::invalid_argument);
612 }
613
614 // Store the flags used for first registration
615 96x state->registered_flags[signal_number] = flags;
616 96x }
617
618 102x new_reg->next_in_set = reg;
619 102x *insertion_point = new_reg;
620
621 102x new_reg->next_in_table = registrations_[signal_number];
622 102x new_reg->prev_in_table = nullptr;
623
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 96 times.
102x if (registrations_[signal_number])
624 6x registrations_[signal_number]->prev_in_table = new_reg;
625 102x registrations_[signal_number] = new_reg;
626
627 102x ++state->registration_count[signal_number];
628 102x ++registration_count_[signal_number];
629
630 102x return {};
631 118x }
632
633 inline std::error_code
634 6x posix_signal_service::remove_signal(posix_signal& impl, int signal_number)
635 {
636
3/4
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 4 times.
6x if (signal_number < 0 || signal_number >= max_signal_number)
637 2x return make_error_code(std::errc::invalid_argument);
638
639 4x posix_signal_detail::signal_state* state =
640 4x posix_signal_detail::get_signal_state();
641 4x std::lock_guard state_lock(state->mutex);
642
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4x std::lock_guard lock(mutex_);
643
644 4x signal_registration** deletion_point = &impl.signals_;
645 4x signal_registration* reg = impl.signals_;
646
3/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 4 times.
4x while (reg && reg->signal_number < signal_number)
647 {
648 deletion_point = &reg->next_in_set;
649 reg = reg->next_in_set;
650 }
651
652
3/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
4x if (!reg || reg->signal_number != signal_number)
653 2x return {};
654
655 // Restore default handler on last global unregistration
656
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2x if (state->registration_count[signal_number] == 1)
657 {
658 2x struct sigaction sa = {};
659 2x sa.sa_handler = SIG_DFL;
660 2x sigemptyset(&sa.sa_mask);
661 2x sa.sa_flags = 0;
662
663
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2x if (::sigaction(signal_number, &sa, nullptr) < 0)
664 return make_error_code(std::errc::invalid_argument);
665
666 // Clear stored flags
667 2x state->registered_flags[signal_number] = signal_set::none;
668 2x }
669
670 2x *deletion_point = reg->next_in_set;
671
672
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2x if (registrations_[signal_number] == reg)
673 2x registrations_[signal_number] = reg->next_in_table;
674
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2x if (reg->prev_in_table)
675 reg->prev_in_table->next_in_table = reg->next_in_table;
676
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2x if (reg->next_in_table)
677 reg->next_in_table->prev_in_table = reg->prev_in_table;
678
679 2x --state->registration_count[signal_number];
680 2x --registration_count_[signal_number];
681
682
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2x delete reg;
683 2x return {};
684 6x }
685
686 inline std::error_code
687 116x posix_signal_service::clear_signals(posix_signal& impl)
688 {
689 116x posix_signal_detail::signal_state* state =
690 116x posix_signal_detail::get_signal_state();
691 116x std::lock_guard state_lock(state->mutex);
692
1/2
✓ Branch 0 taken 116 times.
✗ Branch 1 not taken.
116x std::lock_guard lock(mutex_);
693
694 116x std::error_code first_error;
695
696
2/2
✓ Branch 0 taken 100 times.
✓ Branch 1 taken 116 times.
216x while (signal_registration* reg = impl.signals_)
697 {
698 100x int signal_number = reg->signal_number;
699
700
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 94 times.
100x if (state->registration_count[signal_number] == 1)
701 {
702 94x struct sigaction sa = {};
703 94x sa.sa_handler = SIG_DFL;
704 94x sigemptyset(&sa.sa_mask);
705 94x sa.sa_flags = 0;
706
707
2/6
✗ Branch 0 not taken.
✓ Branch 1 taken 94 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 94 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
94x if (::sigaction(signal_number, &sa, nullptr) < 0 && !first_error)
708 first_error = make_error_code(std::errc::invalid_argument);
709
710 // Clear stored flags
711 94x state->registered_flags[signal_number] = signal_set::none;
712 94x }
713
714 100x impl.signals_ = reg->next_in_set;
715
716
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 100 times.
100x if (registrations_[signal_number] == reg)
717 100x registrations_[signal_number] = reg->next_in_table;
718
1/2
✓ Branch 0 taken 100 times.
✗ Branch 1 not taken.
100x if (reg->prev_in_table)
719 reg->prev_in_table->next_in_table = reg->next_in_table;
720
2/2
✓ Branch 0 taken 94 times.
✓ Branch 1 taken 6 times.
100x if (reg->next_in_table)
721 6x reg->next_in_table->prev_in_table = reg->prev_in_table;
722
723 100x --state->registration_count[signal_number];
724 100x --registration_count_[signal_number];
725
726
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 100 times.
100x delete reg;
727 }
728
729
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 116 times.
116x if (first_error)
730 return first_error;
731 116x return {};
732 116x }
733
734 inline void
735 126x posix_signal_service::cancel_wait(posix_signal& impl)
736 {
737 126x bool was_waiting = false;
738 126x signal_op* op = nullptr;
739
740 {
741 126x std::lock_guard lock(mutex_);
742 126x impl.cancelled_ = true;
743
2/2
✓ Branch 0 taken 122 times.
✓ Branch 1 taken 4 times.
126x if (impl.waiting_)
744 {
745 4x was_waiting = true;
746 4x impl.waiting_ = false;
747 4x op = &impl.pending_op_;
748 4x }
749 126x }
750
751
2/2
✓ Branch 0 taken 122 times.
✓ Branch 1 taken 4 times.
126x if (was_waiting)
752 {
753
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4x if (op->ec_out)
754 4x *op->ec_out = make_error_code(capy::error::canceled);
755
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4x if (op->signal_out)
756 4x *op->signal_out = 0;
757 4x op->cont.h = op->h;
758 4x op->d.post(op->cont);
759 4x sched_->work_finished();
760 4x }
761 126x }
762
763 inline void
764 308x posix_signal_service::start_wait(posix_signal& impl, signal_op* op)
765 {
766 {
767 308x std::lock_guard lock(mutex_);
768
769 // Check if cancel() was called before this wait started
770
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 306 times.
308x if (impl.cancelled_)
771 {
772 2x impl.cancelled_ = false;
773
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2x if (op->ec_out)
774 2x *op->ec_out = make_error_code(capy::error::canceled);
775
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2x if (op->signal_out)
776 2x *op->signal_out = 0;
777 2x op->cont.h = op->h;
778
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2x op->d.post(op->cont);
779 2x return;
780 }
781
782 // Check for queued signals first (signal arrived before wait started)
783 306x signal_registration* reg = impl.signals_;
784
2/2
✓ Branch 0 taken 308 times.
✓ Branch 1 taken 306 times.
614x while (reg)
785 {
786
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 308 times.
308x if (reg->undelivered > 0)
787 {
788 --reg->undelivered;
789 op->signal_number = reg->signal_number;
790 // svc=nullptr: no work_finished needed since we never called work_started
791 op->svc = nullptr;
792 sched_->post(op);
793 return;
794 }
795 308x reg = reg->next_in_set;
796 }
797
798 // No queued signals - wait for delivery
799 306x impl.waiting_ = true;
800 // svc=this: signal_op::operator() will call work_finished() to balance this
801 306x op->svc = this;
802 306x sched_->work_started();
803 308x }
804 308x }
805
806 inline void
807 300x posix_signal_service::deliver_signal(int signal_number)
808 {
809
2/4
✓ Branch 0 taken 300 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 300 times.
300x if (signal_number < 0 || signal_number >= max_signal_number)
810 return;
811
812 300x posix_signal_detail::signal_state* state =
813 300x posix_signal_detail::get_signal_state();
814 300x std::lock_guard lock(state->mutex);
815
816 300x posix_signal_service* service = state->service_list;
817
2/2
✓ Branch 0 taken 300 times.
✓ Branch 1 taken 300 times.
600x while (service)
818 {
819
1/2
✓ Branch 0 taken 300 times.
✗ Branch 1 not taken.
300x std::lock_guard svc_lock(service->mutex_);
820
821 300x signal_registration* reg = service->registrations_[signal_number];
822
2/2
✓ Branch 0 taken 302 times.
✓ Branch 1 taken 300 times.
602x while (reg)
823 {
824 302x posix_signal* impl = static_cast<posix_signal*>(reg->owner);
825
826
1/2
✓ Branch 0 taken 302 times.
✗ Branch 1 not taken.
302x if (impl->waiting_)
827 {
828 302x impl->waiting_ = false;
829 302x impl->pending_op_.signal_number = signal_number;
830
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 302 times.
302x service->post(&impl->pending_op_);
831 302x }
832 else
833 {
834 ++reg->undelivered;
835 }
836
837 302x reg = reg->next_in_table;
838 }
839
840 300x service = service->next_;
841 300x }
842 300x }
843
844 inline void
845 posix_signal_service::work_started() noexcept
846 {
847 sched_->work_started();
848 }
849
850 inline void
851 302x posix_signal_service::work_finished() noexcept
852 {
853 302x sched_->work_finished();
854 302x }
855
856 inline void
857 302x posix_signal_service::post(signal_op* op)
858 {
859 302x sched_->post(op);
860 302x }
861
862 inline void
863 1331x posix_signal_service::add_service(posix_signal_service* service)
864 {
865 1331x posix_signal_detail::signal_state* state =
866 1331x posix_signal_detail::get_signal_state();
867 1331x std::lock_guard lock(state->mutex);
868
869 1331x service->next_ = state->service_list;
870 1331x service->prev_ = nullptr;
871
2/2
✓ Branch 0 taken 1326 times.
✓ Branch 1 taken 5 times.
1331x if (state->service_list)
872 5x state->service_list->prev_ = service;
873 1331x state->service_list = service;
874 1331x }
875
876 inline void
877 1331x posix_signal_service::remove_service(posix_signal_service* service)
878 {
879 1331x posix_signal_detail::signal_state* state =
880 1331x posix_signal_detail::get_signal_state();
881 1331x std::lock_guard lock(state->mutex);
882
883
4/6
✓ Branch 0 taken 1326 times.
✓ Branch 1 taken 5 times.
✓ Branch 2 taken 1326 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 1326 times.
1331x if (service->next_ || service->prev_ || state->service_list == service)
884 {
885
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1331 times.
1331x if (state->service_list == service)
886 1331x state->service_list = service->next_;
887
1/2
✓ Branch 0 taken 1331 times.
✗ Branch 1 not taken.
1331x if (service->prev_)
888 service->prev_->next_ = service->next_;
889
2/2
✓ Branch 0 taken 1326 times.
✓ Branch 1 taken 5 times.
1331x if (service->next_)
890 5x service->next_->prev_ = service->prev_;
891 1331x service->next_ = nullptr;
892 1331x service->prev_ = nullptr;
893 1331x }
894 1331x }
895
896 // get_signal_service - factory function
897
898 inline posix_signal_service&
899 1331x get_signal_service(capy::execution_context& ctx, scheduler& sched)
900 {
901 1331x return ctx.make_service<posix_signal_service>(sched);
902 }
903
904 } // namespace detail
905 } // namespace boost::corosio
906
907 #endif // BOOST_COROSIO_POSIX
908
909 #endif // BOOST_COROSIO_NATIVE_DETAIL_POSIX_POSIX_SIGNAL_SERVICE_HPP
910