mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-19 11:19:36 +00:00
Wake a WebSocket read on a pong timeout on every platform
When max_missed_pongs ran out, the heartbeat thread shut down the read side of the socket to end a read() blocked on the silent peer. On Windows that does not wake the wait, so read() stayed blocked until its read timeout and then returned Timeout. The heartbeat test never ran on Windows, so CI did not catch it. The heartbeat now sets a flag that the stream's read interrupt checks, the mechanism SSEClient::stop() uses, and WebSocketSSLStream takes the interrupt too. It is set only when a pong timeout can happen, so other connections wait as before. read() reports a timeout on a connection that has closed as Fail, since Timeout means it is still usable. Build and run the heartbeat test in the Windows jobs, and add a TLS pong timeout test.
This commit is contained in:
1 parent
801bf2e0d2
commit
696b94b5ed
4 files changed
+76
-27
No files matched your search
@@ -4645,6 +4645,8 @@ private:
|
||||
int max_missed_pongs_;
|
||||
int unacked_pings_ = 0;
|
||||
std::atomic<bool> closed_{false};
|
||||
// Set when the peer stopped answering pings, to end a read() waiting on it
|
||||
std::atomic<bool> pong_timed_out_{false};
|
||||
// Set once the caller has bounded read() through set_read_timeout(). Until
|
||||
// then the timeout in effect is the compile-time default, and elapsing it
|
||||
// is a failure that closes the connection, not a Timeout.
|
||||
@@ -10766,6 +10768,7 @@ public:
|
||||
socket_t socket() const override;
|
||||
time_t duration() const override;
|
||||
void set_read_timeout(time_t sec, time_t usec = 0) override;
|
||||
void set_read_interrupt(std::function<bool()> interrupted) override;
|
||||
|
||||
private:
|
||||
mutable std::mutex session_mutex_;
|
||||
@@ -10780,6 +10783,7 @@ private:
|
||||
time_t write_timeout_sec_;
|
||||
time_t write_timeout_usec_;
|
||||
const std::chrono::time_point<std::chrono::steady_clock> start_time_;
|
||||
std::function<bool()> read_interrupted_;
|
||||
};
|
||||
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
@@ -13386,7 +13390,8 @@ inline bool WebSocketSSLStream::is_readable() const {
|
||||
}
|
||||
|
||||
inline bool WebSocketSSLStream::wait_readable() const {
|
||||
return select_read(sock_, read_timeout_sec_, read_timeout_usec_) > 0;
|
||||
return select_read_interruptible(sock_, read_timeout_sec_, read_timeout_usec_,
|
||||
read_interrupted_) > 0;
|
||||
}
|
||||
|
||||
inline bool WebSocketSSLStream::wait_writable() const {
|
||||
@@ -13486,6 +13491,11 @@ inline void WebSocketSSLStream::set_read_timeout(time_t sec, time_t usec) {
|
||||
read_timeout_usec_ = usec;
|
||||
}
|
||||
|
||||
inline void
|
||||
WebSocketSSLStream::set_read_interrupt(std::function<bool()> interrupted) {
|
||||
read_interrupted_ = std::move(interrupted);
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
#endif // CPPHTTPLIB_SSL_ENABLED
|
||||
|
||||
@@ -23331,12 +23341,15 @@ inline ReadResult WebSocket::read(std::string &msg) {
|
||||
impl::FrameRead r =
|
||||
impl::read_websocket_frame(strm_, opcode, payload, fin, is_server_,
|
||||
CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH);
|
||||
// A timeout landed on a frame boundary: the connection is untouched and
|
||||
// still usable, so hand control back without closing it. That is only
|
||||
// useful to a caller who asked for the timeout; the compile-time default
|
||||
// is a backstop against a peer gone quiet, and elapsing it closes the
|
||||
// connection so a plain `while (ws.read(msg))` loop ends.
|
||||
if (r == impl::FrameRead::Timeout && read_timeout_set_) { return Timeout; }
|
||||
// A timeout landed on a frame boundary of an open connection: it is
|
||||
// untouched and still usable, so hand control back without closing it.
|
||||
// That is only useful to a caller who asked for the timeout; the
|
||||
// compile-time default is a backstop against a peer gone quiet, and
|
||||
// elapsing it closes the connection so a plain `while (ws.read(msg))` loop
|
||||
// ends. A wait that a pong timeout cut short finds the connection closed.
|
||||
if (r == impl::FrameRead::Timeout && read_timeout_set_ && !closed_) {
|
||||
return Timeout;
|
||||
}
|
||||
if (r != impl::FrameRead::Ok) {
|
||||
closed_ = true;
|
||||
return Fail;
|
||||
@@ -23465,10 +23478,16 @@ inline WebSocket::~WebSocket() {
|
||||
}
|
||||
ping_cv_.notify_all();
|
||||
if (ping_thread_.joinable()) { ping_thread_.join(); }
|
||||
// A server's stream outlives this object
|
||||
strm_.set_read_interrupt(nullptr);
|
||||
}
|
||||
|
||||
inline void WebSocket::start_heartbeat() {
|
||||
if (ping_interval_sec_ <= 0) { return; }
|
||||
// A pong timeout ends a read() blocked on the silent peer
|
||||
if (max_missed_pongs_ > 0) {
|
||||
strm_.set_read_interrupt([this] { return pong_timed_out_.load(); });
|
||||
}
|
||||
ping_thread_ = std::thread([this]() {
|
||||
std::unique_lock<std::mutex> lock(ping_mutex_);
|
||||
// The predicate keeps a spurious wakeup from sending a ping early
|
||||
@@ -23480,14 +23499,7 @@ inline void WebSocket::start_heartbeat() {
|
||||
if (max_missed_pongs_ > 0 && unacked_pings_ >= max_missed_pongs_) {
|
||||
lock.unlock();
|
||||
close(CloseStatus::GoingAway, "pong timeout");
|
||||
// Wake a read() blocked on the unresponsive peer. Only the read side
|
||||
// is shut down: a TLS backend answers the EOF with an alert, and
|
||||
// writing it to a socket closed for writing raises SIGPIPE.
|
||||
#ifdef _WIN32
|
||||
shutdown(strm_.socket(), SD_RECEIVE);
|
||||
#else
|
||||
shutdown(strm_.socket(), SHUT_RD);
|
||||
#endif
|
||||
pong_timed_out_ = true;
|
||||
return;
|
||||
}
|
||||
lock.unlock();
|
||||
|
||||
Reference in new issue
Block a user