include/boost/corosio/signal_set.hpp

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