From b59d1c5656ca5b03ca3910617f01ccd694484cc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Nogueira?= Date: Wed, 6 Nov 2024 15:58:32 -0300 Subject: [PATCH 1/2] Add getAutoConnectTimeout function. Co-authored-by: Mark Rivers --- asyn/asynDriver/asynDriver.h | 1 + asyn/asynDriver/asynManager.c | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/asyn/asynDriver/asynDriver.h b/asyn/asynDriver/asynDriver.h index 6157f1b9f..d37eeeaa8 100644 --- a/asyn/asynDriver/asynDriver.h +++ b/asyn/asynDriver/asynDriver.h @@ -164,6 +164,7 @@ typedef struct asynManager { asynStatus (*isEnabled)(asynUser *pasynUser,int *yesNo); asynStatus (*isAutoConnect)(asynUser *pasynUser,int *yesNo); asynStatus (*setAutoConnectTimeout)(double timeout); + asynStatus (*getAutoConnectTimeout)(double *timeout); asynStatus (*waitConnect)(asynUser *pasynUser, double timeout); /*The following are methods for interrupts*/ asynStatus (*registerInterruptSource)(const char *portName, diff --git a/asyn/asynDriver/asynManager.c b/asyn/asynDriver/asynManager.c index dc2882685..fe848cea9 100644 --- a/asyn/asynDriver/asynManager.c +++ b/asyn/asynDriver/asynManager.c @@ -316,6 +316,7 @@ static asynStatus isConnected(asynUser *pasynUser,int *yesNo); static asynStatus isEnabled(asynUser *pasynUser,int *yesNo); static asynStatus isAutoConnect(asynUser *pasynUser,int *yesNo); static asynStatus setAutoConnectTimeout(double timeout); +static asynStatus getAutoConnectTimeout(double *timeout); static asynStatus waitConnect(asynUser *pasynUser, double timeout); static asynStatus registerInterruptSource(const char *portName, asynInterface *pasynInterface, void **pasynPvt); @@ -374,6 +375,7 @@ static asynManager manager = { isEnabled, isAutoConnect, setAutoConnectTimeout, + getAutoConnectTimeout, waitConnect, registerInterruptSource, getInterruptPvt, @@ -2376,6 +2378,15 @@ static asynStatus setAutoConnectTimeout(double timeout) return asynSuccess; } +static asynStatus getAutoConnectTimeout(double *timeout) +{ + if(!pasynBase) asynInit(); + epicsMutexMustLock(pasynBase->lock); + *timeout = pasynBase->autoConnectTimeout; + epicsMutexUnlock(pasynBase->lock); + return asynSuccess; +} + static asynStatus setQueueLockPortTimeout(asynUser *pasynUser, double timeout) { userPvt *puserPvt = asynUserToUserPvt(pasynUser); From e1987063c6715e799325cd6621136969ece55dbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Nogueira?= Date: Wed, 6 Nov 2024 16:00:03 -0300 Subject: [PATCH 2/2] Add TCP port connection timeout. This implementation is intended to allow faster forward progress in a startup script in cases where an unreachable device would keep an asyn port locked until the OS timed out the connection attempt (meaning the OS didn't have any other way of diagnosing the fact that the device was unreachable). Forward progress for the drvAsynIPPortConfigure command was already guaranteed, because the connection attempt happens in the port thread, and the main thread would only wait for autoConnectTimeout to see if the connection was successful. Since the asyn port would remain locked, subsequent commands to the port (e.g. asynSetOption, asynOctetSetInputEos, asynOctetSetOutputEos) would block until said OS timeout. Now, we apply the same autoConnectTimeout to the connection attempt itself. This was tested by comparing to the behavior from before the patch when connecting to "2.3.4.5:4001", where commands to the port would hang indefinitely. Use poll as a reasonably portable option. This takes advantage of the existing WSAPoll and FAKE_POLL fallback implementations as well. getsockopt(SO_ERROR) on Windows uses "int", removing the requirement for any custom code for that platform. We don't add any handling for EINTR, since it would add considerably more code, and the connection can simply be retried. For that reason, poll returning -1 is always an error. --- asyn/drvAsynSerial/drvAsynIPPort.c | 54 ++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/asyn/drvAsynSerial/drvAsynIPPort.c b/asyn/drvAsynSerial/drvAsynIPPort.c index a5fd1415f..4090f633e 100644 --- a/asyn/drvAsynSerial/drvAsynIPPort.c +++ b/asyn/drvAsynSerial/drvAsynIPPort.c @@ -505,13 +505,56 @@ connectIt(void *drvPvt, asynUser *pasynUser) } } + } + +#ifdef USE_POLL + if (setNonBlock(fd, 1) < 0) { + epicsSnprintf(pasynUser->errorMessage,pasynUser->errorMessageSize, + "Can't set %s O_NONBLOCK option: %s", + tty->IPDeviceName, strerror(SOCKERRNO)); + epicsSocketDestroy(fd); + return asynError; + } +#endif + + if (pasynUser->reason <= 0) { + /* * Connect to the remote host * If the connect fails, arrange for another DNS lookup in case the * problem is just that the device has DHCP'd itself an new number. */ if (tty->socketType != SOCK_DGRAM) { - if (connect(fd, &tty->farAddr.oa.sa, (int)tty->farAddrSize) < 0) { + int connectResult = connect(fd, &tty->farAddr.oa.sa, (int)tty->farAddrSize); + #ifdef USE_POLL + if (connectResult < 0 && ((SOCKERRNO == EWOULDBLOCK) || (SOCKERRNO == EINPROGRESS))) { + double connectTimeout; + int msConnectTimeout; + struct pollfd pollfd; + + pasynManager->getAutoConnectTimeout(&connectTimeout); + msConnectTimeout = 1000 * connectTimeout; + pollfd.fd = fd; + pollfd.events = POLLOUT; + + /* + * poll() returning 1 is the only case where connect might have been successful. + * Otherwise connectResult will remain -1. + */ + if (poll(&pollfd, 1, msConnectTimeout) == 1) { + int so_error; + socklen_t len = sizeof so_error; + + /* + * We must verify SO_ERROR to make sure the connection was successful. + */ + getsockopt(fd, SOL_SOCKET, SO_ERROR, &so_error, &len); + if (so_error == 0) + connectResult = 0; + } + } + #endif + if (connectResult < 0) { epicsSnprintf(pasynUser->errorMessage,pasynUser->errorMessageSize, "Can't connect to %s: %s", tty->IPDeviceName, strerror(SOCKERRNO)); @@ -532,15 +575,6 @@ connectIt(void *drvPvt, asynUser *pasynUser) epicsSocketDestroy(fd); return asynError; } -#ifdef USE_POLL - if (setNonBlock(fd, 1) < 0) { - epicsSnprintf(pasynUser->errorMessage,pasynUser->errorMessageSize, - "Can't set %s O_NONBLOCK option: %s", - tty->IPDeviceName, strerror(SOCKERRNO)); - epicsSocketDestroy(fd); - return asynError; - } -#endif asynPrint(pasynUser, ASYN_TRACE_FLOW, "Opened connection OK to %s\n", tty->IPDeviceName);