Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions include/boost/corosio/io_stream.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,12 @@ class BOOST_COROSIO_DECL io_stream : public io_object
@return An awaitable that completes with a pair of
`{error_code, bytes_transferred}`. Returns success with the
number of bytes read, or an error code on failure including:
- capy::error::eof: End of stream reached.
Check `ec == cond::eof` for portable comparison.
- operation_canceled: Cancelled via stop_token or cancel().
Check `ec == cond::canceled` for portable comparison.
- cond::eof: The peer closed their send direction by calling
shutdown or close, sending a TCP FIN. Use the portable
condition test: `if (ec == capy::cond::eof)`
- cond::canceled: Cancelled via stop_token or cancel().
Use the portable condition test:
`if (ec == capy::cond::canceled)`

@par Preconditions
The socket must be open and connected.
Expand Down
51 changes: 51 additions & 0 deletions include/boost/corosio/socket.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ namespace corosio {
class BOOST_COROSIO_DECL socket : public io_stream
{
public:
/** Different ways a socket may be shutdown. */
enum shutdown_type
{
shutdown_receive,
shutdown_send,
shutdown_both
};

struct socket_impl : io_stream_impl
{
virtual void connect(
Expand All @@ -82,6 +90,8 @@ class BOOST_COROSIO_DECL socket : public io_stream
endpoint,
std::stop_token,
system::error_code*) = 0;

virtual system::error_code shutdown(shutdown_type) noexcept = 0;
};

struct connect_awaitable
Expand Down Expand Up @@ -271,6 +281,47 @@ class BOOST_COROSIO_DECL socket : public io_stream
*/
void cancel();

/** Disable sends or receives on the socket.

TCP connections are full-duplex: each direction (send and receive)
operates independently. This function allows you to close one or
both directions without destroying the socket.

@li @ref shutdown_send sends a TCP FIN packet to the peer,
signaling that you have no more data to send. You can still
receive data until the peer also closes their send direction.
This is the most common use case, typically called before
close() to ensure graceful connection termination.

@li @ref shutdown_receive disables reading on the socket. This
does NOT send anything to the peer - they are not informed
and may continue sending data. Subsequent reads will fail
or return end-of-file. Incoming data may be discarded or
buffered depending on the operating system.

@li @ref shutdown_both combines both effects: sends a FIN and
disables reading.

When the peer shuts down their send direction (sends a FIN),
subsequent read operations will complete with `capy::cond::eof`.
Use the portable condition test rather than comparing error
codes directly:

@code
auto [ec, n] = co_await sock.read_some(buffer);
if (ec == capy::cond::eof)
{
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// Peer closed their send direction
}
@endcode

Any error from the underlying system call is silently discarded
because it is unlikely to be helpful.

@param what Determines what operations will no longer be allowed.
*/
void shutdown(shutdown_type what);
Comment on lines +284 to +323

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Document shutdown() behavior when the socket is closed.
The public API silently ignores shutdown on a closed socket (see tests). That should be stated to avoid surprises.

✏️ Proposed doc tweak
     /** Disable sends or receives on the socket.
 
         TCP connections are full-duplex: each direction (send and receive)
         operates independently. This function allows you to close one or
         both directions without destroying the socket.
+
+        If the socket is not open, this function has no effect.
🤖 Prompt for AI Agents
In `@include/boost/corosio/socket.hpp` around lines 284 - 323, Update the
shutdown() documentation (the comment above void shutdown(shutdown_type what))
to explicitly state that calling shutdown on an already-closed socket is a no-op
and any such call is silently ignored (no exception or error is produced),
referencing the existing tests that verify this behavior; keep the existing
examples (shutdown_send/shutdown_receive/shutdown_both) and portable eof
guidance (read_some and capy::cond::eof) but add one short sentence clarifying
the closed-socket silent-ignore semantics so callers are not surprised.


private:
friend class acceptor;

Expand Down
16 changes: 16 additions & 0 deletions src/corosio/src/detail/epoll/sockets.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,22 @@ class epoll_socket_impl
system::error_code*,
std::size_t*) override;

system::error_code shutdown(socket::shutdown_type what) noexcept override
{
int how;
switch (what)
{
case socket::shutdown_receive: how = SHUT_RD; break;
case socket::shutdown_send: how = SHUT_WR; break;
case socket::shutdown_both: how = SHUT_RDWR; break;
default:
return make_err(EINVAL);
}
if (::shutdown(fd_, how) != 0)
return make_err(errno);
return {};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

int native_handle() const noexcept { return fd_; }
bool is_open() const noexcept { return fd_ >= 0; }
void cancel() noexcept;
Expand Down
16 changes: 16 additions & 0 deletions src/corosio/src/detail/iocp/sockets.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,22 @@ class win_socket_impl
internal_->write_some(h, d, buf, token, ec, bytes);
}

system::error_code shutdown(socket::shutdown_type what) noexcept override
{
int how;
switch (what)
{
case socket::shutdown_receive: how = SD_RECEIVE; break;
case socket::shutdown_send: how = SD_SEND; break;
case socket::shutdown_both: how = SD_BOTH; break;
default:
return make_err(WSAEINVAL);
}
if (::shutdown(internal_->native_handle(), how) != 0)
return make_err(WSAGetLastError());
return {};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

win_socket_impl_internal* get_internal() const noexcept { return internal_.get(); }
};

Expand Down
12 changes: 10 additions & 2 deletions src/corosio/src/socket.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ socket::
open()
{
if (impl_)
return; // Already open
return;

auto& svc = ctx_->use_service<socket_service>();
auto& wrapper = svc.create_impl();
Expand All @@ -75,7 +75,7 @@ socket::
close()
{
if (!impl_)
return; // Already closed
return;

auto* wrapper = static_cast<socket_impl_type*>(impl_);
wrapper->release();
Expand All @@ -94,5 +94,13 @@ cancel()
#endif
}

void
socket::
shutdown(shutdown_type what)
{
if (impl_)
get().shutdown(what);
}

} // namespace corosio
} // namespace boost
109 changes: 109 additions & 0 deletions test/unit/socket.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,109 @@ struct socket_test
s2.close();
}

// Shutdown

void
testShutdownSend()
{
io_context ioc;
auto [s1, s2] = test::make_socket_pair(ioc);

auto task = [](socket& a, socket& b) -> capy::task<>
{
// Write data then shutdown send
co_await a.write_some(capy::const_buffer("hello", 5));
a.shutdown(socket::shutdown_send);

// Read the data
char buf[32] = {};
auto [ec1, n1] = co_await b.read_some(
capy::mutable_buffer(buf, sizeof(buf)));
BOOST_TEST(!ec1);
BOOST_TEST_EQ(std::string_view(buf, n1), "hello");

// Next read should get EOF
auto [ec2, n2] = co_await b.read_some(
capy::mutable_buffer(buf, sizeof(buf)));
BOOST_TEST(ec2 == capy::cond::eof);
Comment on lines +703 to +706

@coderabbitai coderabbitai Bot Jan 21, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get context around the failing assertion
sed -n '690,720p' test/unit/socket.cpp

Repository: cppalliance/corosio

Length of output: 1007


🏁 Script executed:

#!/bin/bash
# Find testReadAfterPeerClose test
rg -n "testReadAfterPeerClose" test/unit/socket.cpp -A 20 -B 2

Repository: cppalliance/corosio

Length of output: 1518


🏁 Script executed:

#!/bin/bash
# Search for EOF/eof handling patterns in read operations
rg -n "cond::eof|error::eof" --type=cpp | head -30

Repository: cppalliance/corosio

Length of output: 2573


🏁 Script executed:

#!/bin/bash
# Check how EOF is handled in different backends
rg -n "ec.*=.*eof|n.*==.*0" --type=cpp src/ | head -20

Repository: cppalliance/corosio

Length of output: 2196


🏁 Script executed:

#!/bin/bash
# Get the assertion in testReadAfterPeerClose
sed -n '420,430p' test/unit/socket.cpp

Repository: cppalliance/corosio

Length of output: 432


🏁 Script executed:

#!/bin/bash
# Find definitions of cond::eof and error::eof
rg -n "enum.*eof|error::eof|cond::eof" --type=cpp -B 2 -A 2 src/ include/ | head -40

Repository: cppalliance/corosio

Length of output: 2677


🏁 Script executed:

#!/bin/bash
# Check how cond::eof and error::eof relate to each other
rg -n "cond::eof|error::eof" --type=hpp include/ -B 1 -A 1 | head -50

Repository: cppalliance/corosio

Length of output: 90


🏁 Script executed:

#!/bin/bash
# Find cond::eof and error::eof definitions
rg -n "cond|error" include/boost/corosio/ --type=cpp -B 1 -A 1 | grep -E "eof|namespace|enum|struct|class" | head -60

Repository: cppalliance/corosio

Length of output: 1212


🏁 Script executed:

#!/bin/bash
# Look for condition codes or error codes definitions
fd -e h -e hpp include/ | xargs rg "cond::|error::" -l

Repository: cppalliance/corosio

Length of output: 1707


🏁 Script executed:

#!/bin/bash
# Search for any file mentioning cond::eof or difference between condition and error
rg -n "cond::eof" . --type=cpp -B 3 -A 3

Repository: cppalliance/corosio

Length of output: 1916


🏁 Script executed:

#!/bin/bash
# Check socket.hpp documentation for shutdown behavior
sed -n '300,320p' include/boost/corosio/socket.hpp

Repository: cppalliance/corosio

Length of output: 766


🏁 Script executed:

#!/bin/bash
# Find where cond::eof is defined
rg -n "namespace.*cond|class.*eof|cond\s*=" include/ src/ --type=cpp

Repository: cppalliance/corosio

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Check error codes and condition codes definitions
rg -n "error_code|condition_code" include/boost/corosio/ -A 3 | head -50

Repository: cppalliance/corosio

Length of output: 3247


🏁 Script executed:

#!/bin/bash
# Look for the definitions of cond and error namespaces/categories
find include/boost/corosio -name "*.hpp" -o -name "*.h" | xargs grep -l "cond::" | head -5

Repository: cppalliance/corosio

Length of output: 233


🏁 Script executed:

#!/bin/bash
# Get the full context of io_stream.hpp to understand cond::eof better
cat include/boost/corosio/io_stream.hpp | head -100

Repository: cppalliance/corosio

Length of output: 3730


🏁 Script executed:

#!/bin/bash
# Search for cond namespace and how it relates to error codes
find include/boost/corosio -type f | xargs rg "cond" | head -30

Repository: cppalliance/corosio

Length of output: 2570


🏁 Script executed:

#!/bin/bash
# Look for any condition code category or definition
rg -n "condition|cond" include/boost/capy --type=cpp | head -50

Repository: cppalliance/corosio

Length of output: 121


🏁 Script executed:

#!/bin/bash
# Check if there's any sys condition or condition code category defined
rg -n "struct.*cond|enum.*cond|make_error_code.*cond" . --type=cpp -B 2 -A 2 | head -50

Repository: cppalliance/corosio

Length of output: 521


🏁 Script executed:

#!/bin/bash
# Look for how cond::eof is being used/compared in the codebase
rg "cond::eof" . --type=cpp -B 5 -A 5

Repository: cppalliance/corosio

Length of output: 2521


🏁 Script executed:

#!/bin/bash
# Look for all EOF assertions in tests to see the pattern
rg "cond::eof|error::eof|n.*==.*0" test/unit/socket.cpp -B 2 -A 1

Repository: cppalliance/corosio

Length of output: 756


🏁 Script executed:

#!/bin/bash
# Check if capy::cond is defined in this repo or external
find . -name "*.hpp" -o -name "*.h" | xargs grep -l "namespace capy" | head -10

Repository: cppalliance/corosio

Length of output: 102


🏁 Script executed:

#!/bin/bash
# Look for how shutdown_send and close differ
rg -n "shutdown_send|shutdown_receive" include/boost/corosio/socket.hpp -B 5 -A 5

Repository: cppalliance/corosio

Length of output: 1275


🏁 Script executed:

#!/bin/bash
# Check if there's any code path that returns {ec={}, n=0} vs cond::eof
rg -n "ec_out.*=.*NULL|bytes_transferred.*==.*0" src/corosio/src/detail/ --type=cpp -B 3 -A 3 | head -50

Repository: cppalliance/corosio

Length of output: 1275


🏁 Script executed:

#!/bin/bash
# Check if there are any tests that explicitly verify both error::eof and cond::eof equivalence
rg "error::eof|cond::eof" test/unit/ --type=cpp -B 3 -A 3 | grep -A 3 -B 3 "BOOST_TEST"

Repository: cppalliance/corosio

Length of output: 1364


🏁 Script executed:

#!/bin/bash
# Look at the complete testShutdownSend function to understand if there's any special handling
sed -n '688,715p' test/unit/socket.cpp

Repository: cppalliance/corosio

Length of output: 951


🏁 Script executed:

#!/bin/bash
# Verify if other shutdown tests use the same assertion pattern
rg -n "testShutdown" test/unit/socket.cpp -A 30 | grep -E "testShutdown|BOOST_TEST.*eof|BOOST_TEST.*n.*=="

Repository: cppalliance/corosio

Length of output: 317


🏁 Script executed:

#!/bin/bash
# Search for where capy::cond and capy::error are defined or aliased
find . -type f \( -name "*.hpp" -o -name "*.h" -o -name "*.cpp" \) -exec grep -l "cond\|error" {} \; | grep -E "capy|corosio" | head -20

Repository: cppalliance/corosio

Length of output: 839


🏁 Script executed:

#!/bin/bash
# Look for any mapping or equivalence between cond::eof and error::eof
rg "cond|error" . --type=cpp -B 2 -A 2 | grep -E "using|typedef|==" | head -30

Repository: cppalliance/corosio

Length of output: 2294


🏁 Script executed:

#!/bin/bash
# Check testShutdownReceive and testShutdownOnClosedSocket for their EOF assertions
sed -n '716,770p' test/unit/socket.cpp | grep -E "BOOST_TEST|eof|ec"

Repository: cppalliance/corosio

Length of output: 595


🏁 Script executed:

#!/bin/bash
# Look for the actual implementation of read_some to see what it returns
rg -n "read_some.*{|co_return.*ec" src/corosio/src/detail/ --type=cpp -A 3 | head -50

Repository: cppalliance/corosio

Length of output: 183


🏁 Script executed:

#!/bin/bash
# Look for socket implementation details about read_some
rg -n "read_some" src/corosio/src/detail/ --type=cpp -A 5 | head -60

Repository: cppalliance/corosio

Length of output: 2656


🏁 Script executed:

#!/bin/bash
# Check what capy is - is it an external library?
find . -name "*.hpp" -o -name "*.h" | xargs grep "namespace capy" | head -5

Repository: cppalliance/corosio

Length of output: 119


🏁 Script executed:

#!/bin/bash
# Get full testShutdownReceive to see EOF assertions
sed -n '716,760p' test/unit/socket.cpp

Repository: cppalliance/corosio

Length of output: 1238


🏁 Script executed:

#!/bin/bash
# Look for any documentation about cond vs error in capy library
rg -n "portable|condition.*test|cond::" include/boost/corosio/ --type=cpp -B 2 -A 2 | head -80

Repository: cppalliance/corosio

Length of output: 5617


🏁 Script executed:

#!/bin/bash
# Search for capy library - is it external or in this repo?
find . -name "*.hpp" -o -name "*.h" | xargs grep -l "namespace capy" | xargs head -20

Repository: cppalliance/corosio

Length of output: 636


🏁 Script executed:

#!/bin/bash
# Look for error code category definitions to understand cond vs error equivalence
rg -n "error_category|condition_check|operator==" include/boost/corosio/ --type=cpp -B 2 -A 2 | head -60

Repository: cppalliance/corosio

Length of output: 1507


🏁 Script executed:

#!/bin/bash
# Check if there's any special handling that converts error::eof to cond::eof
rg -n "error::eof.*cond|cond::eof.*error" . --type=cpp -B 2 -A 2

Repository: cppalliance/corosio

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Look for any test utilities that might explain the difference
cat test/unit/socket.cpp | head -100

Repository: cppalliance/corosio

Length of output: 2397


🏁 Script executed:

#!/bin/bash
# Get the first 50 lines of test/unit/socket.cpp to see all imports
head -50 test/unit/socket.cpp

Repository: cppalliance/corosio

Length of output: 1340


🏁 Script executed:

#!/bin/bash
# Now understand the relationship - search for how capy::cond::eof and capy::error::eof relate
# Check if cond provides portable comparison operators
rg -n "cond::eof|error::eof" . --type=cpp | wc -l

Repository: cppalliance/corosio

Length of output: 65


🏁 Script executed:

#!/bin/bash
# Let me compare all three EOF test cases side by side
echo "=== testReadAfterPeerClose ===" && sed -n '403,430p' test/unit/socket.cpp
echo -e "\n=== testReadPartialEOF ===" && rg -A 20 "testReadPartialEOF" test/unit/socket.cpp | head -30
echo -e "\n=== testShutdownSend ===" && sed -n '685,710p' test/unit/socket.cpp

Repository: cppalliance/corosio

Length of output: 2853


🏁 Script executed:

#!/bin/bash
# Count how EOF is checked across all shutdown/close tests
echo "=== EOF handling patterns ===" && \
rg "testRead.*Close|testShutdown" test/unit/socket.cpp -A 25 | grep -E "testRead|testShutdown|BOOST_TEST.*ec.*eof|BOOST_TEST.*n.*==" | head -20

Repository: cppalliance/corosio

Length of output: 502


🏁 Script executed:

#!/bin/bash
# Verify what the composed read() operation does with EOF
sed -n '630,680p' test/unit/socket.cpp | grep -E "testRead|BOOST_TEST.*eof"

Repository: cppalliance/corosio

Length of output: 183


EOF assertion should accept zero bytes for robustness.
The test testReadAfterPeerClose (line 427) already accepts either error or zero bytes (ec2 || n2 == 0), but this test uses a strict check. After shutdown_send, both scenarios represent peer-initiated close and should handle EOF consistently. Consider accepting either condition:

-            BOOST_TEST(ec2 == capy::cond::eof);
+            BOOST_TEST(ec2 == capy::cond::eof || n2 == 0);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Next read should get EOF
auto [ec2, n2] = co_await b.read_some(
capy::mutable_buffer(buf, sizeof(buf)));
BOOST_TEST(ec2 == capy::cond::eof);
// Next read should get EOF
auto [ec2, n2] = co_await b.read_some(
capy::mutable_buffer(buf, sizeof(buf)));
BOOST_TEST(ec2 == capy::cond::eof || n2 == 0);
🤖 Prompt for AI Agents
In `@test/unit/socket.cpp` around lines 703 - 706, The EOF assertion in the test
after shutdown_send is too strict: locate the assertion using b.read_some that
binds auto [ec2, n2] and replace the strict check (BOOST_TEST(ec2 ==
capy::cond::eof)) with a condition that accepts either capy::cond::eof or zero
bytes (e.g., BOOST_TEST((ec2 == capy::cond::eof) || (n2 == 0))) so it matches
the robust pattern used in testReadAfterPeerClose.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it should not accept zero bytes, that would be a postcondition violation. The contract says eof.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vinniefalco, thank you for the clarification. You're absolutely right—I was incorrect. The contract for shutdown_send requires that an explicit EOF error code (capy::cond::eof) be returned, not just zero bytes. The test is correct as written.

The difference between shutdown() and close() is important here: shutdown() has a stricter postcondition that guarantees an EOF error code, while close() may signal EOF either through an error code or zero bytes (which is why testReadAfterPeerClose uses the more permissive ec2 || n2 == 0 pattern).


✏️ Learnings added
Learnt from: vinniefalco
Repo: cppalliance/corosio PR: 40
File: test/unit/socket.cpp:703-706
Timestamp: 2026-01-21T00:31:19.012Z
Learning: In boost::corosio, `socket::shutdown(shutdown_send)` has a contract that requires subsequent read operations to return an explicit EOF error code (`capy::cond::eof`), not just zero bytes. This is a stricter postcondition than `socket::close()`, which may signal EOF either through an error code or by returning zero bytes.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

};
capy::run_async(ioc.get_executor())(task(s1, s2));

ioc.run();
s1.close();
s2.close();
}

void
testShutdownReceive()
{
io_context ioc;
auto [s1, s2] = test::make_socket_pair(ioc);

auto task = [](socket& a, socket& b) -> capy::task<>
{
// Shutdown receive on b
b.shutdown(socket::shutdown_receive);

// b can still send
co_await b.write_some(capy::const_buffer("from_b", 6));

char buf[32] = {};
auto [ec, n] = co_await a.read_some(
capy::mutable_buffer(buf, sizeof(buf)));
BOOST_TEST(!ec);
BOOST_TEST_EQ(std::string_view(buf, n), "from_b");
};
capy::run_async(ioc.get_executor())(task(s1, s2));

ioc.run();
s1.close();
s2.close();
}

void
testShutdownOnClosedSocket()
{
io_context ioc;
socket sock(ioc);

// Shutdown on closed socket should not crash
sock.shutdown(socket::shutdown_send);
sock.shutdown(socket::shutdown_receive);
sock.shutdown(socket::shutdown_both);
}

void
testShutdownBothSendDirection()
{
io_context ioc;
auto [s1, s2] = test::make_socket_pair(ioc);

auto task = [](socket& a, socket& b) -> capy::task<>
{
// Write data then shutdown both
co_await a.write_some(capy::const_buffer("goodbye", 7));
a.shutdown(socket::shutdown_both);

// Peer should receive the data
char buf[32] = {};
auto [ec1, n1] = co_await b.read_some(
capy::mutable_buffer(buf, sizeof(buf)));
BOOST_TEST(!ec1);
BOOST_TEST_EQ(std::string_view(buf, n1), "goodbye");

// Next read should get EOF
auto [ec2, n2] = co_await b.read_some(
capy::mutable_buffer(buf, sizeof(buf)));
BOOST_TEST(ec2 == capy::cond::eof);
};
capy::run_async(ioc.get_executor())(task(s1, s2));

ioc.run();
s1.close();
s2.close();
}

// Data Integrity

void
Expand Down Expand Up @@ -770,6 +873,12 @@ struct socket_test
testReadAfterPeerClose();
testWriteAfterPeerClose();

// Shutdown
testShutdownSend();
testShutdownReceive();
testShutdownOnClosedSocket();
testShutdownBothSendDirection();

// Cancellation
testCancelRead();
testCloseWhileReading();
Expand Down
Loading