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