diff --git a/test/testing_darwinunix.c b/test/testing_darwinunix.c index 4ce32e06..c0e8d2a3 100644 --- a/test/testing_darwinunix.c +++ b/test/testing_darwinunix.c @@ -18,103 +18,6 @@ // TODO don't start the timer on any platform until after we call setjmp(); also decide whether to start the timer before or after resuming the thread on Windows -static jmp_buf timeout_ret; - -static void onTimeout(int sig) -{ - longjmp(timeout_ret, 1); -} - -void testingprivRunWithTimeout(testingT *t, const char *file, long line, int64_t timeout, void (*f)(testingT *t, void *data), void *data, const char *comment, int failNowOnError) -{ - char *timeoutstr; - void (*prevsig)(int); - struct itimerval duration, prevDuration; - int setitimerError = 0; - - timeoutstr = testingNsecString(timeout); - prevsig = signal(SIGALRM, onTimeout); - - duration.it_interval.tv_sec = 0; - duration.it_interval.tv_usec = 0; - duration.it_value.tv_sec = timeout / testingNsecPerSec; - duration.it_value.tv_usec = (timeout % testingNsecPerSec) / testingNsecPerUsec; - if (setitimer(ITIMER_REAL, &duration, &prevDuration) != 0) { - setitimerError = errno; - testingprivTLogfFull(t, file, line, "error applying %s timeout: %s", comment, strerror(setitimerError)); - testingTFail(t); - goto out; - } - - if (setjmp(timeout_ret) == 0) { - (*f)(t, data); - failNowOnError = 0; // we succeeded - } else { - testingprivTLogfFull(t, file, line, "%s timeout passed (%s)", comment, timeoutstr); - testingTFail(t); - } - -out: - if (setitimerError == 0) - setitimer(ITIMER_REAL, &prevDuration, NULL); - signal(SIGALRM, prevsig); - testingFreeNsecString(timeoutstr); - if (failNowOnError) - testingTFailNow(t); -} - -void testingSleep(int64_t nsec) -{ - struct timespec duration, remaining; - void (*prevsig)(int); - sigset_t set, prevSet; - struct itimerval setiDuration, prevSetiDuration; - unsigned sec; - - duration.tv_sec = nsec / testingNsecPerSec; - duration.tv_nsec = nsec % testingNsecPerSec; - for (;;) { - errno = 0; - if (nanosleep(&duration, &remaining) == 0) - return; - if (errno != EINTR) - break; - duration = remaining; - } - - // if we got here, nanosleep() failed outright - if (sigemptyset(&set) != 0) - goto fallback; - if (sigaddset(&set, SIGABRT) != 0) - goto fallback; - if (pthread_sigmask(SIG_BLOCK, &set, &prevSet) != 0) - goto fallback; - prevsig = signal(SIGALRM, SIG_IGN); - setiDuration.it_interval.tv_sec = 0; - setiDuration.it_interval.tv_usec = 0; - // keep using duration for this in case nanosleep() was interrupted before it failed - setiDuration.it_value.tv_sec = duration.tv_sec; - setiDuration.it_value.tv_usec = duration.tv_nsec / testingNsecPerUsec; - if (setitimer(ITIMER_REAL, &setiDuration, &prevSetiDuration) != 0) { - pthread_sigmask(SIG_SETMASK, &prevSet, NULL); - signal(SIGALRM, prevsig); - goto fallback; - } - // TODO can this return an errno other than EINTR? - sigsuspend(&prevSet); - setitimer(ITIMER_REAL, &prevSetiDuration, NULL); - signal(SIGALRM, prevsig); - return; - -fallback: - // hopefully we never reach this point, because it has the least granularity of all, but there are no errors, so... - sec = duration.tv_sec; - if (duration.tv_nsec > 0) - sec++; - while (sec > 0) - sec = sleep(sec); -} - struct testingThread { pthread_t thread; void (*f)(void *data); diff --git a/test/timer_darwinunix.c b/test/timer_darwinunix.c index b35c088b..1dbc9a60 100644 --- a/test/timer_darwinunix.c +++ b/test/timer_darwinunix.c @@ -1,13 +1,18 @@ // 3 may 2019 +// TODO pin down minimum POSIX versions (depends on what macOS 10.8 conforms to and what GLib/GTK+ require) +#define _POSIX_C_SOURCE 200112L +#include +#include +#include +#include +#include +#include +#include +#include +#include #ifdef __APPLE__ #include #include -#else -#include -#include -#include -#include -#include #endif #include "timer.h" @@ -81,3 +86,187 @@ timerDuration timerTimeSub(timerTime end, timerTime start) } #endif + +struct timeoutParams { + jmp_buf retpos; + timer_t timer; + struct sigaction oldsig; +}; + +static struct timeoutParams p; + +static void onTimeout(int sig, siginfo_t *info, void *ctx) +{ + if (info->si_value.sival_ptr == &p) + longjmp(p.retpos, 1); + // otherwise, call the overloaded SIGALRM handler + if ((p.oldsig.sa_flags & SA_SIGINFO) != 0) { + (*(p.oldsig.sa_sigaction))(sig, info, ctx); + return; + } + if (p.oldsig.sa_handler == SIG_IGN) + return; + if (p.oldsig.sa_handler == SIG_DFL) { + // SIG_DFL for SIGALRM is to terminate the program + // because POSIX doesn't specify how to convert from signal number to exit code, we will have to do this instead + // (POSIX does say these should be safe to call unless the signal was explicitly raised, which we aren't doing, and timer_create() isn't documented as doing that either...) + signal(sig, SIG_DFL); + raise(sig); + } + (*(p.oldsig.sa_handler))(sig); +} + +// POSIX doesn't have atomic operations :| +// We could have special OS X-specific code that uses OSAtomic.h, but they have signedness type mismatches :| :| +// There's also the GCC intrinsic atomic operations, but I cannot find a way to portably detect their presence :| :| :| +// And pthread_mutex_lock() and pthread_mutex_unlock() CAN fail :| :| :| :| + +static pthread_mutex_t nonReentranceMutex = PTHREAD_MUTEX_INITIALIZER; +static volatile uint32_t nonReentranceInCall = 0; + +static void mustpthread_mutex_lock(pthread_mutex_t *mu) +{ + int err; + + err = pthread_mutex_lock(mu); + if (err != 0) { + fprintf(stderr, "*** internal error in timerRunWithTimeout(): pthread_mutex_lock() failed: %s (%d)\n", strerror(err), err); + abort(); + } +} + +static void mustpthread_mutex_unlock(pthread_mutex_t *mu) +{ + int err; + + err = pthread_mutex_unlock(mu); + if (err != 0) { + fprintf(stderr, "*** internal error in timerRunWithTimeout(): pthread_mutex_unlock() failed: %s (%d)\n", strerror(err), err); + abort(); + } +} + +static int setupNonReentrance(void) +{ + int err; + + mustpthread_mutex_lock(&nonReentranceMutex); + err = 0; + if (nonReentranceInCall) + err = EALREADY; + else + nonReentranceInCall = 1; + mustpthread_mutex_unlock(&nonReentranceMutex); + return err; +} + +static void teardownNonReentrance(void) +{ + mustpthread_mutex_lock(&nonReentranceMutex); + nonReentranceInCall = 0; + mustpthread_mutex_unlock(&nonReentranceMutex); +} + +timerSysError timerRunWithTimeout(timerDuration d, void (*f)(void *data), void *data, int *timedOut) +{ + sigset_t sigalrm, allsigs; + sigset_t prevMask; + volatile int restorePrevMask = 0; + struct sigevent event; + volatile int destroyTimer = 0; + struct sigaction sig; + volatile int restoreSignal = 0; + struct itimerspec duration; + int err = 0; + + *timedOut = 0; + err = setupNonReentrance(); + if (err != 0) + return (timerSysError) err; + + memset(&p, 0, sizeof (struct timeoutParams)); + + errno = 0; + if (sigemptyset(&sigalrm) != 0) + return (timerSysError) errno; + errno = 0; + if (sigaddset(&sigalrm, SIGALRM) != 0) + return (timerSysError) errno; + errno = 0; + if (sigfillset(&allsigs) != 0) + return (timerSysError) errno; + + err = pthread_sigmask(SIG_BLOCK, &sigalrm, &prevMask); + if (err != 0) + return (timerSysError) err; + restorePrevMask = 1; + + memset(&event, 0, sizeof (struct sigevent)); + event.sigev_notify = SIGEV_SIGNAL; + event.sigev_signo = SIGALRM; + event.sigev_value.sival_ptr = &p; + errno = 0; + if (timer_create(CLOCK_REALTIME, &event, &(p.timer)) != 0) { + err = errno; + goto out; + } + destroyTimer = 1; + + if (setjmp(p.retpos) == 0) { + sig.sa_mask = &allsigs; + sig.sa_flags = SA_SIGINFO; + sig.sa_sigaction = onTimeout; + errno = 0; + if (sigaction(SIGALRM, &sig, &(p.oldsig)) != 0) { + err = errno; + goto out; + } + restoreSignal = 1; + + duration.it_interval.tv_sec = 0; + duration.it_interval.tv_nsec = 0; + duration.it_value.tv_sec = d / testingNsecPerSec; + duration.it_value.tv_nsec = d % testingNsecPerSec; + errno = 0; + if (timer_settimer(p.timer, 0, &duration, NULL) != 0) { + err = errno; + goto out; + } + // and fire away + err = pthread_sigmask(SIG_UNBLOCK, &sigalrm, NULL); + if (err != 0) + goto out; + + (*f)(data); + } else + *timedOut = 1; + err = 0; + +out: + if (restoreSignal) + sigaction(SIGALRM, &(p.oldsig), NULL); + if (destroyTimer) + timer_delete(p.timer); + if (restorePrevMask) + pthread_sigmask(SIG_SETMASK, &prevMask, NULL); + teardownNonReentrance(); + return (timerSysError) err; +} + +timerSysError timerSleep(timerDuration d) +{ + struct timespec duration, remaining; + int err; + + duration.tv_sec = d / testingNsecPerSec; + duration.tv_nsec = d % testingNsecPerSec; + for (;;) { + errno = 0; + if (nanosleep(&duration, &remaining) == 0) + return 0; + err = errno; + if (err != EINTR) + return (timerSysError) err; + duration = remaining; + } +} diff --git a/test/timer_windows.c b/test/timer_windows.c index 549354c3..582a63bb 100644 --- a/test/timer_windows.c +++ b/test/timer_windows.c @@ -206,10 +206,6 @@ struct timeoutParams { HRESULT hr; }; -static DWORD timeoutParamsSlot; -static HRESULT timeoutParamsHRESULT = S_OK; -static INIT_ONCE timeoutParamsOnce = INIT_ONCE_STATIC_INIT; - static void onTimeout(void) { struct timeoutParams *p; @@ -218,6 +214,10 @@ static void onTimeout(void) longjmp(p->retpos, 1); } +static DWORD timeoutParamsSlot; +static HRESULT timeoutParamsHRESULT = S_OK; +static INIT_ONCE timeoutParamsOnce = INIT_ONCE_STATIC_INIT; + static BOOL CALLBACK timeoutParamsSlotInit(PINIT_ONCE once, PVOID param, PVOID *ctx) { SetLastError(0); @@ -317,6 +317,7 @@ static unsigned __stdcall timerThreadProc(void *data) timerSysError timerRunWithTimeout(timerDuration d, void (*f)(void *data), void *data, int *timedOut) { struct timeoutParams *p; + int doTeardownNonReentrance = 0; MSG msg; volatile HANDLE timerThread = NULL; LARGE_INTEGER duration; @@ -332,6 +333,7 @@ timerSysError timerRunWithTimeout(timerDuration d, void (*f)(void *data), void * hr = setupNonReentrance(p); if (hr != S_OK) goto out; + doTeardownNonReentrance = 1; // to ensure that the PostThreadMessage() above will not fail because the thread doesn't have a message queue PeekMessage(&msg, NULL, WM_USER, WM_USER, PM_NOREMOVE); @@ -401,7 +403,8 @@ out: CloseHandle(p->timer); if (p->targetThread != NULL) CloseHandle(p->targetThread); - teardownNonReentrance(); + if (doTeardownNonReentrance) + teardownNonReentrance(); free(p); return (timerSysError) hr; }