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 186x 100.0% 100.0% boost::corosio::detail::posix_signal_service::reset_token_cancel(boost::corosio::detail::posix_signal&) :210 1051x 100.0% 100.0% boost::corosio::detail::posix_signal_detail::flags_supported(boost::corosio::signal_set::flags_t) :287 207x 100.0% 100.0% boost::corosio::detail::posix_signal_detail::to_sigaction_flags(boost::corosio::signal_set::flags_t) :299 159x 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 39x 100.0% 100.0% boost::corosio::detail::posix_signal_detail::open_signal_pipe(boost::corosio::detail::posix_signal_detail::signal_state*) :339 207x 100.0% 97.0% boost::corosio::detail::posix_signal_detail::corosio_posix_signal_handler(int) :373 317x 100.0% 100.0% boost::corosio::detail::posix_signal_detail::drain_signal_pipe() :389 317x 100.0% 100.0% boost::corosio::detail::signal_op::operator()() :405 321x 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 192x 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 1121x 100.0% 100.0% boost::corosio::detail::posix_signal::add(int, boost::corosio::signal_set::flags_t) :484 211x 100.0% 100.0% boost::corosio::detail::posix_signal::remove(int) :490 26x 100.0% 100.0% boost::corosio::detail::posix_signal::clear() :496 200x 100.0% 100.0% boost::corosio::detail::posix_signal::cancel() :502 203x 100.0% 100.0% boost::corosio::detail::posix_signal_service::posix_signal_service(boost::capy::execution_context&) :509 165x 100.0% 80.0% boost::corosio::detail::posix_signal_service::~posix_signal_service() :521 330x 100.0% 100.0% boost::corosio::detail::posix_signal_service::shutdown() :527 165x 100.0% 91.0% boost::corosio::detail::posix_signal_service::construct() :591 192x 100.0% 100.0% boost::corosio::detail::posix_signal_service::destroy_impl(boost::corosio::detail::posix_signal&) :604 186x 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 211x 98.2% 84.0% boost::corosio::detail::posix_signal_service::remove_signal(boost::corosio::detail::posix_signal&, int) :725 26x 100.0% 95.0% boost::corosio::detail::posix_signal_service::clear_signals(boost::corosio::detail::posix_signal&) :778 200x 100.0% 95.0% boost::corosio::detail::posix_signal_service::cancel_wait(boost::corosio::detail::posix_signal&) :826 203x 100.0% 100.0% boost::corosio::detail::posix_signal_service::cancel_wait_token(boost::corosio::detail::posix_signal&) :855 717x 100.0% 100.0% boost::corosio::detail::posix_signal::token_canceller::operator()() const :887 717x 100.0% 100.0% boost::corosio::detail::posix_signal_service::start_wait(boost::corosio::detail::posix_signal&, boost::corosio::detail::signal_op*) :893 1051x 100.0% 94.0% boost::corosio::detail::posix_signal_service::deliver_signal(int) :950 317x 95.0% 76.0% boost::corosio::detail::posix_signal_service::work_finished() :994 319x 100.0% 100.0% boost::corosio::detail::posix_signal_service::post(boost::corosio::detail::signal_op*) :1000 319x 100.0% 100.0% boost::corosio::detail::posix_signal_service::add_service(boost::corosio::detail::posix_signal_service*) :1006 165x 100.0% 100.0% boost::corosio::detail::posix_signal_service::remove_service(boost::corosio::detail::posix_signal_service*) :1020 165x 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 186x void destroy(io_object::implementation* p) override
169 {
170 186x auto& impl = static_cast<posix_signal&>(*p);
171 186x [[maybe_unused]] auto n = impl.clear();
172 186x impl.disarm_stop();
173 186x impl.cancel();
174 186x destroy_impl(impl);
175 186x }
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 1051x void reset_token_cancel(posix_signal& impl) noexcept
211 {
212 1051x std::lock_guard lock(mutex_);
213 1051x impl.token_cancelled_ = false;
214 1051x }
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 207x 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 207x return true;
294 }
295
296 // Map abstract flags to sigaction() flags.
297 // Caller must ensure flags_supported() returns true first.
298 inline int
299 159x to_sigaction_flags(signal_set::flags_t flags)
300 {
301 159x int sa_flags = 0;
302 159x if (flags & signal_set::restart)
303 23x sa_flags |= SA_RESTART;
304 159x if (flags & signal_set::no_child_stop)
305 3x sa_flags |= SA_NOCLDSTOP;
306 #ifdef SA_NOCLDWAIT
307 159x if (flags & signal_set::no_child_wait)
308 2x sa_flags |= SA_NOCLDWAIT;
309 #endif
310 159x if (flags & signal_set::no_defer)
311 4x sa_flags |= SA_NODEFER;
312 159x if (flags & signal_set::reset_handler)
313 2x sa_flags |= SA_RESETHAND;
314 159x return sa_flags;
315 }
316
317 // Check if two flag values are compatible
318 inline bool
319 39x flags_compatible(signal_set::flags_t existing, signal_set::flags_t requested)
320 {
321 // dont_care is always compatible
322 76x if ((existing & signal_set::dont_care) ||
323 37x (requested & signal_set::dont_care))
324 7x return true;
325
326 // Mask out dont_care bit for comparison
327 32x constexpr auto mask = ~signal_set::dont_care;
328 32x 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 207x open_signal_pipe(signal_state* state)
340 {
341 207x if (state->read_fd >= 0)
342 193x return {};
343
344 int fds[2];
345 14x if (::pipe(fds) < 0)
346 1x return make_err(errno);
347
348 30x for (int i = 0; i < 2; ++i)
349 {
350 23x int fl = ::fcntl(fds[i], F_GETFL, 0);
351 42x if (fl == -1 || ::fcntl(fds[i], F_SETFL, fl | O_NONBLOCK) == -1 ||
352 19x ::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 7x state->read_fd = fds[0];
362 7x state->write_fd = fds[1];
363 7x 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 317x corosio_posix_signal_handler(int signal_number)
374 {
375 317x int saved_errno = errno;
376 317x signal_state* state = get_signal_state();
377 [[maybe_unused]] ssize_t r =
378 317x ::write(state->write_fd, &signal_number, sizeof(int));
379 317x errno = saved_errno;
380 // With sigaction(), the handler persists automatically (unlike some
381 // signal() implementations that reset to SIG_DFL).
382 317x }
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 317x drain_signal_pipe()
390 {
391 317x signal_state* state = get_signal_state();
392 int signal_number;
393 634x while (::read(state->read_fd, &signal_number, sizeof(int)) ==
394 static_cast<ssize_t>(sizeof(int)))
395 {
396 317x posix_signal_service::deliver_signal(signal_number);
397 }
398 317x }
399
400 } // namespace posix_signal_detail
401
402 // signal_op implementation
403
404 inline void
405 321x signal_op::operator()()
406 {
407 321x if (ec_out)
408 321x *ec_out = {};
409 321x if (signal_out)
410 321x *signal_out = signal_number;
411
412 // Capture svc before resuming (coro may destroy us)
413 321x auto* service = svc;
414 321x svc = nullptr;
415
416 321x cont.h = h;
417 321x d.post(cont);
418
419 // Balance the work_started() from start_wait
420 321x if (service)
421 319x service->work_finished();
422 321x }
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 192x inline posix_signal::posix_signal(posix_signal_service& svc) noexcept
433 192x : svc_(svc)
434 {
435 192x }
436
437 inline std::coroutine_handle<>
438 1121x 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 1121x pending_op_.h = h;
446 1121x pending_op_.d = d;
447 1121x pending_op_.ec_out = ec;
448 1121x pending_op_.signal_out = signal_out;
449 1121x 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 1121x stop_cb_.reset();
458
459 1121x if (token.stop_requested())
460 {
461 70x if (ec)
462 70x *ec = make_error_code(capy::error::canceled);
463 70x if (signal_out)
464 70x *signal_out = 0;
465 70x pending_op_.cont.h = h;
466 70x d.post(pending_op_.cont);
467 // completion is always posted to scheduler queue, never inline.
468 70x 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 1051x svc_.reset_token_cancel(*this);
475 1051x if (token.stop_possible())
476 721x stop_cb_.emplace(token, token_canceller{this});
477
478 1051x svc_.start_wait(*this, &pending_op_);
479 // completion is always posted to scheduler queue, never inline.
480 1051x return std::noop_coroutine();
481 }
482
483 inline std::error_code
484 211x posix_signal::add(int signal_number, signal_set::flags_t flags)
485 {
486 211x return svc_.add_signal(*this, signal_number, flags);
487 }
488
489 inline std::error_code
490 26x posix_signal::remove(int signal_number)
491 {
492 26x return svc_.remove_signal(*this, signal_number);
493 }
494
495 inline std::error_code
496 200x posix_signal::clear()
497 {
498 200x return svc_.clear_signals(*this);
499 }
500
501 inline void
502 203x posix_signal::cancel() noexcept
503 {
504 203x svc_.cancel_wait(*this);
505 203x }
506
507 // posix_signal_service implementation
508
509 165x inline posix_signal_service::posix_signal_service(
510 165x capy::execution_context& ctx)
511 165x : sched_(&get_scheduler(ctx))
512 {
513 10725x for (int i = 0; i < max_signal_number; ++i)
514 {
515 10560x registrations_[i] = nullptr;
516 10560x registration_count_[i] = 0;
517 }
518 165x add_service(this);
519 165x }
520
521 330x inline posix_signal_service::~posix_signal_service()
522 {
523 165x remove_service(this);
524 330x }
525
526 inline void
527 165x 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 165x intrusive_list<posix_signal> doomed;
535
536 {
537 posix_signal_detail::signal_state* state =
538 165x posix_signal_detail::get_signal_state();
539 165x std::lock_guard state_lock(state->mutex);
540 165x std::lock_guard lock(mutex_);
541
542 171x for (auto* impl = impl_list_.pop_front(); impl != nullptr;
543 6x impl = impl_list_.pop_front())
544 {
545 12x while (auto* reg = impl->signals_)
546 {
547 6x 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 6x if (state->registration_count[signal_number] == 1)
556 {
557 4x struct sigaction sa = {};
558 4x sa.sa_handler = SIG_DFL;
559 4x sigemptyset(&sa.sa_mask);
560 4x sa.sa_flags = 0;
561 4x std::ignore = ::sigaction(signal_number, &sa, nullptr);
562 4x state->registered_flags[signal_number] = signal_set::none;
563 }
564
565 6x --state->registration_count[signal_number];
566 6x --registration_count_[signal_number];
567
568 6x impl->signals_ = reg->next_in_set;
569 6x delete reg;
570 6x }
571 6x 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 10725x for (int i = 0; i < max_signal_number; ++i)
580 10560x registrations_[i] = nullptr;
581 165x }
582
583 171x for (auto* impl = doomed.pop_front(); impl != nullptr;
584 6x impl = doomed.pop_front())
585 {
586 6x delete impl;
587 }
588 165x }
589
590 inline io_object::implementation*
591 192x posix_signal_service::construct()
592 {
593 192x auto* impl = new posix_signal(*this);
594
595 {
596 192x std::lock_guard lock(mutex_);
597 192x impl_list_.push_back(impl);
598 192x }
599
600 192x return impl;
601 }
602
603 inline void
604 186x posix_signal_service::destroy_impl(posix_signal& impl)
605 {
606 {
607 186x std::lock_guard lock(mutex_);
608 186x impl_list_.remove(&impl);
609 186x }
610
611 186x delete &impl;
612 186x }
613
614 inline std::error_code
615 211x posix_signal_service::add_signal(
616 posix_signal& impl, int signal_number, signal_set::flags_t flags)
617 {
618 211x if (signal_number < 0 || signal_number >= max_signal_number)
619 4x 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 207x 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 207x 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 207x std::lock_guard state_lock(state->mutex);
639 207x if (auto ec = posix_signal_detail::open_signal_pipe(state))
640 7x return ec;
641 207x }
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 200x std::lock_guard reg_lock(reader_mutex_);
647 200x if (!reader_registered_)
648 {
649 137x if (auto ec = sched_->register_signal_reader(state->read_fd))
650 2x return ec;
651 135x reader_registered_ = true;
652 }
653 200x }
654
655 198x std::lock_guard state_lock(state->mutex);
656 198x std::lock_guard lock(mutex_);
657
658 // Find insertion point (list is sorted by signal number)
659 198x signal_registration** insertion_point = &impl.signals_;
660 198x signal_registration* reg = impl.signals_;
661 221x while (reg && reg->signal_number < signal_number)
662 {
663 23x insertion_point = &reg->next_in_set;
664 23x 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 198x if (reg && reg->signal_number == signal_number)
670 {
671 13x if (!posix_signal_detail::flags_compatible(reg->flags, flags))
672 4x return make_error_code(std::errc::invalid_argument);
673 9x return {};
674 }
675
676 // Check flag compatibility with global registration
677 // (different signal_set already registered this signal with different flags)
678 185x if (state->registration_count[signal_number] > 0)
679 {
680 26x if (!posix_signal_detail::flags_compatible(
681 state->registered_flags[signal_number], flags))
682 2x return make_error_code(std::errc::invalid_argument);
683 }
684
685 183x auto* new_reg = new signal_registration;
686 183x new_reg->signal_number = signal_number;
687 183x new_reg->flags = flags;
688 183x new_reg->owner = &impl;
689 183x new_reg->undelivered = 0;
690
691 // Install signal handler on first global registration
692 183x if (state->registration_count[signal_number] == 0)
693 {
694 159x struct sigaction sa = {};
695 159x sa.sa_handler = posix_signal_detail::corosio_posix_signal_handler;
696 159x sigemptyset(&sa.sa_mask);
697 159x sa.sa_flags = posix_signal_detail::to_sigaction_flags(flags);
698
699 159x 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 158x state->registered_flags[signal_number] = flags;
707 }
708
709 182x new_reg->next_in_set = reg;
710 182x *insertion_point = new_reg;
711
712 182x new_reg->next_in_table = registrations_[signal_number];
713 182x new_reg->prev_in_table = nullptr;
714 182x if (registrations_[signal_number])
715 18x registrations_[signal_number]->prev_in_table = new_reg;
716 182x registrations_[signal_number] = new_reg;
717
718 182x ++state->registration_count[signal_number];
719 182x ++registration_count_[signal_number];
720
721 182x return {};
722 198x }
723
724 inline std::error_code
725 26x posix_signal_service::remove_signal(posix_signal& impl, int signal_number)
726 {
727 26x if (signal_number < 0 || signal_number >= max_signal_number)
728 2x return make_error_code(std::errc::invalid_argument);
729
730 posix_signal_detail::signal_state* state =
731 24x posix_signal_detail::get_signal_state();
732 24x std::lock_guard state_lock(state->mutex);
733 24x std::lock_guard lock(mutex_);
734
735 24x signal_registration** deletion_point = &impl.signals_;
736 24x signal_registration* reg = impl.signals_;
737 26x while (reg && reg->signal_number < signal_number)
738 {
739 2x deletion_point = &reg->next_in_set;
740 2x reg = reg->next_in_set;
741 }
742
743 24x if (!reg || reg->signal_number != signal_number)
744 3x return {};
745
746 // Restore default handler on last global unregistration
747 21x if (state->registration_count[signal_number] == 1)
748 {
749 17x struct sigaction sa = {};
750 17x sa.sa_handler = SIG_DFL;
751 17x sigemptyset(&sa.sa_mask);
752 17x sa.sa_flags = 0;
753
754 17x if (::sigaction(signal_number, &sa, nullptr) < 0)
755 1x return make_error_code(std::errc::invalid_argument);
756
757 // Clear stored flags
758 16x state->registered_flags[signal_number] = signal_set::none;
759 }
760
761 20x *deletion_point = reg->next_in_set;
762
763 20x if (registrations_[signal_number] == reg)
764 18x registrations_[signal_number] = reg->next_in_table;
765 20x if (reg->prev_in_table)
766 2x reg->prev_in_table->next_in_table = reg->next_in_table;
767 20x if (reg->next_in_table)
768 2x reg->next_in_table->prev_in_table = reg->prev_in_table;
769
770 20x --state->registration_count[signal_number];
771 20x --registration_count_[signal_number];
772
773 20x delete reg;
774 20x return {};
775 24x }
776
777 inline std::error_code
778 200x posix_signal_service::clear_signals(posix_signal& impl)
779 {
780 posix_signal_detail::signal_state* state =
781 200x posix_signal_detail::get_signal_state();
782 200x std::lock_guard state_lock(state->mutex);
783 200x std::lock_guard lock(mutex_);
784
785 200x std::error_code first_error;
786
787 356x while (signal_registration* reg = impl.signals_)
788 {
789 156x int signal_number = reg->signal_number;
790
791 156x if (state->registration_count[signal_number] == 1)
792 {
793 138x struct sigaction sa = {};
794 138x sa.sa_handler = SIG_DFL;
795 138x sigemptyset(&sa.sa_mask);
796 138x sa.sa_flags = 0;
797
798 138x 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 138x state->registered_flags[signal_number] = signal_set::none;
803 }
804
805 156x impl.signals_ = reg->next_in_set;
806
807 156x if (registrations_[signal_number] == reg)
808 154x registrations_[signal_number] = reg->next_in_table;
809 156x if (reg->prev_in_table)
810 2x reg->prev_in_table->next_in_table = reg->next_in_table;
811 156x if (reg->next_in_table)
812 12x reg->next_in_table->prev_in_table = reg->prev_in_table;
813
814 156x --state->registration_count[signal_number];
815 156x --registration_count_[signal_number];
816
817 156x delete reg;
818 156x }
819
820 200x if (first_error)
821 1x return first_error;
822 199x return {};
823 200x }
824
825 inline void
826 203x posix_signal_service::cancel_wait(posix_signal& impl)
827 {
828 203x bool was_waiting = false;
829 203x signal_op* op = nullptr;
830
831 {
832 203x std::lock_guard lock(mutex_);
833 203x impl.cancelled_ = true;
834 203x if (impl.waiting_)
835 {
836 7x was_waiting = true;
837 7x impl.waiting_ = false;
838 7x op = &impl.pending_op_;
839 }
840 203x }
841
842 203x if (was_waiting)
843 {
844 7x if (op->ec_out)
845 7x *op->ec_out = make_error_code(capy::error::canceled);
846 7x if (op->signal_out)
847 7x *op->signal_out = 0;
848 7x op->cont.h = op->h;
849 7x op->d.post(op->cont);
850 7x sched_->work_finished();
851 }
852 203x }
853
854 inline void
855 717x posix_signal_service::cancel_wait_token(posix_signal& impl) noexcept
856 {
857 717x bool was_waiting = false;
858 717x signal_op* op = nullptr;
859
860 {
861 717x 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 717x impl.token_cancelled_ = true;
866 717x if (impl.waiting_)
867 {
868 650x was_waiting = true;
869 650x impl.waiting_ = false;
870 650x op = &impl.pending_op_;
871 }
872 717x }
873
874 717x if (was_waiting)
875 {
876 650x if (op->ec_out)
877 650x *op->ec_out = make_error_code(capy::error::canceled);
878 650x if (op->signal_out)
879 650x *op->signal_out = 0;
880 650x op->cont.h = op->h;
881 650x op->d.post(op->cont);
882 650x sched_->work_finished();
883 }
884 717x }
885
886 inline void
887 717x posix_signal::token_canceller::operator()() const noexcept
888 {
889 717x self->svc_.cancel_wait_token(*self);
890 717x }
891
892 inline void
893 1051x posix_signal_service::start_wait(posix_signal& impl, signal_op* op)
894 {
895 {
896 1051x std::lock_guard lock(mutex_);
897
898 // Check if cancel() was called before this wait started
899 1051x if (impl.cancelled_)
900 {
901 2x impl.cancelled_ = false;
902 2x if (op->ec_out)
903 2x *op->ec_out = make_error_code(capy::error::canceled);
904 2x if (op->signal_out)
905 2x *op->signal_out = 0;
906 2x op->cont.h = op->h;
907 2x op->d.post(op->cont);
908 2x 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 1049x if (impl.token_cancelled_)
914 {
915 65x impl.token_cancelled_ = false;
916 65x if (op->ec_out)
917 65x *op->ec_out = make_error_code(capy::error::canceled);
918 65x if (op->signal_out)
919 65x *op->signal_out = 0;
920 65x op->cont.h = op->h;
921 65x op->d.post(op->cont);
922 65x return;
923 }
924
925 // Check for queued signals first (signal arrived before wait started)
926 984x signal_registration* reg = impl.signals_;
927 1970x while (reg)
928 {
929 988x if (reg->undelivered > 0)
930 {
931 2x --reg->undelivered;
932 2x op->signal_number = reg->signal_number;
933 // svc=nullptr: no work_finished needed since we never called work_started
934 2x op->svc = nullptr;
935 2x sched_->post(op);
936 2x return;
937 }
938 986x reg = reg->next_in_set;
939 }
940
941 // No queued signals - wait for delivery
942 982x impl.waiting_ = true;
943 // svc=this: signal_op::operator() will call work_finished() to balance this
944 982x op->svc = this;
945 982x sched_->work_started();
946 1051x }
947 }
948
949 inline void
950 317x posix_signal_service::deliver_signal(int signal_number)
951 {
952 317x if (signal_number < 0 || signal_number >= max_signal_number)
953 ✗ return;
954
955 posix_signal_detail::signal_state* state =
956 317x posix_signal_detail::get_signal_state();
957 317x std::lock_guard lock(state->mutex);
958
959 317x posix_signal_service* service = state->service_list;
960 634x while (service)
961 {
962 317x std::lock_guard svc_lock(service->mutex_);
963
964 317x signal_registration* reg = service->registrations_[signal_number];
965 638x while (reg)
966 {
967 321x posix_signal* impl = static_cast<posix_signal*>(reg->owner);
968
969 321x if (impl->waiting_)
970 {
971 319x impl->waiting_ = false;
972 319x impl->pending_op_.signal_number = signal_number;
973 319x service->post(&impl->pending_op_);
974 }
975 else
976 {
977 2x ++reg->undelivered;
978 }
979
980 321x reg = reg->next_in_table;
981 }
982
983 317x service = service->next_;
984 317x }
985 317x }
986
987 inline void
988 posix_signal_service::work_started() noexcept
989 {
990 sched_->work_started();
991 }
992
993 inline void
994 319x posix_signal_service::work_finished() noexcept
995 {
996 319x sched_->work_finished();
997 319x }
998
999 inline void
1000 319x posix_signal_service::post(signal_op* op)
1001 {
1002 319x sched_->post(op);
1003 319x }
1004
1005 inline void
1006 165x posix_signal_service::add_service(posix_signal_service* service)
1007 {
1008 posix_signal_detail::signal_state* state =
1009 165x posix_signal_detail::get_signal_state();
1010 165x std::lock_guard lock(state->mutex);
1011
1012 165x service->next_ = state->service_list;
1013 165x service->prev_ = nullptr;
1014 165x if (state->service_list)
1015 8x state->service_list->prev_ = service;
1016 165x state->service_list = service;
1017 165x }
1018
1019 inline void
1020 165x posix_signal_service::remove_service(posix_signal_service* service)
1021 {
1022 posix_signal_detail::signal_state* state =
1023 165x posix_signal_detail::get_signal_state();
1024 165x std::lock_guard lock(state->mutex);
1025
1026 165x if (service->next_ || service->prev_ || state->service_list == service)
1027 {
1028 165x if (state->service_list == service)
1029 163x state->service_list = service->next_;
1030 165x if (service->prev_)
1031 2x service->prev_->next_ = service->next_;
1032 165x if (service->next_)
1033 6x service->next_->prev_ = service->prev_;
1034 165x service->next_ = nullptr;
1035 165x service->prev_ = nullptr;
1036 }
1037 165x }
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