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

96.3% Lines (26/27) 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 return s_.get().wait(h, ex, token_, &ec_, &signal_number_);
199 }
200 };
201
202 public:
203 struct signal_set_impl : io_object_impl
204 {
205 virtual std::coroutine_handle<> wait(
206 std::coroutine_handle<>,
207 capy::executor_ref,
208 std::stop_token,
209 std::error_code*,
210 int*) = 0;
211
212 virtual std::error_code add(int signal_number, flags_t flags) = 0;
213 virtual std::error_code remove(int signal_number) = 0;
214 virtual std::error_code clear() = 0;
215 virtual void cancel() = 0;
216 };
217
218 /** Destructor.
219
220 Cancels any pending operations and releases signal resources.
221 */
222 ~signal_set();
223
224 /** Construct an empty signal set.
225
226 @param ctx The execution context that will own this signal set.
227 */
228 explicit signal_set(capy::execution_context& ctx);
229
230 /** Construct a signal set with initial signals.
231
232 @param ctx The execution context that will own this signal set.
233 @param signal First signal number to add.
234 @param signals Additional signal numbers to add.
235
236 @throws std::system_error Thrown on failure.
237 */
238 template<std::convertible_to<int>... Signals>
239 36 signal_set(
240 capy::execution_context& ctx,
241 int signal,
242 Signals... signals)
243 36 : signal_set(ctx)
244 {
245 auto check = [](std::error_code ec) {
246 if( ec )
247 throw std::system_error(ec);
248 };
249
2/2
✓ Branch 1 taken 36 times.
✓ Branch 4 taken 36 times.
36 check(add(signal));
250
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)), ...);
251 36 }
252
253 /** Move constructor.
254
255 Transfers ownership of the signal set resources.
256
257 @param other The signal set to move from.
258 */
259 signal_set(signal_set&& other) noexcept;
260
261 /** Move assignment operator.
262
263 Closes any existing signal set and transfers ownership.
264 The source and destination must share the same execution context.
265
266 @param other The signal set to move from.
267
268 @return Reference to this signal set.
269
270 @throws std::logic_error if the signal sets have different
271 execution contexts.
272 */
273 signal_set& operator=(signal_set&& other);
274
275 signal_set(signal_set const&) = delete;
276 signal_set& operator=(signal_set const&) = delete;
277
278 /** Add a signal to the signal set.
279
280 This function adds the specified signal to the set with the
281 specified flags. It has no effect if the signal is already
282 in the set with the same flags.
283
284 If the signal is already registered globally (by another
285 signal_set) and the flags differ, an error is returned
286 unless one of them has the `dont_care` flag.
287
288 @param signal_number The signal to be added to the set.
289 @param flags The flags to apply when registering the signal.
290 On POSIX systems, these map to sigaction() flags.
291 On Windows, flags are accepted but ignored.
292
293 @return Success, or an error if the signal could not be added.
294 Returns `errc::invalid_argument` if the signal is already
295 registered with different flags.
296 */
297 std::error_code add(int signal_number, flags_t flags);
298
299 /** Add a signal to the signal set with default flags.
300
301 This is equivalent to calling `add(signal_number, none)`.
302
303 @param signal_number The signal to be added to the set.
304
305 @return Success, or an error if the signal could not be added.
306 */
307 58 std::error_code add(int signal_number)
308 {
309 58 return add(signal_number, none);
310 }
311
312 /** Remove a signal from the signal set.
313
314 This function removes the specified signal from the set. It has
315 no effect if the signal is not in the set.
316
317 @param signal_number The signal to be removed from the set.
318
319 @return Success, or an error if the signal could not be removed.
320 */
321 std::error_code remove(int signal_number);
322
323 /** Remove all signals from the signal set.
324
325 This function removes all signals from the set. It has no effect
326 if the set is already empty.
327
328 @return Success, or an error if resetting any signal handler fails.
329 */
330 std::error_code clear();
331
332 /** Cancel all operations associated with the signal set.
333
334 This function forces the completion of any pending asynchronous
335 wait operations against the signal set. The handler for each
336 cancelled operation will be invoked with an error code that
337 compares equal to `capy::cond::canceled`.
338
339 Cancellation does not alter the set of registered signals.
340 */
341 void cancel();
342
343 /** Wait for a signal to be delivered.
344
345 The operation supports cancellation via `std::stop_token` through
346 the affine awaitable protocol. If the associated stop token is
347 triggered, the operation completes immediately with an error
348 that compares equal to `capy::cond::canceled`.
349
350 @par Example
351 @code
352 signal_set signals(ctx, SIGINT);
353 auto [ec, signum] = co_await signals.wait();
354 if (ec == capy::cond::canceled)
355 {
356 // Cancelled via stop_token or cancel()
357 co_return;
358 }
359 if (ec)
360 {
361 // Handle other errors
362 co_return;
363 }
364 // Process signal
365 std::cout << "Received signal " << signum << std::endl;
366 @endcode
367
368 @return An awaitable that completes with `io_result<int>`.
369 Returns the signal number when a signal is delivered,
370 or an error code on failure. Compare against error conditions
371 (e.g., `ec == capy::cond::canceled`) rather than error codes.
372 */
373 26 auto wait()
374 {
375 26 return wait_awaitable(*this);
376 }
377
378 private:
379 142 signal_set_impl& get() const noexcept
380 {
381 142 return *static_cast<signal_set_impl*>(impl_);
382 }
383 };
384
385 } // namespace boost::corosio
386
387 #endif
388