diff --git a/documentation/RELEASE_NOTES.md b/documentation/RELEASE_NOTES.md index 732c98654..f1eb93a8e 100644 --- a/documentation/RELEASE_NOTES.md +++ b/documentation/RELEASE_NOTES.md @@ -38,6 +38,12 @@ record type by opening these files in a text editor intead of opening a browser and loading the HTML versions or finding and opening the files from the EPICS Documentation site. +### fdManager file descriptor limit removed + +In order to support file descriptors above 1023, fdManager now uses +poll() instead of select() on all architectures that support it +(Linux, MacOS, Windows, newer RTEMS). + ### Post monitors from compress record when it's reset Writing into a compress record's `RES` field now posts a monitor event instead diff --git a/modules/libcom/src/fdmgr/fdManager.cpp b/modules/libcom/src/fdmgr/fdManager.cpp index 8a9ed788f..9e133d2ad 100644 --- a/modules/libcom/src/fdmgr/fdManager.cpp +++ b/modules/libcom/src/fdmgr/fdManager.cpp @@ -19,39 +19,117 @@ // 1) This library is not thread safe // -#include - #define instantiateRecourceLib #include "epicsAssert.h" #include "epicsThread.h" #include "fdManager.h" #include "locationException.h" -using std :: max; +#if !defined(FDMGR_USE_POLL) && !defined(FDMGR_USE_SELECT) +#if defined(__linux__) || defined(darwin) || _WIN32_WINNT >= 0x600 || (defined(__rtems__) && !defined(RTEMS_LEGACY_STACK)) +#define FDMGR_USE_POLL +#else +#define FDMGR_USE_SELECT +#endif +#endif + +#ifdef FDMGR_USE_POLL +#include +#if !defined(_WIN32) +#include +#endif + +static const short PollEvents[] = { // must match fdRegType + POLLRDNORM | POLLRDBAND | POLLIN | POLLHUP | POLLERR, + POLLWRBAND | POLLWRNORM | POLLOUT | POLLERR, + POLLPRI }; + +#if defined(_WIN32) +#define poll WSAPoll +// Filter out PollEvents that Windows does not accept in events (only returns in revents) +#define WIN_POLLEVENT_FILTER(ev) static_cast((ev) & (POLLIN | POLLOUT)) +#else +// Linux, MacOS and RTEMS don't care +#define WIN_POLLEVENT_FILTER(ev) (ev) +#endif +#endif + +#ifdef FDMGR_USE_SELECT +#include +#endif + +struct fdManagerPrivate { + tsDLList regList; + tsDLList activeList; + resTable fdTbl; + const double sleepQuantum; + epics::auto_ptr pTimerQueue; + bool processInProg; + +#ifdef FDMGR_USE_POLL + std::vector pollfds; +#endif + +#ifdef FDMGR_USE_SELECT + fd_set fdSets[fdrNEnums]; + SOCKET maxFD; +#endif + + // + // Set to fdreg when in call back + // and nill otherwise + // + volatile fdReg* pCBReg; + fdManager& owner; + + explicit fdManagerPrivate(fdManager& owner); + void lazyInitTimerQueue(); +}; + +fdManagerPrivate::fdManagerPrivate(fdManager& owner) : + sleepQuantum(epicsThreadSleepQuantum()), + processInProg(false), + pCBReg(NULL), owner(owner) +{} + +inline void fdManagerPrivate::lazyInitTimerQueue() +{ + if (!pTimerQueue.get()) { + pTimerQueue.reset(&epicsTimerQueuePassive::create(owner)); + } +} + +epicsTimer& fdManager::createTimer() +{ + priv->lazyInitTimerQueue(); + return priv->pTimerQueue->createTimer(); +} fdManager fileDescriptorManager; -const unsigned mSecPerSec = 1000u; -const unsigned uSecPerSec = 1000u * mSecPerSec; +static const unsigned mSecPerSec = 1000u; +#ifdef FDMGR_USE_SELECT +static const unsigned uSecPerSec = 1000u * mSecPerSec; +#endif // // fdManager::fdManager() // -// hopefully its a reasonable guess that select() and epicsThreadSleep() +// hopefully its a reasonable guess that poll()/select() and epicsThreadSleep() // will have the same sleep quantum // -LIBCOM_API fdManager::fdManager () : - sleepQuantum ( epicsThreadSleepQuantum () ), - fdSetsPtr ( new fd_set [fdrNEnums] ), - pTimerQueue ( 0 ), maxFD ( 0 ), processInProg ( false ), - pCBReg ( 0 ) +LIBCOM_API fdManager::fdManager() : + priv(new fdManagerPrivate(*this)) { - int status = osiSockAttach (); - assert (status); + int status = osiSockAttach(); + assert(status); - for ( size_t i = 0u; i < fdrNEnums; i++ ) { - FD_ZERO ( &fdSetsPtr[i] ); +#ifdef FDMGR_USE_SELECT + priv->maxFD = 0; + for (size_t i = 0u; i < fdrNEnums; i++) { + FD_ZERO(&priv->fdSets[i]); } +#endif } // @@ -59,82 +137,138 @@ LIBCOM_API fdManager::fdManager () : // LIBCOM_API fdManager::~fdManager() { - fdReg *pReg; + fdReg* pReg; - while ( (pReg = this->regList.get()) ) { + while ((pReg = priv->regList.get())) { pReg->state = fdReg::limbo; pReg->destroy(); } - while ( (pReg = this->activeList.get()) ) { + while ((pReg = priv->activeList.get())) { pReg->state = fdReg::limbo; pReg->destroy(); } - delete this->pTimerQueue; - delete [] this->fdSetsPtr; osiSockRelease(); } // // fdManager::process() // -LIBCOM_API void fdManager::process (double delay) +LIBCOM_API void fdManager::process(double delay) { - this->lazyInitTimerQueue (); + priv->lazyInitTimerQueue(); // // no recursion // - if (this->processInProg) { + if (priv->processInProg) return; - } - this->processInProg = true; + priv->processInProg = true; // // One shot at expired timers prior to going into - // select. This allows zero delay timers to arm + // poll/select. This allows zero delay timers to arm // fd writes. We will never process the timer queue // more than once here so that fd activity get serviced // in a reasonable length of time. // - double minDelay = this->pTimerQueue->process(epicsTime::getCurrent()); + double minDelay = priv->pTimerQueue->process(epicsTime::getCurrent()); - if ( minDelay >= delay ) { + if (minDelay >= delay) { minDelay = delay; } - bool ioPending = false; - tsDLIter < fdReg > iter = this->regList.firstIter (); - while ( iter.valid () ) { - FD_SET(iter->getFD(), &this->fdSetsPtr[iter->getType()]); - ioPending = true; +#ifdef FDMGR_USE_POLL + priv->pollfds.clear(); +#endif + + int ioPending = 0; + tsDLIter iter = priv->regList.firstIter(); + while (iter.valid()) { + ++ioPending; + +#ifdef FDMGR_USE_POLL +#if __cplusplus >= 201100L + priv->pollfds.emplace_back(pollfd{ + .fd = iter->getFD(), + .events = WIN_POLLEVENT_FILTER(PollEvents[iter->getType()]) + }); +#else + struct pollfd pollfd; + pollfd.fd = iter->getFD(); + pollfd.events = WIN_POLLEVENT_FILTER(PollEvents[iter->getType()]); + pollfd.revents = 0; + priv->pollfds.push_back(pollfd); +#endif +#endif + +#ifdef FDMGR_USE_SELECT + FD_SET(iter->getFD(), &priv->fdSets[iter->getType()]); +#endif ++iter; } - if ( ioPending ) { + if (ioPending) { +#ifdef FDMGR_USE_POLL + if (minDelay * mSecPerSec > INT_MAX) + minDelay = INT_MAX / mSecPerSec; + + int status = poll(&priv->pollfds[0], // ancient C++ has no vector.data() + ioPending, static_cast(minDelay * mSecPerSec)); + int i = 0; +#endif + +#ifdef FDMGR_USE_SELECT struct timeval tv; - tv.tv_sec = static_cast ( minDelay ); - tv.tv_usec = static_cast ( (minDelay-tv.tv_sec) * uSecPerSec ); + tv.tv_sec = static_cast(minDelay); + tv.tv_usec = static_cast((minDelay-tv.tv_sec) * uSecPerSec); - fd_set * pReadSet = & this->fdSetsPtr[fdrRead]; - fd_set * pWriteSet = & this->fdSetsPtr[fdrWrite]; - fd_set * pExceptSet = & this->fdSetsPtr[fdrException]; - int status = select (this->maxFD, pReadSet, pWriteSet, pExceptSet, &tv); + int status = select(priv->maxFD, + &priv->fdSets[fdrRead], + &priv->fdSets[fdrWrite], + &priv->fdSets[fdrException], &tv); +#endif - this->pTimerQueue->process(epicsTime::getCurrent()); + priv->pTimerQueue->process(epicsTime::getCurrent()); - if ( status > 0 ) { + if (status > 0) { // // Look for activity // - iter=this->regList.firstIter (); - while ( iter.valid () && status > 0 ) { - tsDLIter < fdReg > tmp = iter; + iter = priv->regList.firstIter(); + while (iter.valid() && status > 0) { + tsDLIter tmp = iter; tmp++; - if (FD_ISSET(iter->getFD(), &this->fdSetsPtr[iter->getType()])) { - FD_CLR(iter->getFD(), &this->fdSetsPtr[iter->getType()]); - this->regList.remove(*iter); - this->activeList.add(*iter); + +#ifdef FDMGR_USE_POLL + // In a single threaded application, nothing should have + // changed the order of regList and pollfds by now. + // But just in case... + int isave = i; + while (priv->pollfds[i].fd != iter->getFD() || + priv->pollfds[i].events != WIN_POLLEVENT_FILTER(PollEvents[iter->getType()])) + { + errlogPrintf("fdManager: skipping (removed?) pollfd %d (expected %d)\n", priv->pollfds[i].fd, iter->getFD()); + i++; // skip pollfd of removed items + if (i >= ioPending) { // skip unknown (inserted?) items + errlogPrintf("fdManager: skipping (inserted?) item %d\n", iter->getFD()); + iter = tmp; + tmp++; + if (!iter.valid()) break; + i = isave; + } + } + if (i >= ioPending) break; // any unhandled item stays in regList for next time + + if (priv->pollfds[i++].revents & PollEvents[iter->getType()]) { +#endif + +#ifdef FDMGR_USE_SELECT + if (FD_ISSET(iter->getFD(), &priv->fdSets[iter->getType()])) { + FD_CLR(iter->getFD(), &priv->fdSets[iter->getType()]); +#endif + priv->regList.remove(*iter); + priv->activeList.add(*iter); iter->state = fdReg::active; status--; } @@ -145,8 +279,8 @@ LIBCOM_API void fdManager::process (double delay) // I am careful to prevent problems if they access the // above list while in a "callBack()" routine // - fdReg * pReg; - while ( (pReg = this->activeList.get()) ) { + fdReg* pReg; + while ((pReg = priv->activeList.get())) { pReg->state = fdReg::limbo; // @@ -154,45 +288,53 @@ LIBCOM_API void fdManager::process (double delay) // can detect if it was deleted // during the call back // - this->pCBReg = pReg; + priv->pCBReg = pReg; pReg->callBack(); - if (this->pCBReg != NULL) { + if (priv->pCBReg != NULL) { // // check only after we see that it is non-null so // that we don't trigger bounds-checker dangling pointer // error // - assert (this->pCBReg==pReg); - this->pCBReg = 0; + assert(priv->pCBReg == pReg); + priv->pCBReg = NULL; if (pReg->onceOnly) { pReg->destroy(); } else { - this->regList.add(*pReg); + priv->regList.add(*pReg); pReg->state = fdReg::pending; } } } } - else if ( status < 0 ) { + else if (status < 0) { int errnoCpy = SOCKERRNO; +#ifdef FDMGR_USE_SELECT // don't depend on flags being properly set if // an error is returned from select - for ( size_t i = 0u; i < fdrNEnums; i++ ) { - FD_ZERO ( &fdSetsPtr[i] ); + for (size_t i = 0u; i < fdrNEnums; i++) { + FD_ZERO(&priv->fdSets[i]); } +#endif // - // print a message if its an unexpected error + // print a message if it's an unexpected error // - if ( errnoCpy != SOCK_EINTR ) { + if (errnoCpy != SOCK_EINTR) { char sockErrBuf[64]; - epicsSocketConvertErrnoToString ( - sockErrBuf, sizeof ( sockErrBuf ) ); - fprintf ( stderr, - "fdManager: select failed because \"%s\"\n", - sockErrBuf ); + epicsSocketConvertErrnoToString( + sockErrBuf, sizeof(sockErrBuf)); + errlogPrintf("fdManager: " +#ifdef FDMGR_USE_POLL + "poll()" +#endif +#ifdef FDMGR_USE_SELECT + "select()" +#endif + " failed because \"%s\"\n", + sockErrBuf); } } } @@ -203,9 +345,9 @@ LIBCOM_API void fdManager::process (double delay) * of select() */ epicsThreadSleep(minDelay); - this->pTimerQueue->process(epicsTime::getCurrent()); + priv->pTimerQueue->process(epicsTime::getCurrent()); } - this->processInProg = false; + priv->processInProg = false; } // @@ -222,7 +364,7 @@ void fdReg::destroy() // fdReg::~fdReg() { - this->manager.removeReg(*this); + manager.removeReg(*this); } // @@ -230,57 +372,62 @@ fdReg::~fdReg() // void fdReg::show(unsigned level) const { - printf ("fdReg at %p\n", (void *) this); - if (level>1u) { - printf ("\tstate = %d, onceOnly = %d\n", - this->state, this->onceOnly); + printf("fdReg at %p\n", this); + if (level > 1u) { + printf("\tstate = %d, onceOnly = %d\n", + state, onceOnly); } - this->fdRegId::show(level); + fdRegId::show(level); } // // fdRegId::show() // -void fdRegId::show ( unsigned level ) const +void fdRegId::show(unsigned level) const { - printf ( "fdRegId at %p\n", - static_cast ( this ) ); - if ( level > 1u ) { - printf ( "\tfd = %d, type = %d\n", - int(this->fd), this->type ); + printf("fdRegId at %p\n", this); + if (level > 1u) { + printf("\tfd = %" +#if defined(_WIN32) + "I" +#endif + "d, type = %d\n", + fd, type); } } // -// fdManager::installReg () +// fdManager::installReg() // -void fdManager::installReg (fdReg ®) +void fdManager::installReg(fdReg ®) { - this->maxFD = max ( this->maxFD, reg.getFD()+1 ); - // Most applications will find that its important to push here to +#ifdef FDMGR_USE_SELECT + priv->maxFD = std::max(priv->maxFD, reg.getFD()+1); +#endif + // Most applications will find that it's important to push here to // the front of the list so that transient writes get executed // first allowing incoming read protocol to find that outgoing // buffer space is newly available. - this->regList.push ( reg ); + priv->regList.push(reg); reg.state = fdReg::pending; - int status = this->fdTbl.add ( reg ); - if ( status != 0 ) { - throwWithLocation ( fdInterestSubscriptionAlreadyExits () ); + int status = priv->fdTbl.add(reg); + if (status != 0) { + throwWithLocation(fdInterestSubscriptionAlreadyExits()); } +// errlogPrintf("fdManager::adding fd %d\n", reg.getFD()); } // -// fdManager::removeReg () +// fdManager::removeReg() // -void fdManager::removeReg (fdReg ®In) +void fdManager::removeReg(fdReg ®In) { - fdReg *pItemFound; + fdReg* pItemFound; - pItemFound = this->fdTbl.remove (regIn); - if (pItemFound!=®In) { - fprintf(stderr, - "fdManager::removeReg() bad fd registration object\n"); + pItemFound = priv->fdTbl.remove(regIn); + if (pItemFound != ®In) { + errlogPrintf("fdManager::removeReg() bad fd registration object\n"); return; } @@ -288,16 +435,16 @@ void fdManager::removeReg (fdReg ®In) // signal fdManager that the fdReg was deleted // during the call back // - if (this->pCBReg == ®In) { - this->pCBReg = 0; + if (priv->pCBReg == ®In) { + priv->pCBReg = NULL; } switch (regIn.state) { case fdReg::active: - this->activeList.remove (regIn); + priv->activeList.remove(regIn); break; case fdReg::pending: - this->regList.remove (regIn); + priv->regList.remove(regIn); break; case fdReg::limbo: break; @@ -309,49 +456,55 @@ void fdManager::removeReg (fdReg ®In) } regIn.state = fdReg::limbo; - FD_CLR(regIn.getFD(), &this->fdSetsPtr[regIn.getType()]); +#ifdef FDMGR_USE_SELECT + FD_CLR(regIn.getFD(), &priv->fdSets[regIn.getType()]); +#endif + +// errlogPrintf("fdManager::removing fd %d\n", regIn.getFD()); } // -// fdManager::reschedule () +// fdManager::reschedule() // NOOP - this only runs single threaded, and therefore they can only // add a new timer from places that will always end up in a reschedule // -void fdManager::reschedule () +void fdManager::reschedule() { } -double fdManager::quantum () +double fdManager::quantum() { - return this->sleepQuantum; + return priv->sleepQuantum; } // // lookUpFD() // -LIBCOM_API fdReg *fdManager::lookUpFD (const SOCKET fd, const fdRegType type) +LIBCOM_API fdReg* fdManager::lookUpFD(const SOCKET fd, const fdRegType type) { - if (fd<0) { + if (fd < 0) { return NULL; } - fdRegId id (fd,type); - return this->fdTbl.lookup(id); + fdRegId id(fd,type); + return priv->fdTbl.lookup(id); } // // fdReg::fdReg() // -fdReg::fdReg (const SOCKET fdIn, const fdRegType typIn, +fdReg::fdReg(const SOCKET fdIn, const fdRegType typIn, const bool onceOnlyIn, fdManager &managerIn) : - fdRegId (fdIn,typIn), state (limbo), - onceOnly (onceOnlyIn), manager (managerIn) + fdRegId(fdIn,typIn), state(limbo), + onceOnly(onceOnlyIn), manager(managerIn) { +#ifdef FDMGR_USE_SELECT if (!FD_IN_FDSET(fdIn)) { - fprintf (stderr, "%s: fd > FD_SETSIZE ignored\n", + errlogPrintf("%s: fd > FD_SETSIZE ignored\n", __FILE__); return; } - this->manager.installReg (*this); +#endif + manager.installReg(*this); } template class resTable; diff --git a/modules/libcom/src/fdmgr/fdManager.h b/modules/libcom/src/fdmgr/fdManager.h index 3112b26f4..e998131aa 100644 --- a/modules/libcom/src/fdmgr/fdManager.h +++ b/modules/libcom/src/fdmgr/fdManager.h @@ -19,6 +19,16 @@ #ifndef fdManagerH_included #define fdManagerH_included +#include +namespace epics { +#if __cplusplus>=201103L +template +using auto_ptr = std::unique_ptr; +#else +using std::auto_ptr; +#endif +} + #include "libComAPI.h" // reset share lib defines #include "tsDLList.h" #include "resourceLib.h" @@ -37,27 +47,27 @@ class LIBCOM_API fdRegId { public: - fdRegId (const SOCKET fdIn, const fdRegType typeIn) : + fdRegId(const SOCKET fdIn, const fdRegType typeIn) : fd(fdIn), type(typeIn) {} - SOCKET getFD () const + SOCKET getFD() const { - return this->fd; + return fd; } - fdRegType getType () const + fdRegType getType() const { - return this->type; + return type; } - bool operator == (const fdRegId &idIn) const + bool operator == (const fdRegId& idIn) const { - return this->fd == idIn.fd && this->type==idIn.type; + return fd == idIn.fd && type == idIn.type; } - resTableIndex hash () const; + resTableIndex hash() const; - virtual void show (unsigned level) const; + virtual void show(unsigned level) const; virtual ~fdRegId() {} private: @@ -70,43 +80,31 @@ private: // // file descriptor manager // -class fdManager : public epicsTimerQueueNotify { +class LIBCOM_API fdManager : public epicsTimerQueueNotify { public: // // exceptions // class fdInterestSubscriptionAlreadyExits {}; - LIBCOM_API fdManager (); - LIBCOM_API virtual ~fdManager (); - LIBCOM_API void process ( double delay ); // delay parameter is in seconds + fdManager(); + virtual ~fdManager(); + void process(double delay); // delay parameter is in seconds // returns NULL if the fd is unknown - LIBCOM_API class fdReg *lookUpFD (const SOCKET fd, const fdRegType type); + class fdReg* lookUpFD(const SOCKET fd, const fdRegType type); - epicsTimer & createTimer (); + epicsTimer& createTimer(); private: - tsDLList < fdReg > regList; - tsDLList < fdReg > activeList; - resTable < fdReg, fdRegId > fdTbl; - const double sleepQuantum; - fd_set * fdSetsPtr; - epicsTimerQueuePassive * pTimerQueue; - SOCKET maxFD; - bool processInProg; - // - // Set to fdreg when in call back - // and nill otherwise - // - fdReg * pCBReg; - void reschedule (); - double quantum (); - void installReg (fdReg ®); - void removeReg (fdReg ®); - void lazyInitTimerQueue (); - fdManager ( const fdManager & ); - fdManager & operator = ( const fdManager & ); + epics::auto_ptr priv; + + void reschedule(); + double quantum(); + void installReg(fdReg& reg); + void removeReg(fdReg& reg); + fdManager(const fdManager&); + fdManager& operator = (const fdManager&); friend class fdReg; }; @@ -126,11 +124,11 @@ class LIBCOM_API fdReg : public: - fdReg (const SOCKET fdIn, const fdRegType type, - const bool onceOnly=false, fdManager &manager = fileDescriptorManager); - virtual ~fdReg (); + fdReg(const SOCKET fdIn, const fdRegType type, + const bool onceOnly=false, fdManager& manager = fileDescriptorManager); + virtual ~fdReg(); - virtual void show (unsigned level) const; + virtual void show(unsigned level) const; // // Called by the file descriptor manager: @@ -141,7 +139,7 @@ public: // // fdReg::destroy() does a "delete this" // - virtual void destroy (); + virtual void destroy(); private: enum state {active, pending, limbo}; @@ -153,32 +151,32 @@ private: // lifetime of a fdReg object if the constructor // specified "onceOnly" // - virtual void callBack ()=0; + virtual void callBack() = 0; unsigned char state; // state enums go here unsigned char onceOnly; - fdManager &manager; + fdManager& manager; - fdReg ( const fdReg & ); - fdReg & operator = ( const fdReg & ); + fdReg(const fdReg&); + fdReg& operator = (const fdReg&); }; // // fdRegId::hash() // -inline resTableIndex fdRegId::hash () const +inline resTableIndex fdRegId::hash() const { const unsigned fdManagerHashTableMinIndexBits = 8; - const unsigned fdManagerHashTableMaxIndexBits = sizeof(SOCKET)*CHAR_BIT; + const unsigned fdManagerHashTableMaxIndexBits = sizeof(SOCKET) * CHAR_BIT; resTableIndex hashid; - hashid = integerHash ( fdManagerHashTableMinIndexBits, - fdManagerHashTableMaxIndexBits, this->fd ); + hashid = integerHash(fdManagerHashTableMinIndexBits, + fdManagerHashTableMaxIndexBits, fd); // // also evenly distribute based on the type of fdRegType // - hashid ^= this->type; + hashid ^= type; // // the result here is always masked to the @@ -187,18 +185,5 @@ inline resTableIndex fdRegId::hash () const return hashid; } -inline void fdManager::lazyInitTimerQueue () -{ - if ( ! this->pTimerQueue ) { - this->pTimerQueue = & epicsTimerQueuePassive::create ( *this ); - } -} - -inline epicsTimer & fdManager::createTimer () -{ - this->lazyInitTimerQueue (); - return this->pTimerQueue->createTimer (); -} - #endif // fdManagerH_included diff --git a/modules/libcom/test/Makefile b/modules/libcom/test/Makefile index a3893661f..6e5bd19a7 100644 --- a/modules/libcom/test/Makefile +++ b/modules/libcom/test/Makefile @@ -209,6 +209,11 @@ aslibtest_SRCS += aslibtest.c testHarness_SRCS += aslibtest.c TESTS += aslibtest +TESTPROD_HOST += fdManagerTest +fdManagerTest_SRCS += fdManagerTest.cpp +testHarness_SRCS += fdManagerTest.cpp +TESTS += fdManagerTest + # Perl module tests: TESTS += macLib @@ -315,11 +320,6 @@ TESTPROD_HOST += buckTest buckTest_SRCS += buckTest.c testHarness_SRCS += buckTest.c -#TESTPROD_HOST += fdmgrTest -fdmgrTest_SRCS += fdmgrTest.c -fdmgrTest_LIBS += ca -# FIXME: program never exits. - TESTPROD_HOST += epicsAtomicPerform epicsAtomicPerform_SRCS += epicsAtomicPerform.cpp testHarness_SRCS += epicsAtomicPerform.cpp diff --git a/modules/libcom/test/epicsRunLibComTests.c b/modules/libcom/test/epicsRunLibComTests.c index 54c46a68e..d560fd46a 100644 --- a/modules/libcom/test/epicsRunLibComTests.c +++ b/modules/libcom/test/epicsRunLibComTests.c @@ -54,6 +54,7 @@ int initHookTest(void); int ipAddrToAsciiTest(void); int macDefExpandTest(void); int macLibTest(void); +int fdManagerTest(void); int osiSockTest(void); int ringBytesTest(void); int ringPointerTest(void); @@ -110,6 +111,7 @@ void epicsRunLibComTests(void) runTest(ipAddrToAsciiTest); runTest(macDefExpandTest); runTest(macLibTest); + runTest(fdManagerTest); runTest(osiSockTest); runTest(ringBytesTest); runTest(ringPointerTest); diff --git a/modules/libcom/test/fdManagerTest.cpp b/modules/libcom/test/fdManagerTest.cpp new file mode 100644 index 000000000..0dc1b330c --- /dev/null +++ b/modules/libcom/test/fdManagerTest.cpp @@ -0,0 +1,316 @@ +/*************************************************************************\ +* Copyright (c) 2025 Michael Davidsaver +* SPDX-License-Identifier: EPICS +* EPICS Base is distributed subject to a Software License Agreement found +* in file LICENSE that is included with this distribution. +\*************************************************************************/ + +#include + +#include + +#include +#include +#include +#include +#include + +#include +#include + +#if __cplusplus<201103L +# define final +# define override +#endif + +namespace { + +void set_non_blocking(SOCKET sd) +{ + osiSockIoctl_t yes = true; + int status = socket_ioctl ( sd, + FIONBIO, & yes); + if(status) + testFail("set_non_blocking fails : %d", SOCKERRNO); +} + +// RAII for epicsTimer +struct ScopedTimer { + epicsTimer& timer; + explicit + ScopedTimer(epicsTimer& t) :timer(t) {} + ~ScopedTimer() { timer.destroy(); } +}; +struct ScopedFDReg { + fdReg * const reg; + explicit + ScopedFDReg(fdReg* reg) :reg(reg) {} + ~ScopedFDReg() { reg->destroy(); } +}; + +// RAII for socket +struct Socket { + SOCKET sd; + Socket() :sd(INVALID_SOCKET) {} + explicit + Socket(SOCKET sd) :sd(sd) {} + Socket(int af, int type) + :sd(epicsSocketCreate(af, type, 0)) + { + if(sd==INVALID_SOCKET) + testAbort("failed to allocate socket %d %d", af, type); + } +private: + Socket(const Socket&); + Socket& operator=(const Socket&); +public: + ~Socket() { + if(sd!=INVALID_SOCKET) + epicsSocketDestroy(sd); + } + void swap(Socket& o) { + std::swap(sd, o.sd); + } + osiSockAddr bind() + { + osiSockAddr addr = {0}; + addr.ia.sin_family = AF_INET; + addr.ia.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + + if(::bind(sd, &addr.sa, sizeof(addr))) + testAbort("Unable to bind lo : %d", SOCKERRNO); + + osiSockAddr ret; + osiSocklen_t addrlen = sizeof(ret); + if(getsockname(sd, &ret.sa, &addrlen)) + testAbort("Unable to getsockname : %d", SOCKERRNO); + (void)addrlen; + + if(listen(sd, 1)) + testAbort("Unable to listen : %d", SOCKERRNO); + + return ret; + } +}; + +struct DoConnect final : public epicsThreadRunable { + const SOCKET sd; + osiSockAddr to; + DoConnect(SOCKET sd, const osiSockAddr& to) + :sd(sd) + ,to(to) + {} + + void run() override final { + int err = connect(sd, &to.sa, sizeof(to)); + testOk(err==0, "connect() %d %d", err, SOCKERRNO); + } +}; + +struct DoAccept final : public epicsThreadRunable { + const SOCKET sd; + Socket peer; + osiSockAddr peer_addr; + explicit + DoAccept(SOCKET sd) :sd(sd) {} + void run() override final { + osiSocklen_t len(sizeof(peer_addr)); + Socket temp(accept(sd, &peer_addr.sa, &len)); + if(temp.sd==INVALID_SOCKET) + testFail("accept() -> %d", SOCKERRNO); + temp.swap(peer); + } +}; + +struct DoRead final : public epicsThreadRunable { + const SOCKET sd; + char* buf; + unsigned buflen; + int n; + DoRead(SOCKET sd, char* buf, unsigned buflen): sd(sd), buf(buf), buflen(buflen), n(0) {} + void run() override final { + n = recv(sd, buf, buflen, 0); + if(n<0) + testFail("read() -> %d, %d", n, SOCKERRNO); + } +}; + +struct DoWriteAll final : public epicsThreadRunable { + const SOCKET sd; + const char* buf; + unsigned buflen; + DoWriteAll(SOCKET sd, const char* buf, unsigned buflen): sd(sd), buf(buf), buflen(buflen) {} + void run() override final { + unsigned nsent = 0; + while(nsent %d, %d", n, SOCKERRNO); + break; + } + nsent += n; + } + } +}; + +struct Expire final : public epicsTimerNotify { + bool expired; + + Expire() :expired(false) {} + virtual ~Expire() {} + virtual expireStatus expire(const epicsTime &) override final + { + if(!expired) { + expired = true; + testPass("expired"); + } else { + testFail("re-expired?"); + } + return noRestart; + } +}; + +struct Event final : public fdReg { + int* ready; + + Event(fdManager& mgr, fdRegType evt, SOCKET sd, int* ready) + :fdReg(sd, evt, false, mgr) + ,ready(ready) + {} + virtual ~Event() {} + + virtual void callBack() override final { + epics::atomic::set(*ready, 1); + } +}; + +struct OneShot final : public fdReg { + int *mask; + + OneShot(fdManager& mgr, fdRegType evt, SOCKET sd, int *mask) + :fdReg(sd, evt, true, mgr) + ,mask(mask) + {} + virtual ~OneShot() { + epics::atomic::add(*mask, 2); + } + + virtual void callBack() override final { + epics::atomic::add(*mask, 1); + } +}; + +void testEmpty() +{ + fdManager empty; + empty.process(0.1); // ca-gateway always passes 0.01 + testPass("Did nothing"); +} + +void testOnlyTimer() +{ + fdManager mgr; + Expire trig, never; + ScopedTimer trig_timer(mgr.createTimer()), + never_timer(mgr.createTimer()); + epicsTime now(epicsTime::getCurrent()); + trig_timer.timer.start(trig, now+0.1); + never_timer.timer.start(never, now+9999999.0); + mgr.process(0.2); + testOk1(trig.expired); + testOk1(!never.expired); +} + +void testSockIO() +{ + fdManager mgr; + Socket listener(AF_INET, SOCK_STREAM); + set_non_blocking(listener.sd); + osiSockAddr servAddr(listener.bind()); + + Socket client(AF_INET, SOCK_STREAM); + Socket server; + // listen() / connect() + { + int readable = 0; + Event evt(mgr, fdrRead, listener.sd, &readable); + DoConnect conn(client.sd, servAddr); + epicsThread connector(conn, "connect", 0); + connector.start(); + + mgr.process(5.0); + + testOk1(readable); + + DoAccept acc(listener.sd); + acc.run(); + server.swap(acc.peer); + } + set_non_blocking(server.sd); + // writeable + { + int mask = 0; + new OneShot(mgr, fdrWrite, server.sd, &mask); + + mgr.process(5.0); + + testOk(mask==3, "OneShot event mask %x", mask); + } + // read + { + const char msg[] = "testing"; + int readable = 0; + Event evt(mgr, fdrRead, server.sd, &readable); + DoWriteAll op(client.sd, msg, sizeof(msg)-1); + epicsThread writer(op, "writer", 0); + writer.start(); + + mgr.process(5.0); + + testOk1(readable); + + char buf[sizeof(msg)] = ""; + DoRead(server.sd, buf, sizeof(buf)-1).run(); + buf[sizeof(buf)-1] = '\0'; + testOk(strcmp(msg, buf)==0, "%s == %s", msg, buf); + } + // timer while unreadable + { + + int readable = 0; + Event evt(mgr, fdrRead, server.sd, &readable); + Expire tmo; + ScopedTimer timer(mgr.createTimer()); + timer.timer.start(tmo, epicsTime::getCurrent()); // immediate + + mgr.process(1.0); + + testOk1(!readable); + testOk1(tmo.expired); + } + // notification on close() + { + int readable = 0; + Event evt(mgr, fdrRead, server.sd, &readable); + + shutdown(client.sd, SHUT_RDWR); + //Socket().swap(client); + + mgr.process(1.0); + + testOk1(readable); + } +} + +} // namespace + +MAIN(fdManagerTest) +{ + testPlan(13); + osiSockAttach(); + testEmpty(); + testOnlyTimer(); + testSockIO(); + osiSockRelease(); + return testDone(); +} diff --git a/modules/libcom/test/fdmgrTest.c b/modules/libcom/test/fdmgrTest.c deleted file mode 100644 index a7f91d493..000000000 --- a/modules/libcom/test/fdmgrTest.c +++ /dev/null @@ -1,139 +0,0 @@ -/*************************************************************************\ -* Copyright (c) 2006 UChicago Argonne LLC, as Operator of Argonne -* National Laboratory. -* Copyright (c) 2002 The Regents of the University of California, as -* Operator of Los Alamos National Laboratory. -* SPDX-License-Identifier: EPICS -* EPICS BASE is distributed subject to a Software License Agreement found -* in file LICENSE that is included with this distribution. -\*************************************************************************/ - -#include -#include - -#include "fdmgr.h" -#include "epicsTime.h" -#include "epicsAssert.h" -#include "cadef.h" - -#define verify(exp) ((exp) ? (void)0 : \ - epicsAssert(__FILE__, __LINE__, #exp, epicsAssertAuthor)) - -static const unsigned uSecPerSec = 1000000; - -typedef struct cbStructCreateDestroyFD { - fdctx *pfdm; - int trig; -} cbStructCreateDestroyFD; - -void fdHandler (void *pArg) -{ - cbStructCreateDestroyFD *pCBFD = (cbStructCreateDestroyFD *) pArg; - - printf ("triggered\n"); - pCBFD->trig = 1; -} - -void fdCreateDestroyHandler (void *pArg, int fd, int open) -{ - cbStructCreateDestroyFD *pCBFD = (cbStructCreateDestroyFD *) pArg; - int status; - - if (open) { - printf ("new fd = %d\n", fd); - status = fdmgr_add_callback (pCBFD->pfdm, fd, fdi_read, fdHandler, pArg); - verify (status==0); - } - else { - printf ("terminated fd = %d\n", fd); - status = fdmgr_clear_callback (pCBFD->pfdm, fd, fdi_read); - verify (status==0); - } -} - -typedef struct cbStuctTimer { - epicsTimeStamp time; - int done; -} cbStruct; - -void alarmCB (void *parg) -{ - cbStruct *pCBS = (cbStruct *) parg; - epicsTimeGetCurrent (&pCBS->time); - pCBS->done = 1; -} - -void testTimer (fdctx *pfdm, double delay) -{ - int status; - fdmgrAlarmId aid; - struct timeval tmo; - epicsTimeStamp begin; - cbStruct cbs; - double measuredDelay; - double measuredError; - - epicsTimeGetCurrent (&begin); - cbs.done = 0; - tmo.tv_sec = (time_t) delay; - tmo.tv_usec = (unsigned long) ((delay - tmo.tv_sec) * uSecPerSec); - aid = fdmgr_add_timeout (pfdm, &tmo, alarmCB, &cbs); - verify (aid!=fdmgrNoAlarm); - - while (!cbs.done) { - tmo.tv_sec = (time_t) delay; - tmo.tv_usec = (unsigned long) ((delay - tmo.tv_sec) * uSecPerSec); - status = fdmgr_pend_event (pfdm, &tmo); - verify (status==0); - } - - measuredDelay = epicsTimeDiffInSeconds (&cbs.time, &begin); - measuredError = fabs (measuredDelay-delay); - printf ("measured delay for %lf sec was off by %lf sec (%lf %%)\n", - delay, measuredError, 100.0*measuredError/delay); -} - -int main (int argc, char **argv) -{ - int status; - fdctx *pfdm; - cbStructCreateDestroyFD cbsfd; - struct timeval tmo; - chid chan; - - pfdm = fdmgr_init (); - verify (pfdm); - - SEVCHK (ca_task_initialize(), NULL); - cbsfd.pfdm = pfdm; - SEVCHK (ca_add_fd_registration (fdCreateDestroyHandler, &cbsfd), NULL); - - /* - * timer test - */ - testTimer (pfdm, 0.001); - testTimer (pfdm, 0.01); - testTimer (pfdm, 0.1); - testTimer (pfdm, 1.0); - - if (argc==2) { - SEVCHK(ca_search (argv[1], &chan), NULL); - } - - while (1) { - tmo.tv_sec = 0; - tmo.tv_usec = 100000; - cbsfd.trig = 0; - status = fdmgr_pend_event (pfdm, &tmo); - verify (status==0); - ca_poll (); - } - - status = fdmgr_delete (pfdm); - verify (status==0); - - printf ( "Test Complete\n" ); - - return 0; -} -