TLA Line data Source code
1 : //
2 : // Copyright (c) 2026 Michael Vandeberg
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_LOCAL_STREAM_ACCEPTOR_HPP
11 : #define BOOST_COROSIO_LOCAL_STREAM_ACCEPTOR_HPP
12 :
13 : #include <boost/corosio/detail/config.hpp>
14 : #include <boost/corosio/detail/except.hpp>
15 : #include <boost/corosio/detail/op_base.hpp>
16 : #include <boost/corosio/wait_type.hpp>
17 : #include <boost/corosio/io/io_object.hpp>
18 : #include <boost/capy/io_result.hpp>
19 : #include <boost/corosio/local_endpoint.hpp>
20 : #include <boost/corosio/local_stream.hpp>
21 : #include <boost/corosio/local_stream_socket.hpp>
22 : #include <boost/capy/ex/executor_ref.hpp>
23 : #include <boost/capy/ex/execution_context.hpp>
24 : #include <boost/capy/ex/io_env.hpp>
25 : #include <boost/capy/concept/executor.hpp>
26 :
27 : #include <system_error>
28 :
29 : #include <cassert>
30 : #include <concepts>
31 : #include <coroutine>
32 : #include <cstddef>
33 : #include <stop_token>
34 : #include <type_traits>
35 :
36 : namespace boost::corosio {
37 :
38 : /** Options for @ref local_stream_acceptor::bind().
39 :
40 : Controls filesystem cleanup behavior before binding
41 : to a Unix domain socket path.
42 : */
43 : enum class bind_option
44 : {
45 : none,
46 : /// Unlink the socket path before binding (ignored for abstract paths).
47 : unlink_existing
48 : };
49 :
50 : /** An asynchronous Unix domain stream acceptor for coroutine I/O.
51 :
52 : This class provides asynchronous Unix domain stream accept
53 : operations that return awaitable types. The acceptor binds
54 : to a local endpoint (filesystem path or abstract name) and
55 : listens for incoming connections.
56 :
57 : The library does NOT automatically unlink the socket path
58 : on close. Callers are responsible for removing the socket
59 : file before bind (via @ref bind_option::unlink_existing) or
60 : after close.
61 :
62 : @par Thread Safety
63 : Distinct objects: Safe.@n
64 : Shared objects: Unsafe. An acceptor must not have concurrent
65 : accept operations.
66 :
67 : @par Example
68 : @code
69 : io_context ioc;
70 : local_stream_acceptor acc(ioc);
71 : if (auto ec = acc.open())
72 : co_return ec;
73 : if (auto ec = acc.bind(local_endpoint("/tmp/my.sock"),
74 : bind_option::unlink_existing))
75 : co_return ec;
76 : if (auto ec = acc.listen())
77 : co_return ec;
78 : auto [aec, peer] = co_await acc.accept();
79 : @endcode
80 : */
81 : class BOOST_COROSIO_DECL local_stream_acceptor : public io_object
82 : {
83 : struct wait_awaitable
84 : : detail::void_op_base<wait_awaitable>
85 : {
86 : local_stream_acceptor& acc_;
87 : wait_type w_;
88 :
89 HIT 8 : wait_awaitable(local_stream_acceptor& acc, wait_type w) noexcept
90 8 : : acc_(acc), w_(w) {}
91 :
92 6 : std::coroutine_handle<> dispatch(
93 : std::coroutine_handle<> h, capy::executor_ref ex) const
94 : {
95 6 : return acc_.get().wait(h, ex, w_, token_, &ec_);
96 : }
97 : };
98 :
99 : struct move_accept_awaitable
100 : {
101 : local_stream_acceptor& acc_;
102 : std::stop_token token_;
103 : mutable std::error_code ec_;
104 : mutable io_object::implementation* peer_impl_ = nullptr;
105 :
106 4 : explicit move_accept_awaitable(
107 : local_stream_acceptor& acc) noexcept
108 4 : : acc_(acc)
109 : {
110 4 : }
111 :
112 4 : bool await_ready() const noexcept
113 : {
114 : // A pre-set ec_ means the initiator failed before
115 : // dispatch (e.g. a closed object).
116 4 : return static_cast<bool>(ec_) || token_.stop_requested();
117 : }
118 :
119 4 : [[nodiscard]] capy::io_result<local_stream_socket> await_resume() const noexcept
120 : {
121 4 : if (token_.stop_requested())
122 MIS 0 : return {make_error_code(std::errc::operation_canceled),
123 0 : local_stream_socket()};
124 :
125 HIT 4 : if (ec_ || !peer_impl_)
126 2 : return {ec_, local_stream_socket()};
127 :
128 2 : local_stream_socket peer(acc_.ctx_);
129 2 : reset_peer_impl(peer, peer_impl_);
130 2 : return {ec_, std::move(peer)};
131 2 : }
132 :
133 2 : auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env)
134 : -> std::coroutine_handle<>
135 : {
136 2 : token_ = env->stop_token;
137 6 : return acc_.get().accept(
138 6 : h, env->executor, token_, &ec_, &peer_impl_);
139 : }
140 : };
141 :
142 : struct accept_awaitable
143 : {
144 : local_stream_acceptor& acc_;
145 : local_stream_socket& peer_;
146 : std::stop_token token_;
147 : mutable std::error_code ec_;
148 : mutable io_object::implementation* peer_impl_ = nullptr;
149 :
150 29 : accept_awaitable(
151 : local_stream_acceptor& acc, local_stream_socket& peer) noexcept
152 29 : : acc_(acc)
153 29 : , peer_(peer)
154 : {
155 29 : }
156 :
157 29 : bool await_ready() const noexcept
158 : {
159 : // A pre-set ec_ means the initiator failed before
160 : // dispatch (e.g. a closed object).
161 29 : return static_cast<bool>(ec_) || token_.stop_requested();
162 : }
163 :
164 27 : [[nodiscard]] capy::io_result<> await_resume() const noexcept
165 : {
166 27 : if (token_.stop_requested())
167 4 : return {make_error_code(std::errc::operation_canceled)};
168 :
169 23 : if (!ec_ && peer_impl_)
170 17 : peer_.h_.reset(peer_impl_);
171 23 : return {ec_};
172 : }
173 :
174 27 : auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env)
175 : -> std::coroutine_handle<>
176 : {
177 27 : token_ = env->stop_token;
178 81 : return acc_.get().accept(
179 81 : h, env->executor, token_, &ec_, &peer_impl_);
180 : }
181 : };
182 :
183 : public:
184 : /** Destructor.
185 :
186 : Closes the acceptor if open, cancelling any pending operations.
187 : */
188 : ~local_stream_acceptor() override;
189 :
190 : /** Construct an acceptor from an execution context.
191 :
192 : @param ctx The execution context that will own this acceptor.
193 : */
194 : explicit local_stream_acceptor(capy::execution_context& ctx);
195 :
196 : /** Convenience constructor: open + bind + listen.
197 :
198 : Creates a fully-bound listening acceptor in a single
199 : expression, throwing the codes the piecewise `open()` +
200 : `bind()` + `listen()` path returns.
201 :
202 : @param ctx The execution context that will own this acceptor.
203 : @param ep The local endpoint to bind to.
204 : @param backlog The maximum pending connection queue length.
205 :
206 : @throws std::system_error on open, bind, or listen failure.
207 : */
208 : local_stream_acceptor(
209 : capy::execution_context& ctx,
210 : corosio::local_endpoint ep,
211 : int backlog = 128);
212 :
213 : /** Construct an acceptor from an executor.
214 :
215 : The acceptor is associated with the executor's context.
216 :
217 : @param ex The executor whose context will own the acceptor.
218 :
219 : @tparam Ex A type satisfying @ref capy::Executor. Must not
220 : be `local_stream_acceptor` itself (disables implicit
221 : conversion from move).
222 : */
223 : template<class Ex>
224 : requires(!std::same_as<std::remove_cvref_t<Ex>, local_stream_acceptor>) &&
225 : capy::Executor<Ex>
226 : explicit local_stream_acceptor(Ex const& ex) : local_stream_acceptor(ex.context())
227 : {
228 : }
229 :
230 : /** Convenience constructor from an executor.
231 :
232 : @param ex The executor whose context will own the acceptor.
233 : @param ep The local endpoint to bind to.
234 : @param backlog The maximum pending connection queue length.
235 :
236 : @throws std::system_error on open, bind, or listen failure.
237 : */
238 : template<class Ex>
239 : requires capy::Executor<Ex>
240 : local_stream_acceptor(
241 : Ex const& ex, corosio::local_endpoint ep, int backlog = 128)
242 : : local_stream_acceptor(ex.context(), std::move(ep), backlog)
243 : {
244 : }
245 :
246 : /** Move constructor.
247 :
248 : Transfers ownership of the acceptor resources.
249 :
250 : @param other The acceptor to move from.
251 :
252 : @pre No awaitables returned by @p other's methods exist.
253 : @pre The execution context associated with @p other must
254 : outlive this acceptor.
255 : */
256 2 : local_stream_acceptor(local_stream_acceptor&& other) noexcept
257 2 : : local_stream_acceptor(other.ctx_, std::move(other))
258 : {
259 2 : }
260 :
261 : /** Move assignment operator.
262 :
263 : Closes any existing acceptor and transfers ownership.
264 : Both acceptors must share the same execution context.
265 :
266 : @param other The acceptor to move from.
267 :
268 : @return Reference to this acceptor.
269 :
270 : @pre `&ctx_ == &other.ctx_` (same execution context).
271 : @pre No awaitables returned by either `*this` or @p other's
272 : methods exist.
273 : */
274 : local_stream_acceptor& operator=(local_stream_acceptor&& other) noexcept
275 : {
276 : assert(&ctx_ == &other.ctx_ &&
277 : "move-assign requires the same execution_context");
278 : if (this != &other)
279 : {
280 : close();
281 : io_object::operator=(std::move(other));
282 : }
283 : return *this;
284 : }
285 :
286 : local_stream_acceptor(local_stream_acceptor const&) = delete;
287 : local_stream_acceptor& operator=(local_stream_acceptor const&) = delete;
288 :
289 : /** Create the acceptor socket.
290 :
291 : Failures such as descriptor exhaustion are normal runtime
292 : conditions and are reported through the returned error code.
293 :
294 : @param proto The protocol. Defaults to local_stream{}.
295 :
296 : @return The error code, empty on success.
297 : */
298 : [[nodiscard]] std::error_code open(local_stream proto = {}) noexcept;
299 :
300 : /** Bind to a local endpoint.
301 :
302 : @param ep The local endpoint (path) to bind to.
303 : @param opt Bind options. Pass bind_option::unlink_existing
304 : to unlink the socket path before binding (ignored for
305 : abstract sockets and empty endpoints).
306 :
307 : @return An error code on failure, empty on success.
308 :
309 : A closed acceptor reports `errc::bad_file_descriptor`.
310 : */
311 : [[nodiscard]] std::error_code
312 : bind(corosio::local_endpoint ep,
313 : bind_option opt = bind_option::none) noexcept;
314 :
315 : /** Start listening for incoming connections.
316 :
317 : @param backlog The maximum pending connection queue length.
318 :
319 : @return An error code on failure, empty on success.
320 :
321 : A closed acceptor reports `errc::bad_file_descriptor`.
322 : */
323 : [[nodiscard]] std::error_code listen(int backlog = 128) noexcept;
324 :
325 : /** Close the acceptor.
326 :
327 : Cancels any pending accept operations and releases the
328 : underlying socket. Has no effect if the acceptor is not
329 : open.
330 :
331 : @post is_open() == false
332 : */
333 : void close() noexcept;
334 :
335 : /// Check if the acceptor has an open socket handle.
336 421 : bool is_open() const noexcept
337 : {
338 421 : return h_ && get().is_open();
339 : }
340 :
341 : /** Initiate an asynchronous accept into an existing socket.
342 :
343 : Completes when a new connection is available. On success
344 : @p peer is reset to the accepted connection. Only one
345 : accept may be in flight at a time.
346 :
347 : @param peer The socket to receive the accepted connection.
348 :
349 : @par Cancellation
350 : Supports cancellation via stop_token or cancel().
351 : On cancellation, yields `capy::cond::canceled` and
352 : @p peer is not modified.
353 :
354 : @return An awaitable that completes with io_result<>.
355 :
356 : A closed acceptor reports `errc::bad_file_descriptor`.
357 : */
358 29 : [[nodiscard]] auto accept(local_stream_socket& peer)
359 : {
360 29 : accept_awaitable aw(*this, peer);
361 29 : if (!is_open())
362 2 : aw.ec_ = make_error_code(std::errc::bad_file_descriptor);
363 29 : return aw;
364 : }
365 :
366 : /** Wait for an incoming connection or readiness condition.
367 :
368 : Suspends until the listen socket is ready in the
369 : requested direction. For `wait_type::read`, completion
370 : signals that a subsequent @ref accept will succeed
371 : without blocking; a connection already queued when the
372 : wait begins completes it immediately. No connection is
373 : consumed.
374 :
375 : @note `wait_type::write` is not usable on an acceptor:
376 : writability carries no meaning for a listening socket, so
377 : the wait fails with `errc::operation_not_supported` on
378 : every backend.
379 :
380 : @param w The wait direction.
381 :
382 : @return An awaitable that completes with `io_result<>`.
383 :
384 : A closed acceptor completes with `errc::bad_file_descriptor`.
385 :
386 : @par Preconditions
387 : This acceptor must outlive the returned awaitable.
388 : */
389 8 : [[nodiscard]] auto wait(wait_type w)
390 : {
391 8 : wait_awaitable aw(*this, w);
392 8 : if (!is_open())
393 2 : aw.ec_ = make_error_code(std::errc::bad_file_descriptor);
394 8 : return aw;
395 : }
396 :
397 : /** Initiate an asynchronous accept, returning the socket.
398 :
399 : Completes when a new connection is available. Only one
400 : accept may be in flight at a time.
401 :
402 : @par Cancellation
403 : Supports cancellation via stop_token or cancel().
404 : On cancellation, yields `capy::cond::canceled` with
405 : a default-constructed socket.
406 :
407 : @return An awaitable that completes with
408 : io_result<local_stream_socket>.
409 :
410 : A closed acceptor reports `errc::bad_file_descriptor`.
411 : On failure the returned socket is default-constructed and
412 : may only be destroyed or assigned.
413 : */
414 4 : [[nodiscard]] auto accept()
415 : {
416 4 : move_accept_awaitable aw(*this);
417 4 : if (!is_open())
418 2 : aw.ec_ = make_error_code(std::errc::bad_file_descriptor);
419 4 : return aw;
420 : }
421 :
422 : /** Cancel pending asynchronous accept operations.
423 :
424 : Outstanding accept operations complete with
425 : @c capy::cond::canceled. Safe to call when no
426 : operations are pending (no-op).
427 : */
428 : void cancel() noexcept;
429 :
430 : /** Release ownership of the native socket handle.
431 :
432 : Deregisters the acceptor from the reactor and cancels
433 : pending operations without closing the descriptor. The
434 : caller takes ownership of the returned handle.
435 :
436 : @return The native handle.
437 :
438 : @throws std::system_error `errc::bad_file_descriptor` if the
439 : acceptor is not open.
440 :
441 : @post is_open() == false
442 : */
443 : native_handle_type release();
444 :
445 : /** Get the native socket handle.
446 :
447 : @return The native socket handle, or -1/INVALID_SOCKET if not
448 : open.
449 :
450 : @par Preconditions
451 : None. May be called on closed acceptors.
452 : */
453 : native_handle_type native_handle() const noexcept;
454 :
455 : /** Assign an existing native socket to this acceptor.
456 :
457 : Adopts a listening socket created outside the library —
458 : received from a service manager, inherited, or made natively —
459 : and registers it with the backend. The socket must be a
460 : listening stream socket in the local IPC family. Adoption
461 : never alters the descriptor's flags or options: on POSIX the
462 : fd must already be non-blocking, and on Windows the socket
463 : must be overlapped-capable.
464 :
465 : Adoption does not verify listen state; @ref accept reports the
466 : error if the socket is not listening.
467 :
468 : If this object is already open, pending operations complete
469 : with `errc::operation_canceled` and the held socket is closed
470 : before the new one is adopted.
471 :
472 : @par Exception Safety
473 : Strong guarantee on validation failure: the object is
474 : unchanged. If backend registration fails, the object either
475 : retains its previous socket or is left closed, depending on
476 : the backend. In all failure cases the caller retains
477 : ownership of `fd`.
478 :
479 : @param fd The native socket to adopt. On success the object
480 : owns it and will close it.
481 :
482 : @return The error code, empty on success. Validation and
483 : registration failures are normal runtime conditions when
484 : adopting foreign descriptors.
485 : */
486 : [[nodiscard]] std::error_code assign(native_handle_type fd) noexcept;
487 :
488 : /** Return the local endpoint the acceptor is bound to.
489 :
490 : Returns a default-constructed (empty) endpoint if the
491 : acceptor is not open or not yet bound. Safe to call in
492 : any state.
493 : */
494 : corosio::local_endpoint local_endpoint() const noexcept;
495 :
496 : /** Set a socket option on the acceptor.
497 :
498 : Applies a type-safe socket option to the underlying socket.
499 : The option type encodes the protocol level and option name.
500 :
501 : @param opt The option to set.
502 :
503 : @tparam Option A socket option type providing static
504 : `level()` and `name()` members, and `data()` / `size()`
505 : accessors.
506 :
507 : @throws std::system_error `errc::bad_file_descriptor` if the
508 : acceptor is not open; otherwise thrown on failure.
509 : */
510 : template<class Option>
511 6 : void set_option(Option const& opt)
512 : {
513 6 : if (!is_open())
514 2 : detail::throw_system_error(
515 4 : make_error_code(std::errc::bad_file_descriptor),
516 : "local_stream_acceptor::set_option");
517 4 : std::error_code ec = get().set_option(
518 : Option::level(), Option::name(), opt.data(), opt.size());
519 4 : if (ec)
520 2 : detail::throw_system_error(ec, "local_stream_acceptor::set_option");
521 2 : }
522 :
523 : /** Get a socket option from the acceptor.
524 :
525 : Retrieves the current value of a type-safe socket option.
526 :
527 : @return The current option value.
528 :
529 : @tparam Option A socket option type providing static
530 : `level()` and `name()` members, and `data()` / `size()`
531 : / `resize()` members.
532 :
533 : @throws std::system_error `errc::bad_file_descriptor` if the
534 : acceptor is not open; otherwise thrown on failure.
535 : */
536 : template<class Option>
537 6 : Option get_option() const
538 : {
539 6 : if (!is_open())
540 2 : detail::throw_system_error(
541 4 : make_error_code(std::errc::bad_file_descriptor),
542 : "local_stream_acceptor::get_option");
543 4 : Option opt{};
544 4 : std::size_t sz = opt.size();
545 : std::error_code ec =
546 4 : get().get_option(Option::level(), Option::name(), opt.data(), &sz);
547 4 : if (ec)
548 2 : detail::throw_system_error(ec, "local_stream_acceptor::get_option");
549 2 : opt.resize(sz);
550 2 : return opt;
551 : }
552 :
553 : /** Backend hooks for local stream acceptor operations.
554 :
555 : Platform backends derive from this to implement
556 : accept, option, and lifecycle management.
557 : */
558 : struct implementation : io_object::implementation
559 : {
560 : /** Initiate an asynchronous accept.
561 :
562 : On completion the backend sets @p *ec and, on
563 : success, stores a pointer to the new socket
564 : implementation in @p *impl_out.
565 :
566 : @param h Coroutine handle to resume.
567 : @param ex Executor for dispatching the completion.
568 : @param token Stop token for cancellation.
569 : @param ec Output error code.
570 : @param impl_out Output pointer for the accepted socket.
571 : @return Coroutine handle to resume immediately.
572 : */
573 : virtual std::coroutine_handle<> accept(
574 : std::coroutine_handle<>,
575 : capy::executor_ref,
576 : std::stop_token,
577 : std::error_code*,
578 : io_object::implementation**) = 0;
579 :
580 : /** Initiate an asynchronous wait for acceptor readiness.
581 :
582 : Completes when the listen socket becomes ready for
583 : the specified direction. No connection is consumed.
584 : */
585 : virtual std::coroutine_handle<> wait(
586 : std::coroutine_handle<> h,
587 : capy::executor_ref ex,
588 : wait_type w,
589 : std::stop_token token,
590 : std::error_code* ec) = 0;
591 :
592 : /// Return the cached local endpoint.
593 : virtual corosio::local_endpoint local_endpoint() const noexcept = 0;
594 :
595 : /// Return whether the underlying socket is open.
596 : virtual bool is_open() const noexcept = 0;
597 :
598 : /// Return the native handle, or the platform sentinel if closed.
599 : virtual native_handle_type native_handle() const noexcept = 0;
600 :
601 : /// Release and return the native handle without closing.
602 : virtual native_handle_type release_socket() noexcept = 0;
603 :
604 : /// Cancel pending accept operations.
605 : virtual void cancel() noexcept = 0;
606 :
607 : /// Set a raw socket option.
608 : virtual std::error_code set_option(
609 : int level,
610 : int optname,
611 : void const* data,
612 : std::size_t size) noexcept = 0;
613 :
614 : /// Get a raw socket option.
615 : virtual std::error_code
616 : get_option(int level, int optname, void* data, std::size_t* size)
617 : const noexcept = 0;
618 : };
619 :
620 : protected:
621 16 : local_stream_acceptor(handle h, capy::execution_context& ctx) noexcept
622 16 : : io_object(std::move(h))
623 16 : , ctx_(ctx)
624 : {
625 16 : }
626 :
627 2 : local_stream_acceptor(
628 : capy::execution_context& ctx, local_stream_acceptor&& other) noexcept
629 2 : : io_object(std::move(other))
630 2 : , ctx_(ctx)
631 : {
632 2 : }
633 :
634 8 : static void reset_peer_impl(
635 : local_stream_socket& peer, io_object::implementation* impl) noexcept
636 : {
637 8 : if (impl)
638 8 : peer.h_.reset(impl);
639 8 : }
640 :
641 : private:
642 : capy::execution_context& ctx_;
643 :
644 494 : inline implementation& get() const noexcept
645 : {
646 494 : return *static_cast<implementation*>(h_.get());
647 : }
648 : };
649 :
650 : } // namespace boost::corosio
651 :
652 : #endif // BOOST_COROSIO_LOCAL_STREAM_ACCEPTOR_HPP
|