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 : 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 44 : auto check = [](std::error_code ec) {
246 44 : if( ec )
247 0 : throw std::system_error(ec);
248 : };
249 36 : check(add(signal));
250 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
|