Line data 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 26 : if (token_.stop_requested())
188 0 : 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 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 44 : auto check = [](std::error_code ec) {
245 44 : if( ec )
246 0 : throw std::system_error(ec);
247 : };
248 36 : check(add(signal));
249 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
|