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

99.0% Lines (393 / 397) 97.0% Functions (32 / 33)
posix_signal_service.hpp
f(x) Functions (33)
Function Calls Lines Blocks
boost::corosio::detail::posix_signal_service::destroy(boost::corosio::io_object::implementation*) :168 270x 100.0% 100.0% boost::corosio::detail::posix_signal_service::reset_token_cancel(boost::corosio::detail::posix_signal&) :210 1622x 100.0% 100.0% boost::corosio::detail::posix_signal_detail::flags_supported(boost::corosio::signal_set::flags_t) :287 297x 100.0% 100.0% boost::corosio::detail::posix_signal_detail::to_sigaction_flags(boost::corosio::signal_set::flags_t) :299 231x 100.0% 100.0% boost::corosio::detail::posix_signal_detail::flags_compatible(boost::corosio::signal_set::flags_t, boost::corosio::signal_set::flags_t) :319 55x 100.0% 100.0% boost::corosio::detail::posix_signal_detail::open_signal_pipe(boost::corosio::detail::posix_signal_detail::signal_state*) :339 297x 100.0% 97.0% boost::corosio::detail::posix_signal_detail::corosio_posix_signal_handler(int) :373 477x 100.0% 100.0% boost::corosio::detail::posix_signal_detail::drain_signal_pipe() :389 476x 100.0% 100.0% boost::corosio::detail::signal_op::operator()() :405 482x 100.0% 100.0% boost::corosio::detail::signal_op::destroy() :425 0 0.0% 0.0% boost::corosio::detail::posix_signal::posix_signal(boost::corosio::detail::posix_signal_service&) :432 279x 100.0% 100.0% boost::corosio::detail::posix_signal::wait(std::__n4861::coroutine_handle<void>, boost::capy::executor_ref, std::stop_token, std::error_code*, int*) :438 1696x 100.0% 100.0% boost::corosio::detail::posix_signal::add(int, boost::corosio::signal_set::flags_t) :484 303x 100.0% 100.0% boost::corosio::detail::posix_signal::remove(int) :490 37x 100.0% 100.0% boost::corosio::detail::posix_signal::clear() :496 290x 100.0% 100.0% boost::corosio::detail::posix_signal::cancel() :502 295x 100.0% 100.0% boost::corosio::detail::posix_signal_service::posix_signal_service(boost::capy::execution_context&) :509 240x 100.0% 80.0% boost::corosio::detail::posix_signal_service::~posix_signal_service() :521 480x 100.0% 100.0% boost::corosio::detail::posix_signal_service::shutdown() :527 240x 100.0% 91.0% boost::corosio::detail::posix_signal_service::construct() :591 279x 100.0% 100.0% boost::corosio::detail::posix_signal_service::destroy_impl(boost::corosio::detail::posix_signal&) :604 270x 100.0% 100.0% boost::corosio::detail::posix_signal_service::add_signal(boost::corosio::detail::posix_signal&, int, boost::corosio::signal_set::flags_t) :615 303x 98.2% 84.0% boost::corosio::detail::posix_signal_service::remove_signal(boost::corosio::detail::posix_signal&, int) :725 37x 100.0% 95.0% boost::corosio::detail::posix_signal_service::clear_signals(boost::corosio::detail::posix_signal&) :778 290x 100.0% 95.0% boost::corosio::detail::posix_signal_service::cancel_wait(boost::corosio::detail::posix_signal&) :826 295x 100.0% 100.0% boost::corosio::detail::posix_signal_service::cancel_wait_token(boost::corosio::detail::posix_signal&) :855 1121x 100.0% 100.0% boost::corosio::detail::posix_signal::token_canceller::operator()() const :887 1121x 100.0% 100.0% boost::corosio::detail::posix_signal_service::start_wait(boost::corosio::detail::posix_signal&, boost::corosio::detail::signal_op*) :893 1622x 100.0% 94.0% boost::corosio::detail::posix_signal_service::deliver_signal(int) :950 476x 95.0% 76.0% boost::corosio::detail::posix_signal_service::work_finished() :994 479x 100.0% 100.0% boost::corosio::detail::posix_signal_service::post(boost::corosio::detail::signal_op*) :1000 479x 100.0% 100.0% boost::corosio::detail::posix_signal_service::add_service(boost::corosio::detail::posix_signal_service*) :1006 240x 100.0% 100.0% boost::corosio::detail::posix_signal_service::remove_service(boost::corosio::detail::posix_signal_service*) :1020 240x 100.0% 100.0%
Line 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/corosio/native/detail/make_err.hpp>
24 #include <boost/capy/error.hpp>
25
26 #include <mutex>
27 #include <tuple>
28
29 #include <errno.h>
30 #include <fcntl.h>
31 #include <signal.h>
32 #include <unistd.h>
33
34 /*
35 POSIX Signal Service
36 ====================
37
38 Concrete signal service implementation for POSIX backends. Manages signal
39 registrations via sigaction() and dispatches completions through the
40 scheduler. One instance per execution_context, created on first use
41 by the public signal_set.
42
43 See the block comment further down for the full architecture overview.
44 */
45
46 /*
47 POSIX Signal Implementation
48 ===========================
49
50 This file implements signal handling for POSIX systems using sigaction().
51 The implementation supports signal flags (SA_RESTART, etc.) and integrates
52 with any POSIX-compatible scheduler via the abstract scheduler interface.
53
54 Architecture Overview
55 ---------------------
56
57 Three layers manage signal registrations:
58
59 1. signal_state (global singleton)
60 - Tracks the global service list and per-signal registration counts
61 - Stores the flags used for first registration of each signal (for
62 conflict detection when multiple signal_sets register same signal)
63 - Owns the mutex that protects signal handler installation/removal
64
65 2. posix_signal_service (one per execution_context)
66 - Maintains registrations_[] table indexed by signal number
67 - Each slot is a doubly-linked list of signal_registrations for that signal
68 - Also maintains impl_list_ of all posix_signal objects it owns
69
70 3. posix_signal (one per signal_set)
71 - Owns a singly-linked list (sorted by signal number) of signal_registrations
72 - Contains the pending_op_ used for wait operations
73
74 Signal Delivery Flow
75 --------------------
76
77 Delivery uses the self-pipe trick so the signal handler itself performs
78 only async-signal-safe work (mirrors Boost.Asio):
79
80 1. Signal arrives -> corosio_posix_signal_handler(). The handler only
81 write()s the signal number to the global self-pipe (write_fd) and
82 restores errno. No locks, no allocation, no scheduler dispatch.
83
84 2. The read end of the pipe is watched by one backend's event loop
85 (registered via scheduler::register_signal_reader on the first
86 registration). When it becomes readable the backend drains it
87 (drain_signal_pipe) and calls deliver_signal() in normal context.
88
89 3. deliver_signal() iterates all posix_signal_service services:
90 - If a signal_set is waiting (impl->waiting_ == true), post the signal_op
91 to the scheduler for immediate completion
92 - Otherwise, increment reg->undelivered to queue the signal
93
94 4. When wait() is called via start_wait():
95 - First check for queued signals (undelivered > 0); if found, post
96 immediate completion without blocking
97 - Otherwise, set waiting_ = true and call work_started() to keep
98 the io_context alive
99
100 Locking Protocol
101 ----------------
102
103 Two mutex levels exist (MUST acquire in this order to avoid deadlock):
104 1. signal_state::mutex - protects handler registration and service list
105 2. posix_signal_service::mutex_ - protects per-service registration tables
106
107 Async-Signal-Safety
108 -------------------
109
110 The C signal handler (corosio_posix_signal_handler) performs only
111 async-signal-safe operations: it reads the single global write_fd and
112 calls write(), saving/restoring errno. It never locks a mutex, allocates
113 memory, or dispatches through the scheduler. All of that happens in
114 deliver_signal(), which runs in normal thread context from the backend
115 event loop after draining the self-pipe. There is therefore no
116 self-deadlock risk if a signal arrives while a thread holds state->mutex
117 or service->mutex_.
118
119 Flag Handling
120 -------------
121
122 - Flags are abstract values in the public API (signal_set::flags_t)
123 - flags_supported() validates that requested flags are available on
124 this platform; returns false if SA_NOCLDWAIT is unavailable and
125 no_child_wait is requested
126 - to_sigaction_flags() maps validated flags to actual SA_* constants
127 - First registration of a signal establishes the flags; subsequent
128 registrations must be compatible (same flags or dont_care)
129 - Requesting unavailable flags returns operation_not_supported
130
131 Work Tracking
132 -------------
133
134 When waiting for a signal:
135 - start_wait() calls sched_->work_started() to prevent io_context::run()
136 from returning while we wait
137 - signal_op::svc is set to point to the service
138 - signal_op::operator()() calls work_finished() after resuming the coroutine
139
140 If a signal was already queued (undelivered > 0), no work tracking is needed
141 because completion is posted immediately.
142 */
143
144 namespace boost::corosio {
145
146 namespace detail {
147
148 /** Signal service for POSIX backends.
149
150 Manages signal registrations via sigaction() and dispatches signal
151 completions through the scheduler. One instance per execution_context.
152 */
153 class BOOST_COROSIO_DECL posix_signal_service final
154 : public capy::execution_context::service
155 , public io_object::io_service
156 {
157 public:
158 using key_type = posix_signal_service;
159
160 explicit posix_signal_service(capy::execution_context& ctx);
161 ~posix_signal_service() override;
162
163 posix_signal_service(posix_signal_service const&) = delete;
164 posix_signal_service& operator=(posix_signal_service const&) = delete;
165
166 io_object::implementation* construct() override;
167
168 270x void destroy(io_object::implementation* p) override
169 {
170 270x auto& impl = static_cast<posix_signal&>(*p);
171 270x [[maybe_unused]] auto n = impl.clear();
172 270x impl.disarm_stop();
173 270x impl.cancel();
174 270x destroy_impl(impl);
175 270x }
176
177 /** Shut down the service.
178
179 Destroys every implementation the service still owns and gives
180 each of their registrations back to the process-global table.
181 */
182 void shutdown() override;
183
184 void destroy_impl(posix_signal& impl);
185
186 std::error_code add_signal(
187 posix_signal& impl, int signal_number, signal_set::flags_t flags);
188
189 std::error_code remove_signal(posix_signal& impl, int signal_number);
190
191 std::error_code clear_signals(posix_signal& impl);
192
193 void cancel_wait(posix_signal& impl);
194 void start_wait(posix_signal& impl, signal_op* op);
195
196 /** Cancel an in-flight wait on behalf of a stop token.
197
198 Identical to @ref cancel_wait except that it does not set the
199 sticky `cancelled_` latch: a stop token scopes to one operation,
200 so a request arriving after the wait completed must do nothing.
201 */
202 void cancel_wait_token(posix_signal& impl) noexcept;
203
204 /** Clear the per-operation stop flag before a new wait arms.
205
206 Lives here rather than on the implementation because `mutex_` is
207 the service's; the service is a friend of `posix_signal`, not the
208 reverse.
209 */
210 1622x void reset_token_cancel(posix_signal& impl) noexcept
211 {
212 1622x std::lock_guard lock(mutex_);
213 1622x impl.token_cancelled_ = false;
214 1622x }
215
216 static void deliver_signal(int signal_number);
217
218 void work_started() noexcept;
219 void work_finished() noexcept;
220 void post(signal_op* op);
221
222 private:
223 static void add_service(posix_signal_service* service);
224 static void remove_service(posix_signal_service* service);
225
226 scheduler* sched_;
227 std::mutex mutex_;
228
229 // Registers the signal self-pipe's read end with sched_ exactly once per
230 // service, so every io_context that waits on a signal can drain the pipe.
231 // A once_flag (not a bool under mutex_) because registration must run
232 // without holding mutex_ or the signal-state mutex — see add_signal.
233 std::mutex reader_mutex_;
234 bool reader_registered_ = false;
235
236 intrusive_list<posix_signal> impl_list_;
237
238 // Per-signal registration table
239 signal_registration* registrations_[max_signal_number];
240
241 // Registration counts for each signal
242 std::size_t registration_count_[max_signal_number];
243
244 // Linked list of all posix_signal_service services for signal delivery
245 posix_signal_service* next_ = nullptr;
246 posix_signal_service* prev_ = nullptr;
247 };
248
249 } // namespace detail
250
251 } // namespace boost::corosio
252
253 // ---------------------------------------------------------------------------
254 // Inline implementation
255 // ---------------------------------------------------------------------------
256
257 namespace boost::corosio {
258
259 namespace detail {
260
261 namespace posix_signal_detail {
262
263 struct signal_state
264 {
265 std::mutex mutex;
266 posix_signal_service* service_list = nullptr;
267 std::size_t registration_count[max_signal_number] = {};
268 signal_set::flags_t registered_flags[max_signal_number] = {};
269
270 // Self-pipe used to defer signal delivery out of handler context.
271 // The C handler writes the signal number to write_fd (async-signal-
272 // safe); a backend event loop drains read_fd and calls deliver_signal()
273 // in normal context. Created once (on the first signal registration) and
274 // kept for the process lifetime. Each posix_signal_service registers the
275 // read end with its own scheduler (see reader_once_) so every running
276 // io_context can drain it; multiple readers on one pipe are safe because
277 // each signal is a fixed sizeof(int) record read atomically.
278 int read_fd = -1;
279 int write_fd = -1;
280 };
281
282 BOOST_COROSIO_DECL signal_state* get_signal_state();
283
284 // Check if requested flags are supported on this platform.
285 // Returns true if all flags are supported, false otherwise.
286 inline bool
287 297x flags_supported([[maybe_unused]] signal_set::flags_t flags)
288 {
289 #ifndef SA_NOCLDWAIT
290 if (flags & signal_set::no_child_wait)
291 return false;
292 #endif
293 297x return true;
294 }
295
296 // Map abstract flags to sigaction() flags.
297 // Caller must ensure flags_supported() returns true first.
298 inline int
299 231x to_sigaction_flags(signal_set::flags_t flags)
300 {
301 231x int sa_flags = 0;
302 231x if (flags & signal_set::restart)
303 33x sa_flags |= SA_RESTART;
304 231x if (flags & signal_set::no_child_stop)
305 4x sa_flags |= SA_NOCLDSTOP;
306 #ifdef SA_NOCLDWAIT
307 231x if (flags & signal_set::no_child_wait)
308 3x sa_flags |= SA_NOCLDWAIT;
309 #endif
310 231x if (flags & signal_set::no_defer)
311 6x sa_flags |= SA_NODEFER;
312 231x if (flags & signal_set::reset_handler)
313 3x sa_flags |= SA_RESETHAND;
314 231x return sa_flags;
315 }
316
317 // Check if two flag values are compatible
318 inline bool
319 55x flags_compatible(signal_set::flags_t existing, signal_set::flags_t requested)
320 {
321 // dont_care is always compatible
322 107x if ((existing & signal_set::dont_care) ||
323 52x (requested & signal_set::dont_care))
324 10x return true;
325
326 // Mask out dont_care bit for comparison
327 45x constexpr auto mask = ~signal_set::dont_care;
328 45x return (existing & mask) == (requested & mask);
329 }
330
331 // Lazily create the global signal self-pipe. Idempotent; call under
332 // state->mutex before installing the first signal handler so write_fd is
333 // valid by the time the handler can fire. Both ends are non-blocking and
334 // close-on-exec (mirrors the reactor self-pipe setup in select_scheduler).
335 // Returns the failing call's errno and leaves the fds at -1 if creation
336 // fails: an exhausted descriptor table and a rejected fcntl are different
337 // problems to the caller of add().
338 [[nodiscard]] inline std::error_code
339 297x open_signal_pipe(signal_state* state)
340 {
341 297x if (state->read_fd >= 0)
342 280x return {};
343
344 int fds[2];
345 17x if (::pipe(fds) < 0)
346 1x return make_err(errno);
347
348 39x for (int i = 0; i < 2; ++i)
349 {
350 29x int fl = ::fcntl(fds[i], F_GETFL, 0);
351 54x if (fl == -1 || ::fcntl(fds[i], F_SETFL, fl | O_NONBLOCK) == -1 ||
352 25x ::fcntl(fds[i], F_SETFD, FD_CLOEXEC) == -1)
353 {
354 6x auto ec = make_err(errno);
355 6x ::close(fds[0]);
356 6x ::close(fds[1]);
357 6x return ec;
358 }
359 }
360
361 10x state->read_fd = fds[0];
362 10x state->write_fd = fds[1];
363 10x return {};
364 }
365
366 // C signal handler. Async-signal-safe: it touches only the single global
367 // write_fd (an int set before any handler is installed) and calls write(),
368 // which POSIX lists as async-signal-safe. errno is saved and restored so an
369 // interrupted foreground syscall is unaffected. A full pipe (write returns
370 // EAGAIN) or a short write is intentionally dropped — the reactor still
371 // coalesces because deliver_signal reports the signal to every waiting set.
372 inline void
373 477x corosio_posix_signal_handler(int signal_number)
374 {
375 477x int saved_errno = errno;
376 477x signal_state* state = get_signal_state();
377 [[maybe_unused]] ssize_t r =
378 477x ::write(state->write_fd, &signal_number, sizeof(int));
379 477x errno = saved_errno;
380 // With sigaction(), the handler persists automatically (unlike some
381 // signal() implementations that reset to SIG_DFL).
382 477x }
383
384 // Drain the signal self-pipe and deliver each pending signal. Runs in normal
385 // thread context from the backend event loop, so deliver_signal()'s mutex
386 // locking and scheduler post are safe here. Reads until EAGAIN (edge-
387 // triggered backends require a full drain per readiness event).
388 inline void
389 476x drain_signal_pipe()
390 {
391 476x signal_state* state = get_signal_state();
392 int signal_number;
393 952x while (::read(state->read_fd, &signal_number, sizeof(int)) ==
394 static_cast<ssize_t>(sizeof(int)))
395 {
396 476x posix_signal_service::deliver_signal(signal_number);
397 }
398 476x }
399
400 } // namespace posix_signal_detail
401
402 // signal_op implementation
403
404 inline void
405 482x signal_op::operator()()
406 {
407 482x if (ec_out)
408 482x *ec_out = {};
409 482x if (signal_out)
410 482x *signal_out = signal_number;
411
412 // Capture svc before resuming (coro may destroy us)
413 482x auto* service = svc;
414 482x svc = nullptr;
415
416 482x cont.h = h;
417 482x d.post(cont);
418
419 // Balance the work_started() from start_wait
420 482x if (service)
421 479x service->work_finished();
422 482x }
423
424 inline void
425 ✗ signal_op::destroy()
426 {
427 // No-op: signal_op is embedded in posix_signal
428 ✗ }
429
430 // posix_signal implementation
431
432 279x inline posix_signal::posix_signal(posix_signal_service& svc) noexcept
433 279x : svc_(svc)
434 {
435 279x }
436
437 inline std::coroutine_handle<>
438 1696x posix_signal::wait(
439 std::coroutine_handle<> h,
440 capy::executor_ref d,
441 std::stop_token token,
442 std::error_code* ec,
443 int* signal_out)
444 {
445 1696x pending_op_.h = h;
446 1696x pending_op_.d = d;
447 1696x pending_op_.ec_out = ec;
448 1696x pending_op_.signal_out = signal_out;
449 1696x pending_op_.signal_number = 0;
450
451 // Disarm any callback left over from a previous wait before doing
452 // anything else, including the early return below: otherwise that
453 // path leaves this object owning a callback it no longer uses.
454 // Outside start_wait's lock on purpose: ~stop_callback blocks until a
455 // concurrently running callback returns, and that callback takes
456 // posix_signal_service::mutex_.
457 1696x stop_cb_.reset();
458
459 1696x if (token.stop_requested())
460 {
461 74x if (ec)
462 74x *ec = make_error_code(capy::error::canceled);
463 74x if (signal_out)
464 74x *signal_out = 0;
465 74x pending_op_.cont.h = h;
466 74x d.post(pending_op_.cont);
467 // completion is always posted to scheduler queue, never inline.
468 74x return std::noop_coroutine();
469 }
470
471 // Clearing the flag before arming is load-bearing: reset_token_cancel
472 // must run immediately before emplace, not before the early return
473 // above.
474 1622x svc_.reset_token_cancel(*this);
475 1622x if (token.stop_possible())
476 1127x stop_cb_.emplace(token, token_canceller{this});
477
478 1622x svc_.start_wait(*this, &pending_op_);
479 // completion is always posted to scheduler queue, never inline.
480 1622x return std::noop_coroutine();
481 }
482
483 inline std::error_code
484 303x posix_signal::add(int signal_number, signal_set::flags_t flags)
485 {
486 303x return svc_.add_signal(*this, signal_number, flags);
487 }
488
489 inline std::error_code
490 37x posix_signal::remove(int signal_number)
491 {
492 37x return svc_.remove_signal(*this, signal_number);
493 }
494
495 inline std::error_code
496 290x posix_signal::clear()
497 {
498 290x return svc_.clear_signals(*this);
499 }
500
501 inline void
502 295x posix_signal::cancel() noexcept
503 {
504 295x svc_.cancel_wait(*this);
505 295x }
506
507 // posix_signal_service implementation
508
509 240x inline posix_signal_service::posix_signal_service(
510 240x capy::execution_context& ctx)
511 240x : sched_(&get_scheduler(ctx))
512 {
513 15600x for (int i = 0; i < max_signal_number; ++i)
514 {
515 15360x registrations_[i] = nullptr;
516 15360x registration_count_[i] = 0;
517 }
518 240x add_service(this);
519 240x }
520
521 480x inline posix_signal_service::~posix_signal_service()
522 {
523 240x remove_service(this);
524 480x }
525
526 inline void
527 240x posix_signal_service::shutdown()
528 {
529 // Collected under the locks below and deleted after they are released:
530 // ~posix_signal destroys an armed stop_cb_, and ~stop_callback blocks
531 // until a concurrently running token_canceller returns -- which takes
532 // mutex_. Deleting while still holding mutex_ would self-deadlock the
533 // same way disarm_stop() would if called inside the locked loop.
534 240x intrusive_list<posix_signal> doomed;
535
536 {
537 posix_signal_detail::signal_state* state =
538 240x posix_signal_detail::get_signal_state();
539 240x std::lock_guard state_lock(state->mutex);
540 240x std::lock_guard lock(mutex_);
541
542 249x for (auto* impl = impl_list_.pop_front(); impl != nullptr;
543 9x impl = impl_list_.pop_front())
544 {
545 18x while (auto* reg = impl->signals_)
546 {
547 9x int const signal_number = reg->signal_number;
548
549 // The registration table outlives every io_context, so a set
550 // still registered here has to give its count and disposition
551 // back the way clear() would: otherwise the signal stays
552 // installed with these flags and the next add() of it is
553 // refused. The per-node table unlink clear() also does is
554 // skipped in favour of the wholesale null-out below.
555 9x if (state->registration_count[signal_number] == 1)
556 {
557 6x struct sigaction sa = {};
558 6x sa.sa_handler = SIG_DFL;
559 6x sigemptyset(&sa.sa_mask);
560 6x sa.sa_flags = 0;
561 6x std::ignore = ::sigaction(signal_number, &sa, nullptr);
562 6x state->registered_flags[signal_number] = signal_set::none;
563 }
564
565 9x --state->registration_count[signal_number];
566 9x --registration_count_[signal_number];
567
568 9x impl->signals_ = reg->next_in_set;
569 9x delete reg;
570 9x }
571 9x doomed.push_back(impl);
572 }
573
574 // Every live registration hung off an implementation in impl_list_,
575 // so the whole table goes stale at once and can be dropped wholesale
576 // rather than node by node. It has to be dropped: deliver_signal()
577 // walks this service until the destructor unlinks it from the global
578 // list.
579 15600x for (int i = 0; i < max_signal_number; ++i)
580 15360x registrations_[i] = nullptr;
581 240x }
582
583 249x for (auto* impl = doomed.pop_front(); impl != nullptr;
584 9x impl = doomed.pop_front())
585 {
586 9x delete impl;
587 }
588 240x }
589
590 inline io_object::implementation*
591 279x posix_signal_service::construct()
592 {
593 279x auto* impl = new posix_signal(*this);
594
595 {
596 279x std::lock_guard lock(mutex_);
597 279x impl_list_.push_back(impl);
598 279x }
599
600 279x return impl;
601 }
602
603 inline void
604 270x posix_signal_service::destroy_impl(posix_signal& impl)
605 {
606 {
607 270x std::lock_guard lock(mutex_);
608 270x impl_list_.remove(&impl);
609 270x }
610
611 270x delete &impl;
612 270x }
613
614 inline std::error_code
615 303x posix_signal_service::add_signal(
616 posix_signal& impl, int signal_number, signal_set::flags_t flags)
617 {
618 303x if (signal_number < 0 || signal_number >= max_signal_number)
619 6x return make_error_code(std::errc::invalid_argument);
620
621 // Validate that requested flags are supported on this platform
622 // (e.g., SA_NOCLDWAIT may not be available on all POSIX systems)
623 297x if (!posix_signal_detail::flags_supported(flags))
624 ✗ return make_error_code(std::errc::operation_not_supported);
625
626 posix_signal_detail::signal_state* state =
627 297x posix_signal_detail::get_signal_state();
628
629 // Ensure the global self-pipe exists and this service's scheduler is
630 // watching its read end, BEFORE taking the registration locks. The
631 // reactor drain path locks the descriptor mutex and then the signal-state
632 // and service mutexes; register_signal_reader locks the descriptor mutex
633 // (via register_descriptor), so it must run holding neither of those or
634 // the lock order would invert (a real deadlock, caught by TSan). call_once
635 // makes the once-per-service registration safe when two signal_sets on
636 // this context race add() from different threads.
637 {
638 297x std::lock_guard state_lock(state->mutex);
639 297x if (auto ec = posix_signal_detail::open_signal_pipe(state))
640 7x return ec;
641 297x }
642 {
643 // Success-latched so a failed environmental registration
644 // (epoll_ctl ENOMEM/ENOSPC) is retried by the next add()
645 // instead of being lost; the code travels the return channel.
646 290x std::lock_guard reg_lock(reader_mutex_);
647 290x if (!reader_registered_)
648 {
649 203x if (auto ec = sched_->register_signal_reader(state->read_fd))
650 4x return ec;
651 199x reader_registered_ = true;
652 }
653 290x }
654
655 286x std::lock_guard state_lock(state->mutex);
656 286x std::lock_guard lock(mutex_);
657
658 // Find insertion point (list is sorted by signal number)
659 286x signal_registration** insertion_point = &impl.signals_;
660 286x signal_registration* reg = impl.signals_;
661 317x while (reg && reg->signal_number < signal_number)
662 {
663 31x insertion_point = &reg->next_in_set;
664 31x reg = reg->next_in_set;
665 }
666
667 // Already registered in this set - check flag compatibility
668 // (same signal_set adding same signal twice with different flags)
669 286x if (reg && reg->signal_number == signal_number)
670 {
671 18x if (!posix_signal_detail::flags_compatible(reg->flags, flags))
672 5x return make_error_code(std::errc::invalid_argument);
673 13x return {};
674 }
675
676 // Check flag compatibility with global registration
677 // (different signal_set already registered this signal with different flags)
678 268x if (state->registration_count[signal_number] > 0)
679 {
680 37x if (!posix_signal_detail::flags_compatible(
681 state->registered_flags[signal_number], flags))
682 3x return make_error_code(std::errc::invalid_argument);
683 }
684
685 265x auto* new_reg = new signal_registration;
686 265x new_reg->signal_number = signal_number;
687 265x new_reg->flags = flags;
688 265x new_reg->owner = &impl;
689 265x new_reg->undelivered = 0;
690
691 // Install signal handler on first global registration
692 265x if (state->registration_count[signal_number] == 0)
693 {
694 231x struct sigaction sa = {};
695 231x sa.sa_handler = posix_signal_detail::corosio_posix_signal_handler;
696 231x sigemptyset(&sa.sa_mask);
697 231x sa.sa_flags = posix_signal_detail::to_sigaction_flags(flags);
698
699 231x if (::sigaction(signal_number, &sa, nullptr) < 0)
700 {
701 1x delete new_reg;
702 1x return make_error_code(std::errc::invalid_argument);
703 }
704
705 // Store the flags used for first registration
706 230x state->registered_flags[signal_number] = flags;
707 }
708
709 264x new_reg->next_in_set = reg;
710 264x *insertion_point = new_reg;
711
712 264x new_reg->next_in_table = registrations_[signal_number];
713 264x new_reg->prev_in_table = nullptr;
714 264x if (registrations_[signal_number])
715 25x registrations_[signal_number]->prev_in_table = new_reg;
716 264x registrations_[signal_number] = new_reg;
717
718 264x ++state->registration_count[signal_number];
719 264x ++registration_count_[signal_number];
720
721 264x return {};
722 286x }
723
724 inline std::error_code
725 37x posix_signal_service::remove_signal(posix_signal& impl, int signal_number)
726 {
727 37x if (signal_number < 0 || signal_number >= max_signal_number)
728 3x return make_error_code(std::errc::invalid_argument);
729
730 posix_signal_detail::signal_state* state =
731 34x posix_signal_detail::get_signal_state();
732 34x std::lock_guard state_lock(state->mutex);
733 34x std::lock_guard lock(mutex_);
734
735 34x signal_registration** deletion_point = &impl.signals_;
736 34x signal_registration* reg = impl.signals_;
737 37x while (reg && reg->signal_number < signal_number)
738 {
739 3x deletion_point = &reg->next_in_set;
740 3x reg = reg->next_in_set;
741 }
742
743 34x if (!reg || reg->signal_number != signal_number)
744 4x return {};
745
746 // Restore default handler on last global unregistration
747 30x if (state->registration_count[signal_number] == 1)
748 {
749 24x struct sigaction sa = {};
750 24x sa.sa_handler = SIG_DFL;
751 24x sigemptyset(&sa.sa_mask);
752 24x sa.sa_flags = 0;
753
754 24x if (::sigaction(signal_number, &sa, nullptr) < 0)
755 1x return make_error_code(std::errc::invalid_argument);
756
757 // Clear stored flags
758 23x state->registered_flags[signal_number] = signal_set::none;
759 }
760
761 29x *deletion_point = reg->next_in_set;
762
763 29x if (registrations_[signal_number] == reg)
764 26x registrations_[signal_number] = reg->next_in_table;
765 29x if (reg->prev_in_table)
766 3x reg->prev_in_table->next_in_table = reg->next_in_table;
767 29x if (reg->next_in_table)
768 3x reg->next_in_table->prev_in_table = reg->prev_in_table;
769
770 29x --state->registration_count[signal_number];
771 29x --registration_count_[signal_number];
772
773 29x delete reg;
774 29x return {};
775 34x }
776
777 inline std::error_code
778 290x posix_signal_service::clear_signals(posix_signal& impl)
779 {
780 posix_signal_detail::signal_state* state =
781 290x posix_signal_detail::get_signal_state();
782 290x std::lock_guard state_lock(state->mutex);
783 290x std::lock_guard lock(mutex_);
784
785 290x std::error_code first_error;
786
787 516x while (signal_registration* reg = impl.signals_)
788 {
789 226x int signal_number = reg->signal_number;
790
791 226x if (state->registration_count[signal_number] == 1)
792 {
793 201x struct sigaction sa = {};
794 201x sa.sa_handler = SIG_DFL;
795 201x sigemptyset(&sa.sa_mask);
796 201x sa.sa_flags = 0;
797
798 201x if (::sigaction(signal_number, &sa, nullptr) < 0 && !first_error)
799 1x first_error = make_error_code(std::errc::invalid_argument);
800
801 // Clear stored flags
802 201x state->registered_flags[signal_number] = signal_set::none;
803 }
804
805 226x impl.signals_ = reg->next_in_set;
806
807 226x if (registrations_[signal_number] == reg)
808 223x registrations_[signal_number] = reg->next_in_table;
809 226x if (reg->prev_in_table)
810 3x reg->prev_in_table->next_in_table = reg->next_in_table;
811 226x if (reg->next_in_table)
812 16x reg->next_in_table->prev_in_table = reg->prev_in_table;
813
814 226x --state->registration_count[signal_number];
815 226x --registration_count_[signal_number];
816
817 226x delete reg;
818 226x }
819
820 290x if (first_error)
821 1x return first_error;
822 289x return {};
823 290x }
824
825 inline void
826 295x posix_signal_service::cancel_wait(posix_signal& impl)
827 {
828 295x bool was_waiting = false;
829 295x signal_op* op = nullptr;
830
831 {
832 295x std::lock_guard lock(mutex_);
833 295x impl.cancelled_ = true;
834 295x if (impl.waiting_)
835 {
836 10x was_waiting = true;
837 10x impl.waiting_ = false;
838 10x op = &impl.pending_op_;
839 }
840 295x }
841
842 295x if (was_waiting)
843 {
844 10x if (op->ec_out)
845 10x *op->ec_out = make_error_code(capy::error::canceled);
846 10x if (op->signal_out)
847 10x *op->signal_out = 0;
848 10x op->cont.h = op->h;
849 10x op->d.post(op->cont);
850 10x sched_->work_finished();
851 }
852 295x }
853
854 inline void
855 1121x posix_signal_service::cancel_wait_token(posix_signal& impl) noexcept
856 {
857 1121x bool was_waiting = false;
858 1121x signal_op* op = nullptr;
859
860 {
861 1121x std::lock_guard lock(mutex_);
862 // Persist the request even when no wait is parked yet: wait()
863 // arms the callback before start_wait takes this lock, and
864 // start_wait consumes this flag.
865 1121x impl.token_cancelled_ = true;
866 1121x if (impl.waiting_)
867 {
868 1054x was_waiting = true;
869 1054x impl.waiting_ = false;
870 1054x op = &impl.pending_op_;
871 }
872 1121x }
873
874 1121x if (was_waiting)
875 {
876 1054x if (op->ec_out)
877 1054x *op->ec_out = make_error_code(capy::error::canceled);
878 1054x if (op->signal_out)
879 1054x *op->signal_out = 0;
880 1054x op->cont.h = op->h;
881 1054x op->d.post(op->cont);
882 1054x sched_->work_finished();
883 }
884 1121x }
885
886 inline void
887 1121x posix_signal::token_canceller::operator()() const noexcept
888 {
889 1121x self->svc_.cancel_wait_token(*self);
890 1121x }
891
892 inline void
893 1622x posix_signal_service::start_wait(posix_signal& impl, signal_op* op)
894 {
895 {
896 1622x std::lock_guard lock(mutex_);
897
898 // Check if cancel() was called before this wait started
899 1622x if (impl.cancelled_)
900 {
901 3x impl.cancelled_ = false;
902 3x if (op->ec_out)
903 3x *op->ec_out = make_error_code(capy::error::canceled);
904 3x if (op->signal_out)
905 3x *op->signal_out = 0;
906 3x op->cont.h = op->h;
907 3x op->d.post(op->cont);
908 3x return;
909 }
910
911 // A stop request that arrived between wait() arming the callback
912 // and this lock: complete now rather than parking forever.
913 1619x if (impl.token_cancelled_)
914 {
915 64x impl.token_cancelled_ = false;
916 64x if (op->ec_out)
917 64x *op->ec_out = make_error_code(capy::error::canceled);
918 64x if (op->signal_out)
919 64x *op->signal_out = 0;
920 64x op->cont.h = op->h;
921 64x op->d.post(op->cont);
922 64x return;
923 }
924
925 // Check for queued signals first (signal arrived before wait started)
926 1555x signal_registration* reg = impl.signals_;
927 3112x while (reg)
928 {
929 1560x if (reg->undelivered > 0)
930 {
931 3x --reg->undelivered;
932 3x op->signal_number = reg->signal_number;
933 // svc=nullptr: no work_finished needed since we never called work_started
934 3x op->svc = nullptr;
935 3x sched_->post(op);
936 3x return;
937 }
938 1557x reg = reg->next_in_set;
939 }
940
941 // No queued signals - wait for delivery
942 1552x impl.waiting_ = true;
943 // svc=this: signal_op::operator() will call work_finished() to balance this
944 1552x op->svc = this;
945 1552x sched_->work_started();
946 1622x }
947 }
948
949 inline void
950 476x posix_signal_service::deliver_signal(int signal_number)
951 {
952 476x if (signal_number < 0 || signal_number >= max_signal_number)
953 ✗ return;
954
955 posix_signal_detail::signal_state* state =
956 476x posix_signal_detail::get_signal_state();
957 476x std::lock_guard lock(state->mutex);
958
959 476x posix_signal_service* service = state->service_list;
960 952x while (service)
961 {
962 476x std::lock_guard svc_lock(service->mutex_);
963
964 476x signal_registration* reg = service->registrations_[signal_number];
965 958x while (reg)
966 {
967 482x posix_signal* impl = static_cast<posix_signal*>(reg->owner);
968
969 482x if (impl->waiting_)
970 {
971 479x impl->waiting_ = false;
972 479x impl->pending_op_.signal_number = signal_number;
973 479x service->post(&impl->pending_op_);
974 }
975 else
976 {
977 3x ++reg->undelivered;
978 }
979
980 482x reg = reg->next_in_table;
981 }
982
983 476x service = service->next_;
984 476x }
985 476x }
986
987 inline void
988 posix_signal_service::work_started() noexcept
989 {
990 sched_->work_started();
991 }
992
993 inline void
994 479x posix_signal_service::work_finished() noexcept
995 {
996 479x sched_->work_finished();
997 479x }
998
999 inline void
1000 479x posix_signal_service::post(signal_op* op)
1001 {
1002 479x sched_->post(op);
1003 479x }
1004
1005 inline void
1006 240x posix_signal_service::add_service(posix_signal_service* service)
1007 {
1008 posix_signal_detail::signal_state* state =
1009 240x posix_signal_detail::get_signal_state();
1010 240x std::lock_guard lock(state->mutex);
1011
1012 240x service->next_ = state->service_list;
1013 240x service->prev_ = nullptr;
1014 240x if (state->service_list)
1015 12x state->service_list->prev_ = service;
1016 240x state->service_list = service;
1017 240x }
1018
1019 inline void
1020 240x posix_signal_service::remove_service(posix_signal_service* service)
1021 {
1022 posix_signal_detail::signal_state* state =
1023 240x posix_signal_detail::get_signal_state();
1024 240x std::lock_guard lock(state->mutex);
1025
1026 240x if (service->next_ || service->prev_ || state->service_list == service)
1027 {
1028 240x if (state->service_list == service)
1029 237x state->service_list = service->next_;
1030 240x if (service->prev_)
1031 3x service->prev_->next_ = service->next_;
1032 240x if (service->next_)
1033 9x service->next_->prev_ = service->prev_;
1034 240x service->next_ = nullptr;
1035 240x service->prev_ = nullptr;
1036 }
1037 240x }
1038
1039 } // namespace detail
1040 } // namespace boost::corosio
1041
1042 #endif // BOOST_COROSIO_POSIX
1043
1044 #endif // BOOST_COROSIO_NATIVE_DETAIL_POSIX_POSIX_SIGNAL_SERVICE_HPP
1045