libs/corosio/include/boost/corosio/signal_set.hpp

96.4% Lines (27/28) 100.0% Functions (13/13) 88.9% Branches (8/9)
libs/corosio/include/boost/corosio/signal_set.hpp
Line Branch Hits Source Code
1 //
2 // Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com)
3 //
4 // Distributed under the Boost Software License, Version 1.0. (See accompanying
5 // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6 //
7 // Official repository: https://github.com/cppalliance/corosio
8 //
9
10 #ifndef BOOST_COROSIO_SIGNAL_SET_HPP
11 #define BOOST_COROSIO_SIGNAL_SET_HPP
12
13 #include <boost/corosio/detail/config.hpp>
14 #include <boost/corosio/detail/except.hpp>
15 #include <boost/corosio/io_object.hpp>
16 #include <boost/capy/io_result.hpp>
17 #include <boost/capy/error.hpp>
18 #include <boost/capy/ex/executor_ref.hpp>
19 #include <boost/capy/ex/execution_context.hpp>
20 #include <boost/capy/concept/executor.hpp>
21 #include <system_error>
22
23 #include <concepts>
24 #include <coroutine>
25 #include <stop_token>
26 #include <system_error>
27
28 /*
29 Signal Set Public API
30 =====================
31
32 This header provides the public interface for asynchronous signal handling.
33 The implementation is split across platform-specific files:
34 - posix/signals.cpp: Uses sigaction() for robust signal handling
35 - iocp/signals.cpp: Uses C runtime signal() (Windows lacks sigaction)
36
37 Key design decisions:
38
39 1. Abstract flag values: The flags_t enum uses arbitrary bit positions
40 (not SA_RESTART, etc.) to avoid including <signal.h> in public headers.
41 The POSIX implementation maps these to actual SA_* constants internally.
42
43 2. Flag conflict detection: When multiple signal_sets register for the
44 same signal, they must use compatible flags. The first registration
45 establishes the flags; subsequent registrations must match or use
46 dont_care.
47
48 3. Polymorphic implementation: signal_set_impl is an abstract base that
49 platform-specific implementations (posix_signal_impl, win_signal_impl)
50 derive from. This allows the public API to be platform-agnostic.
51
52 4. The inline add(int) overload avoids a virtual call for the common case
53 of adding signals without flags (delegates to add(int, none)).
54 */
55
56 namespace boost::corosio {
57
58 /** An asynchronous signal set for coroutine I/O.
59
60 This class provides the ability to perform an asynchronous wait
61 for one or more signals to occur. The signal set registers for
62 signals using sigaction() on POSIX systems or the C runtime
63 signal() function on Windows.
64
65 @par Thread Safety
66 Distinct objects: Safe.@n
67 Shared objects: Unsafe. A signal_set must not have concurrent
68 wait operations.
69
70 @par Semantics
71 Wraps platform signal handling (sigaction on POSIX, C runtime
72 signal() on Windows). Operations dispatch to OS signal APIs
73 via the io_context reactor.
74
75 @par Supported Signals
76 On Windows, the following signals are supported:
77 SIGINT, SIGTERM, SIGABRT, SIGFPE, SIGILL, SIGSEGV.
78
79 @par Example
80 @code
81 signal_set signals(ctx, SIGINT, SIGTERM);
82 auto [ec, signum] = co_await signals.wait();
83 if (ec == capy::cond::canceled)
84 {
85 // Operation was cancelled via stop_token or cancel()
86 }
87 else if (!ec)
88 {
89 std::cout << "Received signal " << signum << std::endl;
90 }
91 @endcode
92 */
93 class BOOST_COROSIO_DECL signal_set : public io_object
94 {
95 public:
96 /** Flags for signal registration.
97
98 These flags control the behavior of signal handling. Multiple
99 flags can be combined using the bitwise OR operator.
100
101 @note Flags only have effect on POSIX systems. On Windows,
102 only `none` and `dont_care` are supported; other flags return
103 `operation_not_supported`.
104 */
105 enum flags_t : unsigned
106 {
107 /// Use existing flags if signal is already registered.
108 /// When adding a signal that's already registered by another
109 /// signal_set, this flag indicates acceptance of whatever
110 /// flags were used for the existing registration.
111 dont_care = 1u << 16,
112
113 /// No special flags.
114 none = 0,
115
116 /// Restart interrupted system calls.
117 /// Equivalent to SA_RESTART on POSIX systems.
118 restart = 1u << 0,
119
120 /// Don't generate SIGCHLD when children stop.
121 /// Equivalent to SA_NOCLDSTOP on POSIX systems.
122 no_child_stop = 1u << 1,
123
124 /// Don't create zombie processes on child termination.
125 /// Equivalent to SA_NOCLDWAIT on POSIX systems.
126 no_child_wait = 1u << 2,
127
128 /// Don't block the signal while its handler runs.
129 /// Equivalent to SA_NODEFER on POSIX systems.
130 no_defer = 1u << 3,
131
132 /// Reset handler to SIG_DFL after one invocation.
133 /// Equivalent to SA_RESETHAND on POSIX systems.
134 reset_handler = 1u << 4
135 };
136
137 /// Combine two flag values.
138 4 friend constexpr flags_t operator|(flags_t a, flags_t b) noexcept
139 {
140 return static_cast<flags_t>(
141 4 static_cast<unsigned>(a) | static_cast<unsigned>(b));
142 }
143
144 /// Mask two flag values.
145 448 friend constexpr flags_t operator&(flags_t a, flags_t b) noexcept
146 {
147 return static_cast<flags_t>(
148 448 static_cast<unsigned>(a) & static_cast<unsigned>(b));
149 }
150
151 /// Compound assignment OR.
152 2 friend constexpr flags_t& operator|=(flags_t& a, flags_t b) noexcept
153 {
154 2 return a = a | b;
155 }
156
157 /// Compound assignment AND.
158 friend constexpr flags_t& operator&=(flags_t& a, flags_t b) noexcept
159 {
160 return a = a & b;
161 }
162
163 /// Bitwise NOT (complement).
164 friend constexpr flags_t operator~(flags_t a) noexcept
165 {
166 return static_cast<flags_t>(~static_cast<unsigned>(a));
167 }
168
169 private:
170 struct wait_awaitable
171 {
172 signal_set& s_;
173 std::stop_token token_;
174 mutable std::error_code ec_;
175 mutable int signal_number_ = 0;
176
177 26 explicit wait_awaitable(signal_set& s) noexcept : s_(s) {}
178
179 26 bool await_ready() const noexcept
180 {
181 26 return token_.stop_requested();
182 }
183
184 26 capy::io_result<int> await_resume() const noexcept
185 {
186
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 26 times.
26 if (token_.stop_requested())
187 return {capy::error::canceled};
188 26 return {ec_, signal_number_};
189 }
190
191 template<typename Ex>
192 26 auto await_suspend(
193 std::coroutine_handle<> h,
194 Ex const& ex,
195 std::stop_token token) -> std::coroutine_handle<>
196 {
197 26 token_ = std::move(token);
198
1/1
✓ Branch 3 taken 26 times.
26 s_.get().wait(h, ex, token_, &ec_, &signal_number_);
199 26 return std::noop_coroutine();
200 }
201 };
202
203 public:
204 struct signal_set_impl : io_object_impl
205 {
206 virtual void wait(
207 std::coroutine_handle<>,
208 capy::executor_ref,
209 std::stop_token,
210 std::error_code*,
211 int*) = 0;
212
213 virtual std::error_code add(int signal_number, flags_t flags) = 0;
214 virtual std::error_code remove(int signal_number) = 0;
215 virtual std::error_code clear() = 0;
216 virtual void cancel() = 0;
217 };
218
219 /** Destructor.
220
221 Cancels any pending operations and releases signal resources.
222 */
223 ~signal_set();
224
225 /** Construct an empty signal set.
226
227 @param ctx The execution context that will own this signal set.
228 */
229 explicit signal_set(capy::execution_context& ctx);
230
231 /** Construct a signal set with initial signals.
232
233 @param ctx The execution context that will own this signal set.
234 @param signal First signal number to add.
235 @param signals Additional signal numbers to add.
236
237 @throws std::system_error Thrown on failure.
238 */
239 template<std::convertible_to<int>... Signals>
240 36 signal_set(
241 capy::execution_context& ctx,
242 int signal,
243 Signals... signals)
244 36 : signal_set(ctx)
245 {
246 auto check = [](std::error_code ec) {
247 if( ec )
248 throw std::system_error(ec);
249 };
250
2/2
✓ Branch 1 taken 36 times.
✓ Branch 4 taken 36 times.
36 check(add(signal));
251
4/4
✓ Branch 1 taken 6 times.
✓ Branch 4 taken 6 times.
✓ Branch 7 taken 2 times.
✓ Branch 10 taken 2 times.
6 (check(add(signals)), ...);
252 36 }
253
254 /** Move constructor.
255
256 Transfers ownership of the signal set resources.
257
258 @param other The signal set to move from.
259 */
260 signal_set(signal_set&& other) noexcept;
261
262 /** Move assignment operator.
263
264 Closes any existing signal set and transfers ownership.
265 The source and destination must share the same execution context.
266
267 @param other The signal set to move from.
268
269 @return Reference to this signal set.
270
271 @throws std::logic_error if the signal sets have different
272 execution contexts.
273 */
274 signal_set& operator=(signal_set&& other);
275
276 signal_set(signal_set const&) = delete;
277 signal_set& operator=(signal_set const&) = delete;
278
279 /** Add a signal to the signal set.
280
281 This function adds the specified signal to the set with the
282 specified flags. It has no effect if the signal is already
283 in the set with the same flags.
284
285 If the signal is already registered globally (by another
286 signal_set) and the flags differ, an error is returned
287 unless one of them has the `dont_care` flag.
288
289 @param signal_number The signal to be added to the set.
290 @param flags The flags to apply when registering the signal.
291 On POSIX systems, these map to sigaction() flags.
292 On Windows, flags are accepted but ignored.
293
294 @return Success, or an error if the signal could not be added.
295 Returns `errc::invalid_argument` if the signal is already
296 registered with different flags.
297 */
298 std::error_code add(int signal_number, flags_t flags);
299
300 /** Add a signal to the signal set with default flags.
301
302 This is equivalent to calling `add(signal_number, none)`.
303
304 @param signal_number The signal to be added to the set.
305
306 @return Success, or an error if the signal could not be added.
307 */
308 58 std::error_code add(int signal_number)
309 {
310 58 return add(signal_number, none);
311 }
312
313 /** Remove a signal from the signal set.
314
315 This function removes the specified signal from the set. It has
316 no effect if the signal is not in the set.
317
318 @param signal_number The signal to be removed from the set.
319
320 @return Success, or an error if the signal could not be removed.
321 */
322 std::error_code remove(int signal_number);
323
324 /** Remove all signals from the signal set.
325
326 This function removes all signals from the set. It has no effect
327 if the set is already empty.
328
329 @return Success, or an error if resetting any signal handler fails.
330 */
331 std::error_code clear();
332
333 /** Cancel all operations associated with the signal set.
334
335 This function forces the completion of any pending asynchronous
336 wait operations against the signal set. The handler for each
337 cancelled operation will be invoked with an error code that
338 compares equal to `capy::cond::canceled`.
339
340 Cancellation does not alter the set of registered signals.
341 */
342 void cancel();
343
344 /** Wait for a signal to be delivered.
345
346 The operation supports cancellation via `std::stop_token` through
347 the affine awaitable protocol. If the associated stop token is
348 triggered, the operation completes immediately with an error
349 that compares equal to `capy::cond::canceled`.
350
351 @par Example
352 @code
353 signal_set signals(ctx, SIGINT);
354 auto [ec, signum] = co_await signals.wait();
355 if (ec == capy::cond::canceled)
356 {
357 // Cancelled via stop_token or cancel()
358 co_return;
359 }
360 if (ec)
361 {
362 // Handle other errors
363 co_return;
364 }
365 // Process signal
366 std::cout << "Received signal " << signum << std::endl;
367 @endcode
368
369 @return An awaitable that completes with `io_result<int>`.
370 Returns the signal number when a signal is delivered,
371 or an error code on failure. Compare against error conditions
372 (e.g., `ec == capy::cond::canceled`) rather than error codes.
373 */
374 26 auto wait()
375 {
376 26 return wait_awaitable(*this);
377 }
378
379 private:
380 142 signal_set_impl& get() const noexcept
381 {
382 142 return *static_cast<signal_set_impl*>(impl_);
383 }
384 };
385
386 } // namespace boost::corosio
387
388 #endif
389