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