diff --git a/include/spdlog/details/tcp_client-windows.h b/include/spdlog/details/tcp_client-windows.h index bf8f7b87..76e73a36 100644 --- a/include/spdlog/details/tcp_client-windows.h +++ b/include/spdlog/details/tcp_client-windows.h @@ -57,9 +57,82 @@ public: } SOCKET fd() const { return socket_; } + + int connect_socket_with_timeout(SOCKET sockfd, + const struct sockaddr *addr, + int addrlen, + const timeval &tv) { + // If no timeout requested, do a normal blocking connect. + if (tv.tv_sec == 0 && tv.tv_usec == 0) { + int rv = ::connect(sockfd, addr, addrlen); + if (rv == SOCKET_ERROR && WSAGetLastError() == WSAEISCONN) { + return 0; + } + return rv; + } + + // Switch to non‐blocking mode + u_long mode = 1UL; + if (::ioctlsocket(sockfd, FIONBIO, &mode) == SOCKET_ERROR) { + return SOCKET_ERROR; + } + + int rv = ::connect(sockfd, addr, addrlen); + int last_error = WSAGetLastError(); + if (rv == 0 || last_error == WSAEISCONN) { + mode = 0UL; + if (::ioctlsocket(sockfd, FIONBIO, &mode) == SOCKET_ERROR) { + return SOCKET_ERROR; + } + return 0; + } + if (last_error != WSAEWOULDBLOCK) { + // Real error + mode = 0UL; + if (::ioctlsocket(sockfd, FIONBIO, &mode)) { + return SOCKET_ERROR; + } + return SOCKET_ERROR; + } + + // Wait until socket is writable or timeout expires + fd_set wfds; + FD_ZERO(&wfds); + FD_SET(sockfd, &wfds); + + rv = ::select(0, nullptr, &wfds, nullptr, const_cast(&tv)); + + // Restore blocking mode regardless of select result + mode = 0UL; + if (::ioctlsocket(sockfd, FIONBIO, &mode) == SOCKET_ERROR) { + return SOCKET_ERROR; + } + + if (rv == 0) { + WSASetLastError(WSAETIMEDOUT); + return SOCKET_ERROR; + } + if (rv == SOCKET_ERROR) { + return SOCKET_ERROR; + } + + int so_error = 0; + int len = sizeof(so_error); + if (::getsockopt(sockfd, SOL_SOCKET, SO_ERROR, reinterpret_cast(&so_error), &len) == + SOCKET_ERROR) { + return SOCKET_ERROR; + } + if (so_error != 0 && so_error != WSAEISCONN) { + // connection failed + WSASetLastError(so_error); + return SOCKET_ERROR; + } + + return 0; // success + } // try to connect or throw on failure - void connect(const std::string &host, int port) { + void connect(const std::string &host, int port, int timeout_ms = 0) { if (is_connected()) { close(); } @@ -71,6 +144,10 @@ public: hints.ai_flags = AI_NUMERICSERV; // port passed as as numeric value hints.ai_protocol = 0; + timeval tv; + tv.tv_sec = timeout_ms / 1000; + tv.tv_usec = (timeout_ms % 1000) * 1000; + auto port_str = std::to_string(port); struct addrinfo *addrinfo_result; auto rv = ::getaddrinfo(host.c_str(), port_str.c_str(), &hints, &addrinfo_result); @@ -82,7 +159,6 @@ public: } // Try each address until we successfully connect(2). - for (auto *rp = addrinfo_result; rp != nullptr; rp = rp->ai_next) { socket_ = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); if (socket_ == INVALID_SOCKET) { @@ -90,18 +166,24 @@ public: WSACleanup(); continue; } - if (::connect(socket_, rp->ai_addr, (int)rp->ai_addrlen) == 0) { + if (connect_socket_with_timeout(socket_, rp->ai_addr, (int)rp->ai_addrlen, tv) == 0) { + last_error = 0; break; - } else { - last_error = ::WSAGetLastError(); - close(); } + last_error = WSAGetLastError(); + ::closesocket(socket_); + socket_ = INVALID_SOCKET; } ::freeaddrinfo(addrinfo_result); if (socket_ == INVALID_SOCKET) { WSACleanup(); throw_winsock_error_("connect failed", last_error); } + if (timeout_ms > 0) { + DWORD tv = static_cast(timeout_ms); + ::setsockopt(socket_, SOL_SOCKET, SO_RCVTIMEO, (const char *)&tv, sizeof(tv)); + ::setsockopt(socket_, SOL_SOCKET, SO_SNDTIMEO, (const char *)&tv, sizeof(tv)); + } // set TCP_NODELAY int enable_flag = 1; diff --git a/include/spdlog/details/tcp_client.h b/include/spdlog/details/tcp_client.h index 9d3c40d5..1eaa3cb9 100644 --- a/include/spdlog/details/tcp_client.h +++ b/include/spdlog/details/tcp_client.h @@ -39,8 +39,72 @@ public: ~tcp_client() { close(); } + int connect_socket_with_timeout(int sockfd, + const struct sockaddr *addr, + socklen_t addrlen, + const timeval &tv) { + // Blocking connect if timeout is zero + if (tv.tv_sec == 0 && tv.tv_usec == 0) { + int rv = ::connect(sockfd, addr, addrlen); + if (rv < 0 && errno == EISCONN) { + // already connected, treat as success + return 0; + } + return rv; + } + + // Non-blocking path + int orig_flags = ::fcntl(sockfd, F_GETFL, 0); + if (orig_flags < 0) { + return -1; + } + if (::fcntl(sockfd, F_SETFL, orig_flags | O_NONBLOCK) < 0) { + return -1; + } + + int rv = ::connect(sockfd, addr, addrlen); + if (rv == 0 || (rv < 0 && errno == EISCONN)) { + // immediate connect or already connected + ::fcntl(sockfd, F_SETFL, orig_flags); + return 0; + } + if (errno != EINPROGRESS) { + ::fcntl(sockfd, F_SETFL, orig_flags); + return -1; + } + + // wait for writability + fd_set wfds; + FD_ZERO(&wfds); + FD_SET(sockfd, &wfds); + + struct timeval tv_copy = tv; + rv = ::select(sockfd + 1, nullptr, &wfds, nullptr, &tv_copy); + if (rv <= 0) { + // timeout or error + ::fcntl(sockfd, F_SETFL, orig_flags); + if (rv == 0) errno = ETIMEDOUT; + return -1; + } + + // check socket error + int so_error = 0; + socklen_t len = sizeof(so_error); + if (::getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &so_error, &len) < 0) { + ::fcntl(sockfd, F_SETFL, orig_flags); + return -1; + } + ::fcntl(sockfd, F_SETFL, orig_flags); + if (so_error != 0 && so_error != EISCONN) { + errno = so_error; + return -1; + } + + return 0; + } + // try to connect or throw on failure - void connect(const std::string &host, int port) { + void connect(const std::string &host, int port, int timeout_ms = 0) { close(); struct addrinfo hints {}; memset(&hints, 0, sizeof(struct addrinfo)); @@ -49,6 +113,10 @@ public: hints.ai_flags = AI_NUMERICSERV; // port passed as as numeric value hints.ai_protocol = 0; + struct timeval tv; + tv.tv_sec = timeout_ms / 1000; + tv.tv_usec = (timeout_ms % 1000) * 1000; + auto port_str = std::to_string(port); struct addrinfo *addrinfo_result; auto rv = ::getaddrinfo(host.c_str(), port_str.c_str(), &hints, &addrinfo_result); @@ -69,8 +137,9 @@ public: last_errno = errno; continue; } - rv = ::connect(socket_, rp->ai_addr, rp->ai_addrlen); - if (rv == 0) { + ::fcntl(socket_, F_SETFD, FD_CLOEXEC); + if (connect_socket_with_timeout(socket_, rp->ai_addr, rp->ai_addrlen, tv) == 0) { + last_errno = 0; break; } last_errno = errno; @@ -82,6 +151,12 @@ public: throw_spdlog_ex("::connect failed", last_errno); } + if (timeout_ms > 0) { + // Set timeouts for send and recv + ::setsockopt(socket_, SOL_SOCKET, SO_RCVTIMEO, (const char *)&tv, sizeof(tv)); + ::setsockopt(socket_, SOL_SOCKET, SO_SNDTIMEO, (const char *)&tv, sizeof(tv)); + } + // set TCP_NODELAY int enable_flag = 1; ::setsockopt(socket_, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast(&enable_flag), diff --git a/include/spdlog/sinks/tcp_sink.h b/include/spdlog/sinks/tcp_sink.h index 25349645..9ed64133 100644 --- a/include/spdlog/sinks/tcp_sink.h +++ b/include/spdlog/sinks/tcp_sink.h @@ -31,6 +31,7 @@ namespace sinks { struct tcp_sink_config { std::string server_host; int server_port; + int timeout_ms = 0; // The timeout for all 3 major socket operations that is connect, send, and recv bool lazy_connect = false; // if true connect on first log call instead of on construction tcp_sink_config(std::string host, int port) @@ -44,10 +45,22 @@ public: // connect to tcp host/port or throw if failed // host can be hostname or ip address + explicit tcp_sink(const std::string &host, + int port, + int timeout_ms = 0, + bool lazy_connect = false) + : config_{host, port} { + config_.timeout_ms = timeout_ms; + config_.lazy_connect = lazy_connect; + if (!config_.lazy_connect) { + client_.connect(config_.server_host, config_.server_port, config_.timeout_ms); + } + } + explicit tcp_sink(tcp_sink_config sink_config) : config_{std::move(sink_config)} { if (!config_.lazy_connect) { - this->client_.connect(config_.server_host, config_.server_port); + client_.connect(config_.server_host, config_.server_port, config_.timeout_ms); } } @@ -58,7 +71,7 @@ protected: spdlog::memory_buf_t formatted; spdlog::sinks::base_sink::formatter_->format(msg, formatted); if (!client_.is_connected()) { - client_.connect(config_.server_host, config_.server_port); + client_.connect(config_.server_host, config_.server_port, config_.timeout_ms); } client_.send(formatted.data(), formatted.size()); }