2727
2828#ifndef _WIN32
2929# include < arpa/inet.h>
30+ # include < fcntl.h>
3031# include < netinet/tcp.h>
32+ # include < poll.h>
3133#endif
3234
3335#include " ur_client_library/log.h"
@@ -37,6 +39,67 @@ namespace urcl
3739{
3840namespace comm
3941{
42+ namespace
43+ {
44+ // Time slice used while waiting for a non-blocking connect to resolve. Kept short so a
45+ // concurrent requestStop() aborts the wait promptly even if closing the socket does not
46+ // itself wake the wait (as is the case on Windows).
47+ constexpr int CONNECT_POLL_SLICE_MS = 100 ;
48+
49+ // Toggle the blocking mode of a socket. Returns true on success.
50+ bool setSocketBlocking (socket_t socket_fd, bool blocking)
51+ {
52+ #ifdef _WIN32
53+ u_long mode = blocking ? 0 : 1 ;
54+ return ::ioctlsocket (socket_fd, FIONBIO , &mode) == 0 ;
55+ #else
56+ int flags = ::fcntl (socket_fd, F_GETFL , 0 );
57+ if (flags < 0 )
58+ {
59+ return false ;
60+ }
61+ flags = blocking ? (flags & ~O_NONBLOCK ) : (flags | O_NONBLOCK );
62+ return ::fcntl (socket_fd, F_SETFL , flags) == 0 ;
63+ #endif
64+ }
65+
66+ // True if the last connect() call indicated that the connection is being established
67+ // asynchronously (the expected result for a non-blocking socket).
68+ bool connectInProgress ()
69+ {
70+ #ifdef _WIN32
71+ return ::WSAGetLastError () == WSAEWOULDBLOCK ;
72+ #else
73+ return errno == EINPROGRESS ;
74+ #endif
75+ }
76+
77+ // Waits up to timeout_ms for the socket to become writable (connect resolved).
78+ // Returns >0 if the socket is ready/has an event, 0 on timeout, <0 on error.
79+ //
80+ // poll() is used on both platforms (WSAPoll() on Windows). It avoids select()'s FD_SETSIZE
81+ // limitation entirely and keeps a single mental model. On Windows this relies on the WSAPoll
82+ // connect-failure fix introduced in Windows 10 version 2004 / Windows Server 2019: a failed
83+ // non-blocking connect is reported as (POLLHUP | POLLERR | POLLWRNORM). The caller treats any
84+ // returned event as "connect resolved" and consults SO_ERROR for the actual outcome, so it does
85+ // not depend on which particular revents flag is set.
86+ int waitForSocketWritable (socket_t socket_fd, int timeout_ms)
87+ {
88+ #ifdef _WIN32
89+ WSAPOLLFD pfd;
90+ pfd.fd = socket_fd;
91+ pfd.events = POLLWRNORM ; // == POLLOUT
92+ pfd.revents = 0 ;
93+ return ::WSAPoll (&pfd, 1 , timeout_ms);
94+ #else
95+ struct pollfd pfd;
96+ pfd.fd = socket_fd;
97+ pfd.events = POLLOUT ;
98+ pfd.revents = 0 ;
99+ return ::poll (&pfd, 1 , timeout_ms);
100+ #endif
101+ }
102+ } // namespace
40103TCPSocket::TCPSocket ()
41104 : socket_fd_(INVALID_SOCKET ), state_(SocketState::Invalid), reconnection_time_(std::chrono::seconds(10 ))
42105{
@@ -72,6 +135,66 @@ void TCPSocket::setupOptions()
72135 }
73136}
74137
138+ bool TCPSocket::openInterruptible (socket_t socket_fd, struct sockaddr * address, size_t address_len)
139+ {
140+ if (!setSocketBlocking (socket_fd, false ))
141+ {
142+ return false ;
143+ }
144+
145+ int connect_res = ::connect (socket_fd, address, static_cast <socklen_t >(address_len));
146+ bool connected = false ;
147+ if (connect_res == 0 )
148+ {
149+ // Connected immediately (common for loopback).
150+ connected = true ;
151+ }
152+ else if (connectInProgress ())
153+ {
154+ // Poll in short slices until the connect resolves, the OS connect timeout expires,
155+ // or a concurrent requestStop() asks us to abort.
156+ while (true )
157+ {
158+ if (stop_requested_)
159+ {
160+ return false ;
161+ }
162+ int ready = waitForSocketWritable (socket_fd, CONNECT_POLL_SLICE_MS );
163+ if (ready < 0 )
164+ {
165+ // poll() error (e.g. the fd was closed by requestStop()).
166+ return false ;
167+ }
168+ if (ready == 0 )
169+ {
170+ // Timeout slice elapsed without the connect resolving: re-check stop and keep waiting.
171+ continue ;
172+ }
173+ // The socket reported an event: query SO_ERROR to find out whether the connect succeeded.
174+ int so_error = 0 ;
175+ socklen_t len = sizeof (so_error);
176+ if (::getsockopt (socket_fd, SOL_SOCKET , SO_ERROR , reinterpret_cast <char *>(&so_error), &len) < 0 )
177+ {
178+ return false ;
179+ }
180+ connected = (so_error == 0 );
181+ break ;
182+ }
183+ }
184+ else
185+ {
186+ // Immediate, permanent failure (e.g. connection refused).
187+ connected = false ;
188+ }
189+
190+ if (connected && !setSocketBlocking (socket_fd, true ))
191+ {
192+ // Could not restore blocking mode; treat the connection as failed.
193+ return false ;
194+ }
195+ return connected;
196+ }
197+
75198bool TCPSocket::setup (const std::string& host, const int port, const size_t max_num_tries,
76199 const std::chrono::milliseconds reconnection_time)
77200{
@@ -85,13 +208,17 @@ bool TCPSocket::setup(const std::string& host, const int port, const size_t max_
85208 reconnection_time_resolved = reconnection_time_;
86209 }
87210
211+ // Honor a pending stop before doing anything else (e.g. a connect attempt issued just after
212+ // requestStop()). Checked before touching state_ so the cancellation cannot be lost.
213+ if (stop_requested_)
214+ return false ;
215+
88216 if (state_ == SocketState::Connected)
89217 return false ;
90218
91- // Clear any pre-existing Closed/Disconnected/Invalid state so that the
92- // between-attempt sleep below (which exits early when state becomes Closed)
93- // can only be short-circuited by a concurrent external close() call, not by
94- // a Closed state left over from a previous disconnect().
219+ // Clear any leftover Closed/Disconnected state from a previous (deliberate) disconnect() so
220+ // it does not interfere with this fresh attempt. Cancellation is tracked via stop_requested_,
221+ // not via state_, so this reset can no longer erase a teardown signal.
95222 state_ = SocketState::Invalid;
96223
97224 URCL_LOG_DEBUG (" Setting up connection: %s:%d" , host.c_str (), port);
@@ -112,6 +239,9 @@ bool TCPSocket::setup(const std::string& host, const int port, const size_t max_
112239 bool connected = false ;
113240 while (!connected)
114241 {
242+ if (stop_requested_)
243+ return false ;
244+
115245 if (getaddrinfo (host_name, service.c_str (), &hints, &result) != 0 )
116246 {
117247 URCL_LOG_ERROR (" Failed to get address for %s:%d" , host.c_str (), port);
@@ -122,18 +252,28 @@ bool TCPSocket::setup(const std::string& host, const int port, const size_t max_
122252 {
123253 socket_fd_ = ::socket (p->ai_family , p->ai_socktype , p->ai_protocol );
124254
125- if (socket_fd_ != -1 && open (socket_fd_, p->ai_addr , p->ai_addrlen ))
255+ if (socket_fd_ != -1 && openInterruptible (socket_fd_, p->ai_addr , p->ai_addrlen ))
126256 {
127257 connected = true ;
128258 break ;
129259 }
260+
261+ if (stop_requested_)
262+ {
263+ freeaddrinfo (result);
264+ return false ;
265+ }
130266 }
131267
132268 freeaddrinfo (result);
133269
134270 if (!connected)
135271 {
136272 state_ = SocketState::Invalid;
273+ if (stop_requested_)
274+ {
275+ return false ;
276+ }
137277 if (++connect_counter >= max_num_tries && max_num_tries > 0 )
138278 {
139279 URCL_LOG_ERROR (" Failed to establish connection for %s:%d after %d tries" , host.c_str (), port, max_num_tries);
@@ -147,16 +287,15 @@ bool TCPSocket::setup(const std::string& host, const int port, const size_t max_
147287 << std::chrono::duration_cast<std::chrono::duration<float >>(reconnection_time_resolved).count ()
148288 << " seconds." ;
149289 URCL_LOG_ERROR (" %s" , ss.str ().c_str ());
150- // Sleep in short slices so that an external close () (e.g. from ~RTDEClient calling
151- // disconnect() before joining the reconnect thread) can interrupt the wait promptly.
290+ // Sleep in short slices so that a concurrent requestStop () (e.g. from ~RTDEClient or
291+ // ~PrimaryClient before joining the reconnect thread) can interrupt the back-off promptly.
152292 const auto sleep_slice = std::chrono::milliseconds (100 );
153- for (auto slept = std::chrono::milliseconds (0 );
154- slept < reconnection_time_resolved && state_ != SocketState::Closed;
293+ for (auto slept = std::chrono::milliseconds (0 ); slept < reconnection_time_resolved && !stop_requested_;
155294 slept += sleep_slice)
156295 {
157296 std::this_thread::sleep_for (sleep_slice);
158297 }
159- if (state_ == SocketState::Closed )
298+ if (stop_requested_ )
160299 {
161300 return false ;
162301 }
@@ -179,6 +318,19 @@ void TCPSocket::close()
179318 }
180319}
181320
321+ void TCPSocket::requestStop ()
322+ {
323+ // Set the flag before closing so that a reconnect thread observing the closed socket is
324+ // guaranteed to also see the cancellation request (and therefore stop instead of retrying).
325+ stop_requested_ = true ;
326+ close ();
327+ }
328+
329+ void TCPSocket::clearStop ()
330+ {
331+ stop_requested_ = false ;
332+ }
333+
182334std::string TCPSocket::getIP () const
183335{
184336 sockaddr_in name;
0 commit comments