diff --git a/doc/modules/ROOT/pages/guide/endpoints.adoc b/doc/modules/ROOT/pages/guide/endpoints.adoc index 51f807a1..4cedd0da 100644 --- a/doc/modules/ROOT/pages/guide/endpoints.adoc +++ b/doc/modules/ROOT/pages/guide/endpoints.adoc @@ -202,7 +202,8 @@ auto [ec] = co_await s.connect(target); [source,cpp] ---- corosio::tcp_acceptor acc(ioc); -acc.listen(corosio::endpoint(8080)); // Bind to all interfaces +if (auto ec = acc.listen(corosio::endpoint(8080))) // Bind to all interfaces + return ec; ---- == From Resolver Results diff --git a/doc/modules/ROOT/pages/guide/signals.adoc b/doc/modules/ROOT/pages/guide/signals.adoc index 38327556..1365ed5c 100644 --- a/doc/modules/ROOT/pages/guide/signals.adoc +++ b/doc/modules/ROOT/pages/guide/signals.adoc @@ -1,429 +1,430 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -= Signal Handling - -The `signal_set` class provides asynchronous signal handling. It allows -coroutines to wait for operating system signals like SIGINT (Ctrl+C) or -SIGTERM. - -NOTE: Code snippets assume: -[source,cpp] ----- -#include -#include - -namespace corosio = boost::corosio; ----- - -== Overview - -[source,cpp] ----- -corosio::signal_set signals(ioc, SIGINT, SIGTERM); - -auto [ec, signum] = co_await signals.wait(); -if (!ec) - std::cout << "Received signal " << signum << "\n"; ----- - -== Construction - -=== Empty Signal Set - -[source,cpp] ----- -corosio::signal_set signals(ioc); -signals.add(SIGINT); -signals.add(SIGTERM); ----- - -=== With Initial Signals - -[source,cpp] ----- -// One signal -corosio::signal_set s1(ioc, SIGINT); - -// Two signals -corosio::signal_set s2(ioc, SIGINT, SIGTERM); - -// Three signals -corosio::signal_set s3(ioc, SIGINT, SIGTERM, SIGHUP); ----- - -== Supported Signals - -=== Windows - -On Windows, the following signals are supported: - -[cols="1,2"] -|=== -| Signal | Description - -| `SIGINT` -| Interrupt (Ctrl+C) - -| `SIGTERM` -| Termination request - -| `SIGABRT` -| Abnormal termination - -| `SIGFPE` -| Floating-point exception - -| `SIGILL` -| Illegal instruction - -| `SIGSEGV` -| Segmentation violation -|=== - -=== POSIX - -On POSIX systems, all standard signals are supported. - -== Managing Signals - -=== add() - -Add a signal to the set: - -[source,cpp] ----- -signals.add(SIGUSR1); ----- - -Adding a signal that's already in the set has no effect. - -==== Signal Flags (POSIX) - -On POSIX systems, you can specify signal flags when adding a signal: - -[source,cpp] ----- -using flags = corosio::signal_set; - -// Restart interrupted system calls automatically -signals.add(SIGCHLD, flags::restart); - -// Multiple flags can be combined -signals.add(SIGCHLD, flags::restart | flags::no_child_stop); ----- - -Available flags: - -[cols="1,2"] -|=== -| Flag | Description - -| `none` -| No special flags (default) - -| `restart` -| Automatically restart interrupted system calls (SA_RESTART) - -| `no_child_stop` -| Don't generate SIGCHLD when children stop (SA_NOCLDSTOP) - -| `no_child_wait` -| Don't create zombie processes on child termination (SA_NOCLDWAIT) - -| `no_defer` -| Don't block the signal while its handler runs (SA_NODEFER) - -| `reset_handler` -| Reset handler to SIG_DFL after one invocation (SA_RESETHAND) - -| `dont_care` -| Accept existing flags if signal is already registered -|=== - -NOTE: On Windows, only `none` and `dont_care` flags are supported. On some POSIX -systems, `no_child_wait` may not be available. Using unsupported flags returns -`operation_not_supported`. - -==== Flag Compatibility - -When multiple `signal_set` objects register for the same signal, they must use -compatible flags: - -[source,cpp] ----- -corosio::signal_set s1(ioc); -corosio::signal_set s2(ioc); - -s1.add(SIGINT, flags::restart); // OK - first registration -s2.add(SIGINT, flags::restart); // OK - same flags -s2.add(SIGINT, flags::no_defer); // Error! - different flags - -// Use dont_care to accept existing flags -s2.add(SIGINT, flags::dont_care); // OK - accepts existing flags ----- - -=== remove() - -Remove a signal from the set: - -[source,cpp] ----- -signals.remove(SIGINT); - -// With error code -boost::system::error_code ec; -signals.remove(SIGINT, ec); ----- - -Removing a signal that's not in the set has no effect. - -=== clear() - -Remove all signals from the set: - -[source,cpp] ----- -signals.clear(); - -// With error code -boost::system::error_code ec; -signals.clear(ec); ----- - -== Waiting for Signals - -The `wait()` operation waits for any signal in the set: - -[source,cpp] ----- -auto [ec, signum] = co_await signals.wait(); - -if (!ec) -{ - switch (signum) - { - case SIGINT: - std::cout << "Interrupt received\n"; - break; - case SIGTERM: - std::cout << "Termination requested\n"; - break; - } -} ----- - -== Cancellation - -=== cancel() - -Cancel pending wait operations: - -[source,cpp] ----- -signals.cancel(); ----- - -The wait completes with `capy::error::canceled`: - -[source,cpp] ----- -auto [ec, signum] = co_await signals.wait(); -if (ec == capy::error::canceled) - std::cout << "Wait was cancelled\n"; ----- - -Cancellation does NOT remove signals from the set. The signal set remains -configured and can be waited on again. - -=== Stop Token Cancellation - -Signal waits support stop token cancellation through the affine protocol. - -== Use Cases - -=== Graceful Shutdown - -[source,cpp] ----- -capy::task shutdown_handler( - corosio::io_context& ioc, - std::atomic& running) -{ - corosio::signal_set signals(ioc, SIGINT, SIGTERM); - - auto [ec, signum] = co_await signals.wait(); - if (!ec) - { - std::cout << "Shutdown signal received\n"; - running = false; - ioc.stop(); - } -} ----- - -=== Multiple Signal Waits - -You can wait for signals multiple times: - -[source,cpp] ----- -capy::task signal_loop(corosio::io_context& ioc) -{ - corosio::signal_set signals(ioc, SIGUSR1); - - for (;;) - { - auto [ec, signum] = co_await signals.wait(); - if (ec) - break; - - std::cout << "Received USR1, doing work...\n"; - // Handle signal - } -} ----- - -=== Reload Configuration - -[source,cpp] ----- -capy::task config_reloader( - corosio::io_context& ioc, - Config& config) -{ - corosio::signal_set signals(ioc, SIGHUP); - - for (;;) - { - auto [ec, signum] = co_await signals.wait(); - if (ec) - break; - - std::cout << "Reloading configuration...\n"; - config.reload(); - } -} ----- - -=== Child Process Management (POSIX) - -[source,cpp] ----- -capy::task child_reaper(corosio::io_context& ioc) -{ - using flags = corosio::signal_set; - - corosio::signal_set signals(ioc); - - // Only notify on child termination, not stop/continue - // Prevent zombie processes automatically - signals.add(SIGCHLD, flags::no_child_stop | flags::no_child_wait); - - for (;;) - { - auto [ec, signum] = co_await signals.wait(); - if (ec) - break; - - // With no_child_wait, children are reaped automatically - std::cout << "Child process terminated\n"; - } -} ----- - -== Move Semantics - -Signal sets are move-only: - -[source,cpp] ----- -corosio::signal_set s1(ioc, SIGINT); -corosio::signal_set s2 = std::move(s1); // OK - -corosio::signal_set s3 = s2; // Error: deleted copy constructor ----- - -IMPORTANT: Source and destination must share the same execution context. - -== Thread Safety - -[cols="1,2"] -|=== -| Operation | Thread Safety - -| Distinct signal_sets -| Safe from different threads - -| Same signal_set -| NOT safe for concurrent operations -|=== - -Don't call `wait()`, `add()`, `remove()`, `clear()`, or `cancel()` -concurrently on the same signal_set. - -== Example: Server with Graceful Shutdown - -[source,cpp] ----- -capy::task run_server(corosio::io_context& ioc) -{ - std::atomic running{true}; - - // Start signal handler - capy::run_async(ioc.get_executor())( - [](corosio::io_context& ioc, std::atomic& running) - -> capy::task - { - corosio::signal_set signals(ioc, SIGINT, SIGTERM); - co_await signals.wait(); - running = false; - ioc.stop(); - }(ioc, running)); - - // Accept loop - corosio::acceptor acc(ioc); - acc.listen(corosio::endpoint(8080)); - - while (running) - { - corosio::tcp_socket peer(ioc); - auto [ec] = co_await acc.accept(peer); - if (ec) - break; - - // Handle connection... - } -} ----- - -== Platform Notes - -=== Windows - -Windows has limited signal support. The library uses `signal()` from the -C runtime for compatibility. Only `none` and `dont_care` flags are supported; -other flags return `operation_not_supported`. - -=== POSIX - -On POSIX systems, signals are handled using `sigaction()` which provides: - -* Reliable signal delivery (handler doesn't reset to SIG_DFL) -* Support for signal flags (SA_RESTART, SA_NOCLDSTOP, etc.) -* Proper signal masking during handler execution - -The `restart` flag is particularly useful—without it, blocking calls like -`read()` can fail with `EINTR` when a signal arrives. - -== Next Steps - -* xref:timers.adoc[Timers] — Timed operations -* xref:io-context.adoc[I/O Context] — The event loop -* xref:../tutorials/echo-server.adoc[Echo Server Tutorial] — Server example +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + += Signal Handling + +The `signal_set` class provides asynchronous signal handling. It allows +coroutines to wait for operating system signals like SIGINT (Ctrl+C) or +SIGTERM. + +NOTE: Code snippets assume: +[source,cpp] +---- +#include +#include + +namespace corosio = boost::corosio; +---- + +== Overview + +[source,cpp] +---- +corosio::signal_set signals(ioc, SIGINT, SIGTERM); + +auto [ec, signum] = co_await signals.wait(); +if (!ec) + std::cout << "Received signal " << signum << "\n"; +---- + +== Construction + +=== Empty Signal Set + +[source,cpp] +---- +corosio::signal_set signals(ioc); +signals.add(SIGINT); +signals.add(SIGTERM); +---- + +=== With Initial Signals + +[source,cpp] +---- +// One signal +corosio::signal_set s1(ioc, SIGINT); + +// Two signals +corosio::signal_set s2(ioc, SIGINT, SIGTERM); + +// Three signals +corosio::signal_set s3(ioc, SIGINT, SIGTERM, SIGHUP); +---- + +== Supported Signals + +=== Windows + +On Windows, the following signals are supported: + +[cols="1,2"] +|=== +| Signal | Description + +| `SIGINT` +| Interrupt (Ctrl+C) + +| `SIGTERM` +| Termination request + +| `SIGABRT` +| Abnormal termination + +| `SIGFPE` +| Floating-point exception + +| `SIGILL` +| Illegal instruction + +| `SIGSEGV` +| Segmentation violation +|=== + +=== POSIX + +On POSIX systems, all standard signals are supported. + +== Managing Signals + +=== add() + +Add a signal to the set: + +[source,cpp] +---- +signals.add(SIGUSR1); +---- + +Adding a signal that's already in the set has no effect. + +==== Signal Flags (POSIX) + +On POSIX systems, you can specify signal flags when adding a signal: + +[source,cpp] +---- +using flags = corosio::signal_set; + +// Restart interrupted system calls automatically +signals.add(SIGCHLD, flags::restart); + +// Multiple flags can be combined +signals.add(SIGCHLD, flags::restart | flags::no_child_stop); +---- + +Available flags: + +[cols="1,2"] +|=== +| Flag | Description + +| `none` +| No special flags (default) + +| `restart` +| Automatically restart interrupted system calls (SA_RESTART) + +| `no_child_stop` +| Don't generate SIGCHLD when children stop (SA_NOCLDSTOP) + +| `no_child_wait` +| Don't create zombie processes on child termination (SA_NOCLDWAIT) + +| `no_defer` +| Don't block the signal while its handler runs (SA_NODEFER) + +| `reset_handler` +| Reset handler to SIG_DFL after one invocation (SA_RESETHAND) + +| `dont_care` +| Accept existing flags if signal is already registered +|=== + +NOTE: On Windows, only `none` and `dont_care` flags are supported. On some POSIX +systems, `no_child_wait` may not be available. Using unsupported flags returns +`operation_not_supported`. + +==== Flag Compatibility + +When multiple `signal_set` objects register for the same signal, they must use +compatible flags: + +[source,cpp] +---- +corosio::signal_set s1(ioc); +corosio::signal_set s2(ioc); + +s1.add(SIGINT, flags::restart); // OK - first registration +s2.add(SIGINT, flags::restart); // OK - same flags +s2.add(SIGINT, flags::no_defer); // Error! - different flags + +// Use dont_care to accept existing flags +s2.add(SIGINT, flags::dont_care); // OK - accepts existing flags +---- + +=== remove() + +Remove a signal from the set: + +[source,cpp] +---- +signals.remove(SIGINT); + +// With error code +boost::system::error_code ec; +signals.remove(SIGINT, ec); +---- + +Removing a signal that's not in the set has no effect. + +=== clear() + +Remove all signals from the set: + +[source,cpp] +---- +signals.clear(); + +// With error code +boost::system::error_code ec; +signals.clear(ec); +---- + +== Waiting for Signals + +The `wait()` operation waits for any signal in the set: + +[source,cpp] +---- +auto [ec, signum] = co_await signals.wait(); + +if (!ec) +{ + switch (signum) + { + case SIGINT: + std::cout << "Interrupt received\n"; + break; + case SIGTERM: + std::cout << "Termination requested\n"; + break; + } +} +---- + +== Cancellation + +=== cancel() + +Cancel pending wait operations: + +[source,cpp] +---- +signals.cancel(); +---- + +The wait completes with `capy::error::canceled`: + +[source,cpp] +---- +auto [ec, signum] = co_await signals.wait(); +if (ec == capy::error::canceled) + std::cout << "Wait was cancelled\n"; +---- + +Cancellation does NOT remove signals from the set. The signal set remains +configured and can be waited on again. + +=== Stop Token Cancellation + +Signal waits support stop token cancellation through the affine protocol. + +== Use Cases + +=== Graceful Shutdown + +[source,cpp] +---- +capy::task shutdown_handler( + corosio::io_context& ioc, + std::atomic& running) +{ + corosio::signal_set signals(ioc, SIGINT, SIGTERM); + + auto [ec, signum] = co_await signals.wait(); + if (!ec) + { + std::cout << "Shutdown signal received\n"; + running = false; + ioc.stop(); + } +} +---- + +=== Multiple Signal Waits + +You can wait for signals multiple times: + +[source,cpp] +---- +capy::task signal_loop(corosio::io_context& ioc) +{ + corosio::signal_set signals(ioc, SIGUSR1); + + for (;;) + { + auto [ec, signum] = co_await signals.wait(); + if (ec) + break; + + std::cout << "Received USR1, doing work...\n"; + // Handle signal + } +} +---- + +=== Reload Configuration + +[source,cpp] +---- +capy::task config_reloader( + corosio::io_context& ioc, + Config& config) +{ + corosio::signal_set signals(ioc, SIGHUP); + + for (;;) + { + auto [ec, signum] = co_await signals.wait(); + if (ec) + break; + + std::cout << "Reloading configuration...\n"; + config.reload(); + } +} +---- + +=== Child Process Management (POSIX) + +[source,cpp] +---- +capy::task child_reaper(corosio::io_context& ioc) +{ + using flags = corosio::signal_set; + + corosio::signal_set signals(ioc); + + // Only notify on child termination, not stop/continue + // Prevent zombie processes automatically + signals.add(SIGCHLD, flags::no_child_stop | flags::no_child_wait); + + for (;;) + { + auto [ec, signum] = co_await signals.wait(); + if (ec) + break; + + // With no_child_wait, children are reaped automatically + std::cout << "Child process terminated\n"; + } +} +---- + +== Move Semantics + +Signal sets are move-only: + +[source,cpp] +---- +corosio::signal_set s1(ioc, SIGINT); +corosio::signal_set s2 = std::move(s1); // OK + +corosio::signal_set s3 = s2; // Error: deleted copy constructor +---- + +IMPORTANT: Source and destination must share the same execution context. + +== Thread Safety + +[cols="1,2"] +|=== +| Operation | Thread Safety + +| Distinct signal_sets +| Safe from different threads + +| Same signal_set +| NOT safe for concurrent operations +|=== + +Don't call `wait()`, `add()`, `remove()`, `clear()`, or `cancel()` +concurrently on the same signal_set. + +== Example: Server with Graceful Shutdown + +[source,cpp] +---- +capy::task run_server(corosio::io_context& ioc) +{ + std::atomic running{true}; + + // Start signal handler + capy::run_async(ioc.get_executor())( + [](corosio::io_context& ioc, std::atomic& running) + -> capy::task + { + corosio::signal_set signals(ioc, SIGINT, SIGTERM); + co_await signals.wait(); + running = false; + ioc.stop(); + }(ioc, running)); + + // Accept loop + corosio::acceptor acc(ioc); + if (auto ec = acc.listen(corosio::endpoint(8080))) + co_return; + + while (running) + { + corosio::tcp_socket peer(ioc); + auto [ec] = co_await acc.accept(peer); + if (ec) + break; + + // Handle connection... + } +} +---- + +== Platform Notes + +=== Windows + +Windows has limited signal support. The library uses `signal()` from the +C runtime for compatibility. Only `none` and `dont_care` flags are supported; +other flags return `operation_not_supported`. + +=== POSIX + +On POSIX systems, signals are handled using `sigaction()` which provides: + +* Reliable signal delivery (handler doesn't reset to SIG_DFL) +* Support for signal flags (SA_RESTART, SA_NOCLDSTOP, etc.) +* Proper signal masking during handler execution + +The `restart` flag is particularly useful—without it, blocking calls like +`read()` can fail with `EINTR` when a signal arrives. + +== Next Steps + +* xref:timers.adoc[Timers] — Timed operations +* xref:io-context.adoc[I/O Context] — The event loop +* xref:../tutorials/echo-server.adoc[Echo Server Tutorial] — Server example diff --git a/doc/modules/ROOT/pages/guide/tcp_acceptor.adoc b/doc/modules/ROOT/pages/guide/tcp_acceptor.adoc index 4eaa6c7f..0ec70be0 100644 --- a/doc/modules/ROOT/pages/guide/tcp_acceptor.adoc +++ b/doc/modules/ROOT/pages/guide/tcp_acceptor.adoc @@ -29,7 +29,8 @@ An tcp_acceptor binds to a local endpoint and waits for clients to connect: [source,cpp] ---- corosio::tcp_acceptor acc(ioc); -acc.listen(corosio::endpoint(8080)); // Listen on port 8080 +if (auto ec = acc.listen(corosio::endpoint(8080))) // Listen on port 8080 + return ec; corosio::tcp_socket peer(ioc); auto [ec] = co_await acc.accept(peer); @@ -65,7 +66,11 @@ listening for connections: [source,cpp] ---- -acc.listen(corosio::endpoint(8080)); +if (auto ec = acc.listen(corosio::endpoint(8080))) +{ + std::cerr << "Listen failed: " << ec.message() << "\n"; + return ec; +} ---- This performs three operations: @@ -74,13 +79,14 @@ This performs three operations: 2. Binds to the specified endpoint 3. Marks the socket as passive (listening) -Throws `std::system_error` on failure. +Returns a `std::error_code` indicating success or failure. The return value +is marked `[[nodiscard]]` to prevent accidentally ignoring errors. === Parameters [source,cpp] ---- -void listen(endpoint ep, int backlog = 128); +[[nodiscard]] std::error_code listen(endpoint ep, int backlog = 128); ---- The `backlog` parameter specifies the maximum queue length for pending @@ -94,7 +100,8 @@ To accept connections on any network interface: [source,cpp] ---- // Port only - binds to 0.0.0.0 (all IPv4 interfaces) -acc.listen(corosio::endpoint(8080)); +if (auto ec = acc.listen(corosio::endpoint(8080))) + return ec; ---- === Binding to a Specific Interface @@ -104,8 +111,9 @@ To accept connections only on a specific interface: [source,cpp] ---- // Localhost only -acc.listen(corosio::endpoint( - boost::urls::ipv4_address::loopback(), 8080)); +if (auto ec = acc.listen(corosio::endpoint( + boost::urls::ipv4_address::loopback(), 8080))) + return ec; ---- == Accepting Connections @@ -281,7 +289,11 @@ Coordinate shutdown with signal handling: capy::task run_server(corosio::io_context& ioc) { corosio::tcp_acceptor acc(ioc); - acc.listen(corosio::endpoint(8080)); + if (auto ec = acc.listen(corosio::endpoint(8080))) + { + std::cerr << "Listen failed: " << ec.message() << "\n"; + co_return; + } corosio::signal_set signals(ioc, SIGINT, SIGTERM); diff --git a/doc/modules/ROOT/pages/guide/tls.adoc b/doc/modules/ROOT/pages/guide/tls.adoc index b53d1e6f..c5269897 100644 --- a/doc/modules/ROOT/pages/guide/tls.adoc +++ b/doc/modules/ROOT/pages/guide/tls.adoc @@ -552,7 +552,8 @@ capy::task tls_server( // Set up acceptor corosio::acceptor acc(ioc); - acc.listen(corosio::endpoint(port)); + if (auto ec = acc.listen(corosio::endpoint(port))) + co_return; for (;;) { diff --git a/include/boost/corosio/tcp_acceptor.hpp b/include/boost/corosio/tcp_acceptor.hpp index c150c429..80e75aa6 100644 --- a/include/boost/corosio/tcp_acceptor.hpp +++ b/include/boost/corosio/tcp_acceptor.hpp @@ -53,7 +53,8 @@ namespace boost::corosio { @code io_context ioc; tcp_acceptor acc(ioc); - acc.listen(endpoint(8080)); // Bind to port 8080 + if (auto ec = acc.listen(endpoint(8080))) // Bind to port 8080 + return ec; tcp_socket peer(ioc); auto [ec] = co_await acc.accept(peer); @@ -198,11 +199,23 @@ class BOOST_COROSIO_DECL tcp_acceptor : public io_object bind to all interfaces on a specific port. @param backlog The maximum length of the queue of pending - connections. Defaults to a reasonable system value. + connections. Defaults to 128. - @throws std::system_error on failure. + @return An error code indicating success or the reason for failure. + A default-constructed error code indicates success. + + @par Error Conditions + @li `errc::address_in_use`: The endpoint is already in use. + @li `errc::address_not_available`: The address is not available + on any local interface. + @li `errc::permission_denied`: Insufficient privileges to bind + to the endpoint (e.g., privileged port). + @li `errc::operation_not_supported`: The acceptor service is + unavailable in the context (POSIX only). + + @throws Nothing. */ - void listen(endpoint ep, int backlog = 128); + [[nodiscard]] std::error_code listen(endpoint ep, int backlog = 128); /** Close the acceptor. diff --git a/src/corosio/src/tcp_acceptor.cpp b/src/corosio/src/tcp_acceptor.cpp index 615b96bd..727370ee 100644 --- a/src/corosio/src/tcp_acceptor.cpp +++ b/src/corosio/src/tcp_acceptor.cpp @@ -34,36 +34,41 @@ tcp_acceptor( { } -void +std::error_code tcp_acceptor:: listen(endpoint ep, int backlog) { if (impl_) close(); + std::error_code ec; + #if BOOST_COROSIO_HAS_IOCP auto& svc = ctx_->use_service(); auto& wrapper = svc.create_acceptor_impl(); impl_ = &wrapper; - std::error_code ec = svc.open_acceptor( - *wrapper.get_internal(), ep, backlog); + ec = svc.open_acceptor(*wrapper.get_internal(), ep, backlog); #else // POSIX backends use abstract acceptor_service for runtime polymorphism. // The concrete service (epoll_sockets or select_sockets) must be installed // by the context constructor before any acceptor operations. auto* svc = ctx_->find_service(); if (!svc) - detail::throw_logic_error("tcp_acceptor::listen: no acceptor service installed"); + { + // Should not happen with properly constructed io_context + return make_error_code(std::errc::operation_not_supported); + } auto& wrapper = svc->create_acceptor_impl(); impl_ = &wrapper; - std::error_code ec = svc->open_acceptor(wrapper, ep, backlog); + ec = svc->open_acceptor(wrapper, ep, backlog); #endif + // Both branches above define 'wrapper' as a reference to the impl if (ec) { wrapper.release(); impl_ = nullptr; - detail::throw_system_error(ec, "tcp_acceptor::listen"); } + return ec; } void diff --git a/src/corosio/src/tcp_server.cpp b/src/corosio/src/tcp_server.cpp index 86ccd42c..467d2f47 100644 --- a/src/corosio/src/tcp_server.cpp +++ b/src/corosio/src/tcp_server.cpp @@ -93,9 +93,10 @@ std::error_code tcp_server::bind(endpoint ep) { impl_->ports.emplace_back(impl_->ctx); - // VFALCO this should return error_code - impl_->ports.back().listen(ep); - return {}; + auto ec = impl_->ports.back().listen(ep); + if (ec) + impl_->ports.pop_back(); + return ec; } void diff --git a/src/corosio/src/test/mocket.cpp b/src/corosio/src/test/mocket.cpp index 86155c53..e5f838de 100644 --- a/src/corosio/src/test/mocket.cpp +++ b/src/corosio/src/test/mocket.cpp @@ -152,7 +152,9 @@ make_mocket_pair( // Use ephemeral port (0) - OS assigns an available port tcp_acceptor acc(ctx); - acc.listen(endpoint(ipv4_address::loopback(), 0)); + auto listen_ec = acc.listen(endpoint(ipv4_address::loopback(), 0)); + if (listen_ec) + throw std::runtime_error("mocket listen failed: " + listen_ec.message()); auto port = acc.local_endpoint().port(); // Open peer socket for connect diff --git a/src/corosio/src/test/socket_pair.cpp b/src/corosio/src/test/socket_pair.cpp index 2e5de093..fff5f7d7 100644 --- a/src/corosio/src/test/socket_pair.cpp +++ b/src/corosio/src/test/socket_pair.cpp @@ -32,7 +32,8 @@ make_socket_pair(basic_io_context& ctx) // Use ephemeral port (0) - OS assigns an available port tcp_acceptor acc(ctx); - acc.listen(endpoint(ipv4_address::loopback(), 0)); + if (auto ec = acc.listen(endpoint(ipv4_address::loopback(), 0))) + throw std::runtime_error("socket_pair listen failed: " + ec.message()); auto port = acc.local_endpoint().port(); tcp_socket s1(ctx); diff --git a/test/unit/acceptor.cpp b/test/unit/acceptor.cpp index 25c0901d..7c6c5aec 100644 --- a/test/unit/acceptor.cpp +++ b/test/unit/acceptor.cpp @@ -53,7 +53,8 @@ struct acceptor_test_impl tcp_acceptor acc(ioc); // Listen on a port - acc.listen(endpoint(0)); // Port 0 = ephemeral port + auto ec = acc.listen(endpoint(0)); // Port 0 = ephemeral port + BOOST_TEST(!ec); BOOST_TEST_EQ(acc.is_open(), true); // Close it @@ -66,7 +67,8 @@ struct acceptor_test_impl { Context ioc; tcp_acceptor acc1(ioc); - acc1.listen(endpoint(0)); + auto ec = acc1.listen(endpoint(0)); + BOOST_TEST(!ec); BOOST_TEST_EQ(acc1.is_open(), true); // Move construct @@ -83,7 +85,8 @@ struct acceptor_test_impl Context ioc; tcp_acceptor acc1(ioc); tcp_acceptor acc2(ioc); - acc1.listen(endpoint(0)); + auto ec = acc1.listen(endpoint(0)); + BOOST_TEST(!ec); BOOST_TEST_EQ(acc1.is_open(), true); BOOST_TEST_EQ(acc2.is_open(), false); @@ -107,7 +110,8 @@ struct acceptor_test_impl // acceptor impl alive until IOCP delivers the cancellation. Context ioc; tcp_acceptor acc(ioc); - acc.listen(endpoint(0)); + auto ec = acc.listen(endpoint(0)); + BOOST_TEST(!ec); // These must outlive the coroutines bool accept_done = false; @@ -158,7 +162,8 @@ struct acceptor_test_impl // The acceptor_ptr shared_ptr in accept_op ensures this. Context ioc; tcp_acceptor acc(ioc); - acc.listen(endpoint(0)); + auto ec = acc.listen(endpoint(0)); + BOOST_TEST(!ec); tcp_socket peer(ioc); bool accept_done = false; diff --git a/test/unit/socket.cpp b/test/unit/socket.cpp index 70e370c4..ea265d7f 100644 --- a/test/unit/socket.cpp +++ b/test/unit/socket.cpp @@ -86,17 +86,13 @@ make_socket_pair_t(Context& ctx) for (int attempt = 0; attempt < 20; ++attempt) { port = get_socket_test_port(); - try + if (!acc.listen(endpoint(ipv4_address::loopback(), port))) { - acc.listen(endpoint(ipv4_address::loopback(), port)); listening = true; break; } - catch (const std::system_error&) - { - acc.close(); - acc = tcp_acceptor(ctx); - } + acc.close(); + acc = tcp_acceptor(ctx); } if (!listening) throw std::runtime_error("socket_pair: failed to find available port"); @@ -1202,7 +1198,8 @@ struct socket_test_impl tcp_acceptor acc(ioc); // Bind to loopback with port 0 (ephemeral) - acc.listen(endpoint(ipv4_address::loopback(), 0)); + auto listen_ec = acc.listen(endpoint(ipv4_address::loopback(), 0)); + BOOST_TEST(!listen_ec); // Acceptor's local endpoint should have a non-zero OS-assigned port auto acc_local = acc.local_endpoint(); @@ -1272,18 +1269,14 @@ struct socket_test_impl bool found = false; for (int attempt = 0; attempt < 100; ++attempt) { - try + if (!acc.listen(endpoint(ipv4_address::loopback(), test_port))) { - acc.listen(endpoint(ipv4_address::loopback(), test_port)); found = true; break; } - catch (const std::system_error&) - { - acc.close(); - acc = tcp_acceptor(ioc); - test_port += fast_rand(); - } + acc.close(); + acc = tcp_acceptor(ioc); + test_port += fast_rand(); } if (!found) { diff --git a/test/unit/socket_stress.cpp b/test/unit/socket_stress.cpp index 65e067f3..047d6f37 100644 --- a/test/unit/socket_stress.cpp +++ b/test/unit/socket_stress.cpp @@ -100,17 +100,13 @@ make_stress_pair(Context& ctx) for (int attempt = 0; attempt < 50; ++attempt) { port = get_stress_port(); - try + if (!acc.listen(endpoint(ipv4_address::loopback(), port))) { - acc.listen(endpoint(ipv4_address::loopback(), port)); listening = true; break; } - catch (const std::system_error&) - { - acc.close(); - acc = tcp_acceptor(ctx); - } + acc.close(); + acc = tcp_acceptor(ctx); } if (!listening) throw std::runtime_error("stress_pair: failed to find available port"); @@ -663,17 +659,13 @@ struct accept_stress_test_impl for (int attempt = 0; attempt < 50; ++attempt) { port = get_stress_port(); - try + if (!acc.listen(endpoint(ipv4_address::loopback(), port))) { - acc.listen(endpoint(ipv4_address::loopback(), port)); listening = true; break; } - catch (const std::system_error&) - { - acc.close(); - acc = tcp_acceptor(ioc); - } + acc.close(); + acc = tcp_acceptor(ioc); } if (!listening) { diff --git a/test/unit/tcp_server.cpp b/test/unit/tcp_server.cpp index 2e63b7e2..e72be9ae 100644 --- a/test/unit/tcp_server.cpp +++ b/test/unit/tcp_server.cpp @@ -133,16 +133,10 @@ struct tcp_server_test for(int attempt = 0; attempt < 20; ++attempt) { port = static_cast(49152 + (attempt * 7) % 16383); - try - { - acc.listen(endpoint(ipv4_address::loopback(), port)); + if (!acc.listen(endpoint(ipv4_address::loopback(), port))) break; - } - catch(std::system_error const&) - { - acc.close(); - acc = tcp_acceptor(ioc); - } + acc.close(); + acc = tcp_acceptor(ioc); } acc.close(); @@ -284,16 +278,10 @@ struct tcp_server_test for(int attempt = 0; attempt < 20; ++attempt) { port = static_cast(49152 + (attempt * 7) % 16383); - try - { - acc.listen(endpoint(ipv4_address::loopback(), port)); + if (!acc.listen(endpoint(ipv4_address::loopback(), port))) break; - } - catch(std::system_error const&) - { - acc.close(); - acc = tcp_acceptor(ioc); - } + acc.close(); + acc = tcp_acceptor(ioc); } acc.close(); @@ -436,6 +424,81 @@ struct tcp_server_test srv.join(); } + void + testListenErrorCode() + { + io_context ioc; + + // Test success case + tcp_acceptor acc1(ioc); + auto ec1 = acc1.listen(endpoint(ipv4_address::loopback(), 0)); + BOOST_TEST(!ec1); + BOOST_TEST(acc1.is_open()); + auto port = acc1.local_endpoint().port(); + BOOST_TEST(port != 0); + + // Test with explicit backlog + tcp_acceptor acc2(ioc); + auto ec2 = acc2.listen(endpoint(ipv4_address::loopback(), 0), 64); + BOOST_TEST(!ec2); + BOOST_TEST(acc2.is_open()); + BOOST_TEST(acc2.local_endpoint().port() != 0); + } + + void + testBindSuccess() + { + io_context ioc; + + // Test that tcp_server::bind returns no error and doesn't throw + test_server srv(ioc); + auto ec = srv.bind(endpoint(ipv4_address::loopback(), 0)); + BOOST_TEST(!ec); + } + + void + testListenErrorNonLocalAddress() + { + io_context ioc; + + // Binding to a non-local IP address should fail with + // "can't assign requested address" (EADDRNOTAVAIL) on all platforms. + // 192.0.2.1 is from TEST-NET-1 (RFC 5737), reserved for documentation + // and never assigned to real interfaces. + tcp_acceptor acc(ioc); + auto ec = acc.listen(endpoint(ipv4_address({192, 0, 2, 1}), 0)); + BOOST_TEST(ec); + BOOST_TEST(!acc.is_open()); + } + + void + testBindErrorNonLocalAddress() + { + io_context ioc; + + // tcp_server::bind should return an error for non-local address + test_server srv(ioc); + auto ec = srv.bind(endpoint(ipv4_address({192, 0, 2, 1}), 0)); + BOOST_TEST(ec); + } + + void + testListenOnOpenAcceptor() + { + io_context ioc; + tcp_acceptor acc(ioc); + + // First listen + auto ec1 = acc.listen(endpoint(ipv4_address::loopback(), 0)); + BOOST_TEST(!ec1); + BOOST_TEST(acc.is_open()); + + // Re-listen should close and reopen + auto ec2 = acc.listen(endpoint(ipv4_address::loopback(), 0)); + BOOST_TEST(!ec2); + BOOST_TEST(acc.is_open()); + } + void run() { @@ -446,6 +509,11 @@ struct tcp_server_test testStopWithoutStart(); testRestart(); testStartWithoutJoinThrows(); + testListenErrorCode(); + testBindSuccess(); + testListenErrorNonLocalAddress(); + testBindErrorNonLocalAddress(); + testListenOnOpenAcceptor(); } };