diff --git a/src/lib/libpipefs.js b/src/lib/libpipefs.js index d9a3fed62180e..25ad2cea03cfd 100644 --- a/src/lib/libpipefs.js +++ b/src/lib/libpipefs.js @@ -21,6 +21,19 @@ addToLibrary({ // able to read from the read end after write end is closed. refcnt : 2, timestamp: new Date(), + readableHandlers: [], + registerReadableHanlders: (notifyCallback) => { + if (notifyCallback == null) return; + notifyCallback.registerCleanupFunc(() => { + const i = pipe.readableHandlers.indexOf(notifyCallback); + if (i !== -1) pipe.readableHandlers.splice(i, 1); + }); + pipe.readableHandlers.push(notifyCallback); + }, + notifyReadableHanders: () => { + pipe.readableHandlers.forEach(cb => cb({{{ cDefs.POLLRDNORM }}} | {{{ cDefs.POLLIN }}})); + pipe.readableHandlers = []; + } }; pipe.buckets.push({ @@ -80,7 +93,7 @@ addToLibrary({ blocks: 0, }; }, - poll(stream) { + poll(stream, timeout, notifyCallback) { var pipe = stream.node.pipe; if ((stream.flags & {{{ cDefs.O_ACCMODE }}}) === {{{ cDefs.O_WRONLY }}}) { @@ -92,6 +105,7 @@ addToLibrary({ } } + pipe.registerReadableHanlders(notifyCallback); return 0; }, dup(stream) { @@ -204,6 +218,7 @@ addToLibrary({ if (freeBytesInCurrBuffer >= dataLen) { currBucket.buffer.set(data, currBucket.offset); currBucket.offset += dataLen; + pipe.notifyReadableHanders(); return dataLen; } else if (freeBytesInCurrBuffer > 0) { currBucket.buffer.set(data.subarray(0, freeBytesInCurrBuffer), currBucket.offset); @@ -235,6 +250,7 @@ addToLibrary({ newBucket.buffer.set(data); } + pipe.notifyReadableHanders(); return dataLen; }, close(stream) { diff --git a/src/lib/libpthread.js b/src/lib/libpthread.js index b4885c15f0a15..6a55ddf8aa1df 100644 --- a/src/lib/libpthread.js +++ b/src/lib/libpthread.js @@ -88,6 +88,11 @@ var LibraryPThread = { 'exit', #if PTHREADS_DEBUG || ASSERTIONS '$ptrToString', +#endif +#if PROXY_TO_PTHREAD + '$addThreadToActiveSelectCallbacks', + '$removeThreadFromActiveSelectCallbacks', + '$activeSelectCallbacks', #endif ], $PThread: { @@ -577,6 +582,9 @@ var LibraryPThread = { #endif #if ASSERTIONS assert(worker); +#endif +#if PROXY_TO_PTHREAD + removeThreadFromActiveSelectCallbacks(pthread_ptr); #endif PThread.returnWorkerToPool(worker); }, @@ -621,6 +629,49 @@ var LibraryPThread = { $registerTLSInit: (tlsInitFunc) => PThread.tlsInitFunctions.push(tlsInitFunc), #endif +#if PROXY_TO_PTHREAD + // On the main worker, activeSelectCallbacks records the set of callbacks + // that are allowed to update the shared region. Any callback not in this + // set (i.e. when !isActiveSelectCallback) must not update the region. + // + // Each select syscall invocation must call deactivateSelectCallbacks to + // reset this set, ensuring that callbacks from previous invocations don't + // affect the current one. + // + // If a callback executes after the thread worker has already returned (due + // to a timeout, a readiness notification or other exceptional conditions) + // but before the next deactivation, it may still update the shared region. + // However the thread worker will not read that value and just ignore it. + // + // activeSelectCallbacks records multiple callback lists one per thread + // worker so that each worker can manage its own set of active callbacks + // independently. + $activeSelectCallbacks: {}, + $addThreadToActiveSelectCallbacks__deps: ['malloc'], + $addThreadToActiveSelectCallbacks: (pthread_ptr) => { + activeSelectCallbacks[pthread_ptr] = { + buf: _malloc(8), + callbacks: [], + }; + }, + $removeThreadFromActiveSelectCallbacks: (pthread_ptr) => { + delete activeSelectCallbacks[pthread_ptr]; + }, + $getActiveSelectCallbacks: (pthread_ptr) => { + return activeSelectCallbacks[pthread_ptr]; + }, + $deactivateSelectCallbacks: (pthread_ptr) => { + activeSelectCallbacks[pthread_ptr].callbacks = []; + }, + $activateSelectCallback: (pthread_ptr, cb) => { + activeSelectCallbacks[pthread_ptr].callbacks.push(cb); + }, + $isActiveSelectCallback: (pthread_ptr, cb) => { + return (activeSelectCallbacks[pthread_ptr] != null) && + (activeSelectCallbacks[pthread_ptr].callbacks.indexOf(cb) != -1); + }, +#endif + $spawnThread: (threadParams) => { #if ASSERTIONS assert(!ENVIRONMENT_IS_PTHREAD, 'Internal Error! spawnThread() can only ever be called from main application thread!'); @@ -648,6 +699,9 @@ var LibraryPThread = { arg: threadParams.arg, pthread_ptr: threadParams.pthread_ptr, }; +#if PROXY_TO_PTHREAD + addThreadToActiveSelectCallbacks(threadParams.pthread_ptr); +#endif #if OFFSCREENCANVAS_SUPPORT // Note that we do not need to quote these names because they are only used // in this file, and not from the external worker.js. @@ -692,7 +746,7 @@ var LibraryPThread = { $pthreadCreateProxied__deps: ['__pthread_create_js'], $pthreadCreateProxied: (pthread_ptr, attr, startRoutine, arg) => ___pthread_create_js(pthread_ptr, attr, startRoutine, arg), -#if OFFSCREENCANVAS_SUPPORT +#if OFFSCREENCANVAS_SUPPORT || PROXY_TO_PTHREAD // ASan wraps the emscripten_builtin_pthread_create call in // __lsan::ScopedInterceptorDisabler. Unfortunately, that only disables it on // the thread that made the call. __pthread_create_js gets proxied to the diff --git a/src/lib/libsyscall.js b/src/lib/libsyscall.js index 5f9a0be9afb17..26409c2f55518 100644 --- a/src/lib/libsyscall.js +++ b/src/lib/libsyscall.js @@ -102,6 +102,71 @@ var SyscallsLibrary = { #endif return ret; }, + + getTimeoutInMillis(timeout) { + // select(2) is declared to accept "struct timeval { time_t tv_sec; suseconds_t tv_usec; }". + // However, musl passes the two values to the syscall as an array of long values. + // Note that sizeof(time_t) != sizeof(long) in wasm32. The former is 8, while the latter is 4. + // This means using "C_STRUCTS.timeval\.tv_usec" leads to a wrong offset. + // So, instead, we use POINTER_SIZE. + var tv_sec = ({{{ makeGetValue('timeout', 0, 'i32') }}}), + tv_usec = ({{{ makeGetValue('timeout', POINTER_SIZE, 'i32') }}}); + return (tv_sec + tv_usec / 1000000) * 1000; + }, + + parseSelectFDSet(readfds, writefds, exceptfds) { + var total = 0; + + var srcReadLow = (readfds ? {{{ makeGetValue('readfds', 0, 'i32') }}} : 0), + srcReadHigh = (readfds ? {{{ makeGetValue('readfds', 4, 'i32') }}} : 0); + var srcWriteLow = (writefds ? {{{ makeGetValue('writefds', 0, 'i32') }}} : 0), + srcWriteHigh = (writefds ? {{{ makeGetValue('writefds', 4, 'i32') }}} : 0); + var srcExceptLow = (exceptfds ? {{{ makeGetValue('exceptfds', 0, 'i32') }}} : 0), + srcExceptHigh = (exceptfds ? {{{ makeGetValue('exceptfds', 4, 'i32') }}} : 0); + + var dstReadLow = 0, + dstReadHigh = 0; + var dstWriteLow = 0, + dstWriteHigh = 0; + var dstExceptLow = 0, + dstExceptHigh = 0; + + var check = (fd, low, high, val) => fd < 32 ? (low & val) : (high & val); + + return { + getTotal: () => total, + setFlags: (fd, flags) => { + var mask = 1 << (fd % 32); + + if ((flags & {{{ cDefs.POLLIN }}}) && check(fd, srcReadLow, srcReadHigh, mask)) { + fd < 32 ? (dstReadLow = dstReadLow | mask) : (dstReadHigh = dstReadHigh | mask); + total++; + } + if ((flags & {{{ cDefs.POLLOUT }}}) && check(fd, srcWriteLow, srcWriteHigh, mask)) { + fd < 32 ? (dstWriteLow = dstWriteLow | mask) : (dstWriteHigh = dstWriteHigh | mask); + total++; + } + if ((flags & {{{ cDefs.POLLPRI }}}) && check(fd, srcExceptLow, srcExceptHigh, mask)) { + fd < 32 ? (dstExceptLow = dstExceptLow | mask) : (dstExceptHigh = dstExceptHigh | mask); + total++; + } + }, + commit: () => { + if (readfds) { + {{{ makeSetValue('readfds', '0', 'dstReadLow', 'i32') }}}; + {{{ makeSetValue('readfds', '4', 'dstReadHigh', 'i32') }}}; + } + if (writefds) { + {{{ makeSetValue('writefds', '0', 'dstWriteLow', 'i32') }}}; + {{{ makeSetValue('writefds', '4', 'dstWriteHigh', 'i32') }}}; + } + if (exceptfds) { + {{{ makeSetValue('exceptfds', '0', 'dstExceptLow', 'i32') }}}; + {{{ makeSetValue('exceptfds', '4', 'dstExceptHigh', 'i32') }}}; + } + } + }; + }, }, $syscallGetVarargI__internal: true, @@ -542,31 +607,46 @@ var SyscallsLibrary = { FS.chdir(stream.path); return 0; }, + __syscall__newselect__deps: ['$newselectInner','malloc','free'], + __syscall__newselect__proxy: 'none', __syscall__newselect: (nfds, readfds, writefds, exceptfds, timeout) => { +#if PROXY_TO_PTHREAD + var waitPtr = _malloc(8); + var result = newselectInner(nfds, readfds, writefds, exceptfds, timeout, waitPtr); + if ((result != 0) || ((timeout) && (SYSCALLS.getTimeoutInMillis(timeout) == 0))) { + _free(waitPtr); + return result; + } + var fdRegion = {{{ makeGetValue('waitPtr', 0, '*') }}}; + Atomics.wait(HEAP32 , fdRegion >> 2, -1); + var fd = Atomics.load(HEAP32 , fdRegion >> 2); + var flags = Atomics.load(HEAP32 , fdRegion >> 2 + 1); + _free(waitPtr); + if (fd < 0) return 0; + var fdSet = SYSCALLS.parseSelectFDSet(readfds, writefds, exceptfds); + fdSet.setFlags(fd, flags); + fdSet.commit(); + return fdSet.getTotal(); +#else + return newselectInner(nfds, readfds, writefds, exceptfds, timeout, -1); +#endif + }, +#if PROXY_TO_PTHREAD + $newselectInner__deps: ['$PThread', '$deactivateSelectCallbacks', '$getActiveSelectCallbacks', '$activateSelectCallback', '$isActiveSelectCallback'], +#endif + $newselectInner__proxy: 'sync', + $newselectInner: (nfds, readfds, writefds, exceptfds, timeout, waitPtr) => { // readfds are supported, // writefds checks socket open status // exceptfds are supported, although on web, such exceptional conditions never arise in web sockets // and so the exceptfds list will always return empty. - // timeout is supported, although on SOCKFS and PIPEFS these are ignored and always treated as 0 - fully async + // timeout is supported, although on SOCKFS these are ignored and always treated as 0 - fully async + // and PIPEFS supports timeout only when PROXY_TO_PTHREAD is enabled. #if ASSERTIONS assert(nfds <= 64, 'nfds must be less than or equal to 64'); // fd sets have 64 bits // TODO: this could be 1024 based on current musl headers #endif - var total = 0; - - var srcReadLow = (readfds ? {{{ makeGetValue('readfds', 0, 'i32') }}} : 0), - srcReadHigh = (readfds ? {{{ makeGetValue('readfds', 4, 'i32') }}} : 0); - var srcWriteLow = (writefds ? {{{ makeGetValue('writefds', 0, 'i32') }}} : 0), - srcWriteHigh = (writefds ? {{{ makeGetValue('writefds', 4, 'i32') }}} : 0); - var srcExceptLow = (exceptfds ? {{{ makeGetValue('exceptfds', 0, 'i32') }}} : 0), - srcExceptHigh = (exceptfds ? {{{ makeGetValue('exceptfds', 4, 'i32') }}} : 0); - - var dstReadLow = 0, - dstReadHigh = 0; - var dstWriteLow = 0, - dstWriteHigh = 0; - var dstExceptLow = 0, - dstExceptHigh = 0; + var fdSet = SYSCALLS.parseSelectFDSet(readfds, writefds, exceptfds); var allLow = (readfds ? {{{ makeGetValue('readfds', 0, 'i32') }}} : 0) | (writefds ? {{{ makeGetValue('writefds', 0, 'i32') }}} : 0) | @@ -577,6 +657,44 @@ var SyscallsLibrary = { var check = (fd, low, high, val) => fd < 32 ? (low & val) : (high & val); + var timeoutInMillis = -1; + if (timeout) { + timeoutInMillis = SYSCALLS.getTimeoutInMillis(timeout); + } + +#if PROXY_TO_PTHREAD + const pthread_ptr = PThread.currentProxiedOperationCallerThread; + deactivateSelectCallbacks(pthread_ptr); // deactivate all old callbacks + var makeNotifyCallback = (fd) => null; + var cleanupFuncs = []; + if (timeoutInMillis != 0) { + var info = getActiveSelectCallbacks(pthread_ptr); + {{{ makeSetValue('waitPtr', 0, 'info.buf', '*') }}}; + Atomics.store(HEAP32, info.buf >> 2, -1); // Initialize the shared region + makeNotifyCallback = (fd) => { + var cb = (flags) => { + if (!isActiveSelectCallback(pthread_ptr, cb)) { + return; // This callback is no longer active. + } + deactivateSelectCallbacks(pthread_ptr); // Only the first event is notified. + cleanupFuncs.forEach(cb => cb()); + Atomics.store(HEAP32, info.buf >> 2 + 1, flags); + Atomics.store(HEAP32, info.buf >> 2, fd); + Atomics.notify(HEAP32, info.buf >> 2); + } + cb.registerCleanupFunc = (f) => { + if (f != null) cleanupFuncs.push(f); + } + activateSelectCallback(pthread_ptr, cb); + return cb; + } + if (timeoutInMillis > 0) { + var cb = makeNotifyCallback(-2); + setTimeout(() => cb(0), timeoutInMillis); + } + } +#endif + for (var fd = 0; fd < nfds; fd++) { var mask = 1 << (fd % 32); if (!(check(fd, allLow, allHigh, mask))) { @@ -588,48 +706,29 @@ var SyscallsLibrary = { var flags = SYSCALLS.DEFAULT_POLLMASK; if (stream.stream_ops.poll) { - var timeoutInMillis = -1; - if (timeout) { - // select(2) is declared to accept "struct timeval { time_t tv_sec; suseconds_t tv_usec; }". - // However, musl passes the two values to the syscall as an array of long values. - // Note that sizeof(time_t) != sizeof(long) in wasm32. The former is 8, while the latter is 4. - // This means using "C_STRUCTS.timeval.tv_usec" leads to a wrong offset. - // So, instead, we use POINTER_SIZE. - var tv_sec = (readfds ? {{{ makeGetValue('timeout', 0, 'i32') }}} : 0), - tv_usec = (readfds ? {{{ makeGetValue('timeout', POINTER_SIZE, 'i32') }}} : 0); - timeoutInMillis = (tv_sec + tv_usec / 1000000) * 1000; - } - flags = stream.stream_ops.poll(stream, timeoutInMillis); +#if PROXY_TO_PTHREAD + flags = stream.stream_ops.poll(stream, timeoutInMillis, makeNotifyCallback(fd)); +#else + flags = stream.stream_ops.poll(stream, ((timeoutInMillis < 0) || readfds) ? timeoutInMillis : 0); +#endif } - if ((flags & {{{ cDefs.POLLIN }}}) && check(fd, srcReadLow, srcReadHigh, mask)) { - fd < 32 ? (dstReadLow = dstReadLow | mask) : (dstReadHigh = dstReadHigh | mask); - total++; - } - if ((flags & {{{ cDefs.POLLOUT }}}) && check(fd, srcWriteLow, srcWriteHigh, mask)) { - fd < 32 ? (dstWriteLow = dstWriteLow | mask) : (dstWriteHigh = dstWriteHigh | mask); - total++; - } - if ((flags & {{{ cDefs.POLLPRI }}}) && check(fd, srcExceptLow, srcExceptHigh, mask)) { - fd < 32 ? (dstExceptLow = dstExceptLow | mask) : (dstExceptHigh = dstExceptHigh | mask); - total++; - } + fdSet.setFlags(fd, flags); } - if (readfds) { - {{{ makeSetValue('readfds', '0', 'dstReadLow', 'i32') }}}; - {{{ makeSetValue('readfds', '4', 'dstReadHigh', 'i32') }}}; - } - if (writefds) { - {{{ makeSetValue('writefds', '0', 'dstWriteLow', 'i32') }}}; - {{{ makeSetValue('writefds', '4', 'dstWriteHigh', 'i32') }}}; - } - if (exceptfds) { - {{{ makeSetValue('exceptfds', '0', 'dstExceptLow', 'i32') }}}; - {{{ makeSetValue('exceptfds', '4', 'dstExceptHigh', 'i32') }}}; + +#if PROXY_TO_PTHREAD + if ((fdSet.getTotal() > 0) || (timeoutInMillis == 0) ) { + fdSet.commit(fd, flags); + // No wait will happen in the caller. Deactivate all callbacks. + deactivateSelectCallbacks(pthread_ptr); + cleanupFuncs.forEach(f => f()); } +#else + fdSet.commit(fd, flags); +#endif - return total; + return fdSet.getTotal(); }, _msync_js__i53abi: true, _msync_js: (addr, len, prot, flags, fd, offset) => { diff --git a/test/core/test_pthread_select_timeout.c b/test/core/test_pthread_select_timeout.c new file mode 100644 index 0000000000000..2f8cd4fa81a1d --- /dev/null +++ b/test/core/test_pthread_select_timeout.c @@ -0,0 +1,129 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +// Check if timeout works without fds +void test_timeout_without_fds() +{ + struct timeval tv, begin, end; + + tv.tv_sec = 1; + tv.tv_usec = 0; + gettimeofday(&begin, NULL); + assert(select(0, NULL, NULL, NULL, &tv) == 0); + gettimeofday(&end, NULL); + assert((end.tv_sec - begin.tv_sec) * 1000000 + end.tv_usec - begin.tv_usec >= 1000000); +} + +// Check if timeout works with fds without events +void test_timeout_with_fds_without_events() +{ + struct timeval tv, begin, end; + fd_set readfds; + int pipe_a[2]; + + assert(pipe(pipe_a) == 0); + + tv.tv_sec = 1; + tv.tv_usec = 0; + FD_ZERO(&readfds); + FD_SET(pipe_a[0], &readfds); + gettimeofday(&begin, NULL); + assert(select(pipe_a[0] + 1, &readfds, NULL, NULL, &tv) == 0); + gettimeofday(&end, NULL); + assert((end.tv_sec - begin.tv_sec) * 1000000 + end.tv_usec - begin.tv_usec >= 1000000); + + close(pipe_a[0]); close(pipe_a[1]); +} + +int pipe_shared[2]; + +void *wakeup_after_2s(void * arg) +{ + const char *t = "test\n"; + + sleep(2); + write(pipe_shared[1], t, strlen(t)); + + return NULL; +} + +// Check if select can unblock on an event +void test_unblock_select() +{ + struct timeval begin, end; + fd_set readfds; + int maxfd; + pthread_t tid; + int pipe_a[2]; + + assert(pipe(pipe_a) == 0); + assert(pipe(pipe_shared) == 0); + + FD_ZERO(&readfds); + FD_SET(pipe_a[0], &readfds); + FD_SET(pipe_shared[0], &readfds); + maxfd = (pipe_a[0] > pipe_shared[0] ? pipe_a[0] : pipe_shared[0]); + assert(pthread_create(&tid, NULL, wakeup_after_2s, NULL) == 0); + gettimeofday(&begin, NULL); + assert(select(maxfd + 1, &readfds, NULL, NULL, NULL) == 1); + gettimeofday(&end, NULL); + assert(FD_ISSET(pipe_shared[0], &readfds)); + assert((end.tv_sec - begin.tv_sec) * 1000000 + end.tv_usec - begin.tv_usec >= 1000000); + + pthread_join(tid, NULL); + + close(pipe_a[0]); close(pipe_a[1]); + close(pipe_shared[0]); close(pipe_shared[1]); +} + +// Check if select works with ready fds +void test_ready_fds() +{ + struct timeval tv; + fd_set readfds; + int maxfd; + const char *t = "test\n"; + int pipe_c[2]; + int pipe_d[2]; + + assert(pipe(pipe_c) == 0); + assert(pipe(pipe_d) == 0); + + write(pipe_c[1], t, strlen(t)); + write(pipe_d[1], t, strlen(t)); + maxfd = (pipe_c[0] > pipe_d[0] ? pipe_c[0] : pipe_d[0]); + + tv.tv_sec = 0; + tv.tv_usec = 0; + FD_ZERO(&readfds); + FD_SET(pipe_c[0], &readfds); + FD_SET(pipe_d[0], &readfds); + assert(select(maxfd + 1, &readfds, NULL, NULL, &tv) == 2); + assert(FD_ISSET(pipe_c[0], &readfds)); + assert(FD_ISSET(pipe_d[0], &readfds)); + + FD_ZERO(&readfds); + FD_SET(pipe_c[0], &readfds); + FD_SET(pipe_d[0], &readfds); + assert(select(maxfd + 1, &readfds, NULL, NULL, NULL) == 2); + assert(FD_ISSET(pipe_c[0], &readfds)); + assert(FD_ISSET(pipe_d[0], &readfds)); + + close(pipe_c[0]); close(pipe_c[1]); + close(pipe_d[0]); close(pipe_d[1]); +} + +int main() +{ + test_timeout_without_fds(); + test_timeout_with_fds_without_events(); + test_unblock_select(); + test_ready_fds(); + return 0; +} diff --git a/test/core/test_select.c b/test/core/test_select.c new file mode 100644 index 0000000000000..97412d7049120 --- /dev/null +++ b/test/core/test_select.c @@ -0,0 +1,25 @@ +#include +#include +#include +#include +#include +#include + +int pipe_a[2]; + +int main() +{ + fd_set readfds; + const char *t = "test\n"; + + assert(pipe(pipe_a) == 0); + FD_ZERO(&readfds); + FD_SET(pipe_a[0], &readfds); + write(pipe_a[1], t, strlen(t)); + assert(select(pipe_a[0] + 1, &readfds, NULL, NULL, NULL) == 1); + assert(FD_ISSET(pipe_a[0], &readfds)); + + close(pipe_a[0]); close(pipe_a[1]); + + return 0; +} diff --git a/test/sockets/test_sockets_echo_client.c b/test/sockets/test_sockets_echo_client.c index 64df6b969b8cc..1c1afd52d1787 100644 --- a/test/sockets/test_sockets_echo_client.c +++ b/test/sockets/test_sockets_echo_client.c @@ -67,13 +67,16 @@ void main_loop() { fd_set fdr; fd_set fdw; int res; + struct timeval tv; // make sure that server.fd is ready to read / write FD_ZERO(&fdr); FD_ZERO(&fdw); FD_SET(server.fd, &fdr); FD_SET(server.fd, &fdw); - res = select(64, &fdr, &fdw, NULL, NULL); + tv.tv_sec = 0; + tv.tv_usec = 0; + res = select(64, &fdr, &fdw, NULL, &tv); if (res == -1) { perror("select failed"); finish(EXIT_FAILURE); diff --git a/test/sockets/test_sockets_echo_server.c b/test/sockets/test_sockets_echo_server.c index a77c599e3c310..43377c88327f6 100644 --- a/test/sockets/test_sockets_echo_server.c +++ b/test/sockets/test_sockets_echo_server.c @@ -77,6 +77,7 @@ void main_loop() { int res; fd_set fdr; fd_set fdw; + struct timeval tv; // see if there are any connections to accept or read / write from FD_ZERO(&fdr); @@ -87,7 +88,9 @@ void main_loop() { if (client.fd) FD_SET(client.fd, &fdr); if (client.fd) FD_SET(client.fd, &fdw); #endif - res = select(64, &fdr, &fdw, NULL, NULL); + tv.tv_sec = 0; + tv.tv_usec = 0; + res = select(64, &fdr, &fdw, NULL, &tv); if (res == -1) { perror("select failed"); exit(EXIT_SUCCESS); diff --git a/test/test_core.py b/test/test_core.py index 0c9611728978a..48dc82b9face6 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -9680,6 +9680,12 @@ def test_externref_emjs(self, dynlink): def test_syscall_intercept(self): self.do_core_test('test_syscall_intercept.c') + def test_pthread_select_timeout(self): + self.do_runf('core/test_pthread_select_timeout.c', cflags=['-pthread', '-sPROXY_TO_PTHREAD=1', '-sEXIT_RUNTIME=1', '-Wno-pthreads-mem-growth']) + + def test_select(self): + self.do_runf('core/test_select.c') + @also_without_bigint def test_jslib_i64_params(self): # Tests the defineI64Param and receiveI64ParamAsI53 helpers that are