diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 56deabf..c639fb0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,6 +23,15 @@ jobs: # Tests - name: Run ST utest run: make ${{ matrix.target }} && ./obj/st_utest + - name: Run ST tools + run: ./auto/tools.sh + - name: Run ST tools in the SRS build + run: EXTRA_CFLAGS=-DMALLOC_STACK ./auto/tools.sh + - name: Run ST tools with ASAN + if: matrix.name == 'linux-x64' + run: | + EXTRA_CFLAGS="-DMALLOC_STACK -DMD_ASAN -fsanitize=address -fno-omit-frame-pointer" \ + LDFLAGS=-fsanitize=address ./auto/tools.sh coverage: name: actions-test-coverage diff --git a/.gitignore b/.gitignore index fad3dc4..ab54028 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ cmake-build-debug /ide/st_clion/Testing/ .DS_Store +/tasks diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b9b41d..66e9526 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ The changelog for the [srs](https://github.com/ossrs/state-threads/tree/srs) bra ## ST 1.9 Changelog +* 2026-10-03, Tools: Add integration tools for every public API, run by auto/tools.sh in CI, [#41](https://github.com/ossrs/state-threads/pull/41). v1.9.1 * 2026-09-30, Changelog: Move the change list from README to CHANGELOG.md, and add the patch version. v1.9.0 ## Before v1.9.0 diff --git a/Makefile b/Makefile index 392d972..7832b36 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ # GPL. # This is the full version of the libst library - modify carefully -VERSION = 1.9.0 +VERSION = 1.9.1 ########################## # Supported OSes: @@ -241,7 +241,7 @@ endif ifeq ($(OS),) ST_ALL = unknown else -ST_ALL = $(TARGETDIR) $(LIBRARIES) $(HEADER) $(DESC) +ST_ALL = $(TARGETDIR) $(LIBRARIES) $(HEADER) $(DESC) obj-link endif all: $(ST_ALL) @@ -258,13 +258,17 @@ unknown: st.pc: st.pc.in sed "s/@VERSION@/${VERSION}/g" < $< > $@ +# Point obj to this platform on every build, even when the library is up to date. +.PHONY: obj-link +obj-link: + rm -f obj; $(LN) $(LNFLAGS) $(TARGETDIR) obj + $(TARGETDIR): if [ ! -d $(TARGETDIR) ]; then mkdir $(TARGETDIR); fi $(SLIBRARY): $(OBJS) $(AR) $(ARFLAGS) $@ $(OBJS) $(RANLIB) $@ - rm -f obj; $(LN) $(LNFLAGS) $(TARGETDIR) obj $(DLIBRARY): $(OBJS:%.o=%-pic.o) $(LD) $(LDFLAGS) $^ -o $@ diff --git a/auto/tools.sh b/auto/tools.sh new file mode 100755 index 0000000..f2a644e --- /dev/null +++ b/auto/tools.sh @@ -0,0 +1,52 @@ +#!/bin/bash +# +# Build ST, then run every integration tool in tools/ under each event system. +# +# The build comes from the environment, empty by default: +# EXTRA_CFLAGS passed to the library build, such as -DMALLOC_STACK +# LDFLAGS passed to the tool link, such as -fsanitize=address +# +# Examples: +# ./auto/tools.sh +# EXTRA_CFLAGS=-DMALLOC_STACK ./auto/tools.sh +# EXTRA_CFLAGS="-DMALLOC_STACK -DMD_ASAN -fsanitize=address -fno-omit-frame-pointer" \ +# LDFLAGS=-fsanitize=address ./auto/tools.sh +# +# Each tool reads ST_TOOL_EVENTSYS, select or alt. It prints " OK" +# per run, or "FAILED " with the output, and exits 1 at the first failure. + +cd "$(dirname "$0")/.." || exit 1 + +ST_TARGET=linux-debug +if [[ $(uname -s) == Darwin ]]; then + ST_TARGET=darwin-debug +fi + +echo "Build ST with make -B $ST_TARGET EXTRA_CFLAGS=\"$EXTRA_CFLAGS\"" +# -B rebuilds every object, so a change of flags never links a stale library. +if ! out=$(make -B $ST_TARGET EXTRA_CFLAGS="$EXTRA_CFLAGS" 2>&1); then + echo "$out" + echo "FAILED build ST" + exit 1 +fi + +for dir in tools/*/; do + name=$(basename "$dir") + # -W relinks the tool, because the binaries are shared across platforms and builds. + if ! out=$(make -C "$dir" -W "$name.c" LDFLAGS="$LDFLAGS" 2>&1); then + echo "$out" + echo "FAILED build $name" + exit 1 + fi + + for eventsys in select alt; do + if ! out=$(cd "$dir" && ST_TOOL_EVENTSYS=$eventsys "./$name" 2>&1); then + echo "$out" + echo "FAILED $name $eventsys" + exit 1 + fi + echo "$name $eventsys OK" + done +done + +echo "All tools OK" diff --git a/public.h b/public.h index f4ede26..4b68a0e 100644 --- a/public.h +++ b/public.h @@ -47,10 +47,10 @@ #include #include -#define ST_VERSION "1.9.0" +#define ST_VERSION "1.9.1" #define ST_VERSION_MAJOR 1 #define ST_VERSION_MINOR 9 -#define ST_VERSION_PATCH 0 +#define ST_VERSION_PATCH 1 /* Undefine this to remove the context switch callback feature. */ #define ST_SWITCH_CB diff --git a/st.spec b/st.spec index 194b464..662f4c8 100644 --- a/st.spec +++ b/st.spec @@ -1,6 +1,6 @@ Summary: State Threads Library Name: st -Version: 1.9.0 +Version: 1.9.1 Release: 1 Copyright: MPL 1.2 or GPL 2+ Packager: Wesley W. Terpstra diff --git a/tools/backtrace/Makefile b/tools/backtrace/Makefile index e0780c5..4c9aca3 100644 --- a/tools/backtrace/Makefile +++ b/tools/backtrace/Makefile @@ -13,7 +13,7 @@ CFLAGS += -arch $(CPU_ARCHS) endif ./backtrace: backtrace.c $(LDLIBS) - $(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) -Wall -o $@ $^ $(LDLIBS) + $(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) -Wall -o $@ $< $(LDLIBS) clean: cd ../.. && make clean diff --git a/tools/backtrace/backtrace.c b/tools/backtrace/backtrace.c index b64388e..4b07285 100644 --- a/tools/backtrace/backtrace.c +++ b/tools/backtrace/backtrace.c @@ -8,6 +8,7 @@ #include #include #include +#include #include @@ -142,6 +143,26 @@ void bar2() #endif int always_use_builtin = 0; +int failed = 0; + +// Whether the frame is the function, such as "bar + 48" on macOS or "./backtrace(bar+0x1c)" on Linux. +int is_frame_of(const char* frame, const char* name) +{ + char on_macos[64], on_linux[64]; + snprintf(on_macos, sizeof(on_macos), " %s + ", name); + snprintf(on_linux, sizeof(on_linux), "(%s+", name); + return strstr(frame, on_macos) || strstr(frame, on_linux); +} + +// Whether the frames hold the functions in order, from the callee up to the coroutine start. +int has_frames(char** symbols, int nn_symbols, const char** names, int nn_names) +{ + int i, j = 0; + for (i = 0; i < nn_symbols && j < nn_names; i++) { + if (is_frame_of(symbols[i], names[j])) j++; + } + return j == nn_names; +} #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wframe-address" @@ -186,8 +207,20 @@ void bar() { for (int i = 0; i < nn_addresses; i++) { printf("%s\n", symbols[i]); } + + // The return addresses from __builtin_return_address(0) begin at the caller foo, not bar. Only level 0 is required, + // because GCC on aarch64 returns NULL for any level above 0. + const char* backtrace_frames[] = {"bar", "foo", "start"}; + const char* builtin_frames[] = {"foo"}; + int ok = always_use_builtin ? has_frames(symbols, nn_addresses, builtin_frames, 1) + : has_frames(symbols, nn_addresses, backtrace_frames, 3); free(symbols); + if (!ok) { + printf("bar FAILED, no frames of the coroutine\n"); + failed = 1; + return; + } printf("bar OK\n"); return; } @@ -217,12 +250,18 @@ int main(int argc, char** argv) always_use_builtin = 1; } - st_init(); + if (st_init() != 0) { + printf("st_init failed\n"); + return 1; + } - st_thread_create(start, NULL, 0, 0); - st_thread_exit(NULL); + st_thread_t trd = st_thread_create(start, NULL, 1, 0); + if (!trd || st_thread_join(trd, NULL) != 0) { + printf("coroutine failed\n"); + return 1; + } - printf("main done\n"); - return 0; + printf("main done, failed=%d\n", failed); + return failed; } diff --git a/tools/helloworld/Makefile b/tools/helloworld/Makefile index 7ec0d2a..b4643c6 100644 --- a/tools/helloworld/Makefile +++ b/tools/helloworld/Makefile @@ -13,7 +13,7 @@ CFLAGS += -arch $(CPU_ARCHS) endif ./helloworld: helloworld.c $(LDLIBS) - $(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) -Wall -o $@ $^ $(LDLIBS) + $(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) -Wall -o $@ $< $(LDLIBS) clean: cd ../.. && make clean diff --git a/tools/helloworld/helloworld.c b/tools/helloworld/helloworld.c index 6c54e98..d457836 100644 --- a/tools/helloworld/helloworld.c +++ b/tools/helloworld/helloworld.c @@ -2,19 +2,32 @@ /* Copyright (c) 2013-2026 The SRS Authors */ #include +#include #include +// Usage: ./helloworld [count], where count defaults to 10, one line every 10ms. int main(int argc, char** argv) { - st_init(); + int count = 10; + if (argc > 1 && (count = atoi(argv[1])) <= 0) { + printf("Invalid count %s\n", argv[1]); + return 1; + } + + if (st_init() != 0) { + printf("st_init failed\n"); + return 1; + } int i; - for (i = 0; i < 10000; i++) { + for (i = 0; i < count; i++) { printf("#%03d, Hello, state-threads world!\n", i); - st_sleep(1); + if (st_usleep(10 * 1000) != 0) { + printf("st_usleep failed\n"); + return 1; + } } return 0; } - diff --git a/tools/jmpbuf/.gitignore b/tools/jmpbuf/.gitignore deleted file mode 100644 index c3a424a..0000000 --- a/tools/jmpbuf/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -jmpbuf -jmpbuf.E.txt - diff --git a/tools/jmpbuf/jmpbuf.c b/tools/jmpbuf/jmpbuf.c deleted file mode 100644 index e36ad6e..0000000 --- a/tools/jmpbuf/jmpbuf.c +++ /dev/null @@ -1,39 +0,0 @@ -/* SPDX-License-Identifier: MIT */ -/* Copyright (c) 2026 Winlin */ - -#include -#include - -/* We define the jmpbuf, because the system's is different in different OS */ -typedef struct _st_jmp_buf { - /* - * OS CPU SIZE - * Darwin __amd64__/__x86_64__ long[8] - * Darwin __aarch64__ long[22] - * Linux __i386__ long[6] - * Linux __amd64__/__x86_64__ long[8] - * Linux __aarch64__ long[22] - * Linux __arm__ long[16] - * Linux __mips__/__mips64 long[13] - * Linux __riscv long[14] - * Linux __loongarch64 long[12] - * Cygwin64 __amd64__/__x86_64__ long[8] - */ - long __jmpbuf[22]; -} _st_jmp_buf_t[1]; - -int main(int argc, char** argv) -{ - jmp_buf ctx = {0}; - int r0 = setjmp(ctx); - int nn_jb = sizeof(ctx); - printf("jmp_buf: r0=%d, sizeof(jmp_buf)=%d (unsigned long long [%d])\n", r0, nn_jb, nn_jb/8); - - _st_jmp_buf_t ctx2 = {0}; - int r1 = sizeof(_st_jmp_buf_t); - int r2 = sizeof(ctx2); - printf("_st_jmp_buf_t: sizeof(_st_jmp_buf_t)=%d/%d (unsigned long long [%d])\n", r1, r2, r2/8); - - return 0; -} - diff --git a/tools/key/.gitignore b/tools/key/.gitignore new file mode 100644 index 0000000..06bfde4 --- /dev/null +++ b/tools/key/.gitignore @@ -0,0 +1 @@ +key diff --git a/tools/pcs/Makefile b/tools/key/Makefile similarity index 57% rename from tools/pcs/Makefile rename to tools/key/Makefile index 83b04a8..0ca41d0 100644 --- a/tools/pcs/Makefile +++ b/tools/key/Makefile @@ -1,6 +1,7 @@ .PHONY: clean -CFLAGS=-g -O0 +LDLIBS=../../obj/libst.a +CFLAGS=-g -O0 -I../../obj -I.. -DDEBUG OS_NAME = $(shell uname -s) ST_TARGET = linux-debug @@ -11,9 +12,13 @@ CPU_ARCHS += $(shell g++ -dM -E - id = id; + } + return ctx; +} + +/* The key destructor, like _srs_context_destructor. */ +static void ctx_free(void *arg) +{ + ctx_t *ctx = (ctx_t *)arg; + freed++; + freed_ids += ctx->id; + free(ctx); +} + +/* The key with ctx_free, and one without a destructor. */ +static int key = -1; +static int plain = -1; + +/* Before any key exists, every key is invalid. */ +static int no_keys(void) +{ + int v = 0; + CHECK(st_thread_getspecific(0) == NULL); + CHECK(st_thread_getspecific(-1) == NULL); + errno = 0; + CHECK(st_thread_setspecific(0, &v) == -1 && errno == EINVAL); + errno = 0; + CHECK(st_thread_setspecific2(st_thread_self(), 0, &v) == -1 && errno == EINVAL); + return 0; +} + +static int create(void) +{ + CHECK(st_key_create(&key, ctx_free) == 0); + CHECK(st_key_create(&plain, NULL) == 0); + CHECK(key >= 0 && plain >= 0 && key != plain); + CHECK(key < st_key_getlimit() && plain < st_key_getlimit()); + + /* A key past the created ones is still invalid. */ + int next = key > plain ? key + 1 : plain + 1; + int v = 0; + CHECK(st_thread_getspecific(next) == NULL); + errno = 0; + CHECK(st_thread_setspecific(next, &v) == -1 && errno == EINVAL); + errno = 0; + CHECK(st_thread_setspecific(-1, &v) == -1 && errno == EINVAL); + return 0; +} + +/* Replacing a value frees the old one, setting the same one does not. */ +static int replace(void) +{ + CHECK(st_thread_getspecific(key) == NULL); + + ctx_t *a = ctx_new(1); + ctx_t *b = ctx_new(2); + CHECK(a && b); + + freed = freed_ids = 0; + CHECK(st_thread_setspecific(key, a) == 0); + CHECK(st_thread_getspecific(key) == a); + CHECK(st_thread_setspecific(key, a) == 0); + CHECK(freed == 0); + + CHECK(st_thread_setspecific(key, b) == 0); + CHECK(st_thread_getspecific(key) == b); + CHECK(freed == 1 && freed_ids == 1); + + CHECK(st_thread_setspecific(key, NULL) == 0); + CHECK(st_thread_getspecific(key) == NULL); + CHECK(freed == 2 && freed_ids == 3); + + /* Without a destructor, a replaced value is only dropped. */ + int x = 1, y = 2; + CHECK(st_thread_setspecific(plain, &x) == 0); + CHECK(st_thread_setspecific(plain, &y) == 0); + CHECK(st_thread_getspecific(plain) == &y); + CHECK(st_thread_setspecific(plain, NULL) == 0); + CHECK(st_thread_getspecific(plain) == NULL); + CHECK(freed == 2); + return 0; +} + +#define NN_WORKERS 8 +#define NN_YIELDS 5 + +/* + * Set its own value, then yield between the other workers, and still read + * its own value each time. Returns 1 when every read matched. + */ +static void *do_own(void *arg) +{ + int id = (int)(long)arg; + if (st_thread_getspecific(key) != NULL || st_thread_getspecific(plain) != NULL) { + return NULL; + } + + ctx_t *ctx = ctx_new(id); + if (!ctx || st_thread_setspecific(key, ctx) != 0) { + return NULL; + } + if (st_thread_setspecific(plain, arg) != 0) { + return NULL; + } + + for (int i = 0; i < NN_YIELDS; i++) { + st_thread_yield(); + ctx_t *v = (ctx_t *)st_thread_getspecific(key); + if (v != ctx || v->id != id || st_thread_getspecific(plain) != arg) { + return NULL; + } + } + return (void *)1; +} + +/* Values stay per coroutine, and each one is freed when its coroutine exits. */ +static int per_coroutine(void) +{ + ctx_t *mine = ctx_new(100); + CHECK(mine); + CHECK(st_thread_setspecific(key, mine) == 0); + + st_thread_t trds[NN_WORKERS]; + for (int i = 0; i < NN_WORKERS; i++) { + trds[i] = st_thread_create(do_own, (void *)(long)(i + 1), 1, 0); + CHECK(trds[i]); + } + + freed = freed_ids = 0; + int ids = 0; + for (int i = 0; i < NN_WORKERS; i++) { + void *retval = NULL; + CHECK(st_thread_join(trds[i], &retval) == 0); + CHECK(retval == (void *)1); + ids += i + 1; + } + CHECK(freed == NN_WORKERS && freed_ids == ids); + + /* The workers did not touch the primordial thread's value. */ + CHECK(st_thread_getspecific(key) == mine); + CHECK(st_thread_setspecific(key, NULL) == 0); + CHECK(freed == NN_WORKERS + 1); + return 0; +} + +/* Return the id of the value another coroutine set on this one. */ +static void *do_read(void *arg) +{ + ctx_t *v = (ctx_t *)st_thread_getspecific(key); + return (void *)(long)(v ? v->id : 0); +} + +/* Wait on the cond, then return the id of the value set while it waited. */ +static void *do_wait(void *arg) +{ + ctx_t *before = (ctx_t *)st_thread_getspecific(key); + if (!before || before->id != 1) { + return NULL; + } + if (st_cond_timedwait((st_cond_t)arg, BLOCK_US) != 0) { + return NULL; + } + ctx_t *after = (ctx_t *)st_thread_getspecific(key); + return (void *)(long)(after ? after->id : 0); +} + +/* st_thread_setspecific2 sets a value on another coroutine, as SrsFastCoroutine::set_cid does. */ +static int set_other(void) +{ + /* Set on a new coroutine before it runs. */ + ctx_t *a = ctx_new(7); + CHECK(a); + st_thread_t trd = st_thread_create(do_read, NULL, 1, 0); + CHECK(trd); + CHECK(st_thread_setspecific2(trd, key, a) == 0); + CHECK(st_thread_getspecific(key) == NULL); + + freed = freed_ids = 0; + void *retval = NULL; + CHECK(st_thread_join(trd, &retval) == 0); + CHECK((long)retval == 7); + CHECK(freed == 1 && freed_ids == 7); + + /* Replace the value of a blocked coroutine, which frees the old one. */ + st_cond_t cond = st_cond_new(); + CHECK(cond); + ctx_t *b = ctx_new(1); + ctx_t *c = ctx_new(2); + CHECK(b && c); + trd = st_thread_create(do_wait, cond, 1, 0); + CHECK(trd); + CHECK(st_thread_setspecific2(trd, key, b) == 0); + CHECK(st_usleep(100) == 0); + + freed = freed_ids = 0; + CHECK(st_thread_setspecific2(trd, key, c) == 0); + CHECK(freed == 1 && freed_ids == 1); + CHECK(st_cond_signal(cond) == 0); + CHECK(st_thread_join(trd, &retval) == 0); + CHECK((long)retval == 2); + CHECK(freed == 2 && freed_ids == 3); + + CHECK(st_cond_destroy(cond) == 0); + return 0; +} + +/* Set a value, then leave in the way arg says. */ +static void exit_nested(void) +{ + st_thread_exit(NULL); +} + +static void *do_leave(void *arg) +{ + long how = (long)arg; + ctx_t *ctx = ctx_new(1); + if (!ctx || st_thread_setspecific(key, ctx) != 0) { + return NULL; + } + if (how == 1) { + exit_nested(); + } else if (how == 2) { + st_usleep(BLOCK_US); + } else if (how == 3) { + st_usleep(100); + } + return NULL; +} + +/* Destructors run on every way a coroutine exits, before st_thread_join returns. */ +static int destructors(void) +{ + freed = freed_ids = 0; + + /* Return from the start function. */ + st_thread_t trd = st_thread_create(do_leave, (void *)0, 1, 0); + CHECK(trd); + CHECK(st_thread_join(trd, NULL) == 0); + CHECK(freed == 1); + + /* st_thread_exit from a nested call. */ + trd = st_thread_create(do_leave, (void *)1, 1, 0); + CHECK(trd); + CHECK(st_thread_join(trd, NULL) == 0); + CHECK(freed == 2); + + /* Interrupted in st_usleep, as SrsFastCoroutine::stop does. */ + trd = st_thread_create(do_leave, (void *)2, 1, 0); + CHECK(trd); + CHECK(st_usleep(100) == 0); + CHECK(freed == 2); + st_thread_interrupt(trd); + CHECK(st_thread_join(trd, NULL) == 0); + CHECK(freed == 3); + + /* A detached coroutine. */ + trd = st_thread_create(do_leave, (void *)3, 0, 0); + CHECK(trd); + for (int i = 0; i < 100 && freed < 4; i++) { + CHECK(st_usleep(1000) == 0); + } + CHECK(freed == 4); + + /* A new coroutine, which may reuse a freed stack, starts with no values. */ + trd = st_thread_create(do_read, NULL, 1, 0); + CHECK(trd); + void *retval = (void *)-1; + CHECK(st_thread_join(trd, &retval) == 0); + CHECK(retval == NULL); + CHECK(freed == 4 && freed_ids == 4); + return 0; +} + +/* Create keys up to st_key_getlimit(); the next one fails with EAGAIN. */ +static int limit(void) +{ + int limit = st_key_getlimit(); + CHECK(limit > 2); + + /* The keys made so far are key and plain. */ + int keys[limit]; + keys[0] = key; + keys[1] = plain; + for (int i = 2; i < limit; i++) { + CHECK(st_key_create(&keys[i], NULL) == 0); + } + + int k = -1; + errno = 0; + CHECK(st_key_create(&k, NULL) == -1 && errno == EAGAIN); + CHECK(k == -1); + + /* Every key is distinct, in range, and holds its own value. */ + for (int i = 0; i < limit; i++) { + CHECK(keys[i] >= 0 && keys[i] < limit); + for (int j = 0; j < i; j++) { + CHECK(keys[i] != keys[j]); + } + } + for (int i = 2; i < limit; i++) { + CHECK(st_thread_setspecific(keys[i], &keys[i]) == 0); + } + for (int i = 2; i < limit; i++) { + CHECK(st_thread_getspecific(keys[i]) == &keys[i]); + CHECK(st_thread_setspecific(keys[i], NULL) == 0); + } + return 0; +} + +int main(int argc, char **argv) +{ + CHECK(tool_init() == 0); + + CHECK(no_keys() == 0); + CHECK(create() == 0); + CHECK(replace() == 0); + CHECK(per_coroutine() == 0); + CHECK(set_other() == 0); + CHECK(destructors() == 0); + CHECK(limit() == 0); + + printf("key OK\n"); + return 0; +} diff --git a/tools/lifecycle/.gitignore b/tools/lifecycle/.gitignore new file mode 100644 index 0000000..0792bb5 --- /dev/null +++ b/tools/lifecycle/.gitignore @@ -0,0 +1 @@ +lifecycle diff --git a/tools/jmpbuf/Makefile b/tools/lifecycle/Makefile similarity index 50% rename from tools/jmpbuf/Makefile rename to tools/lifecycle/Makefile index e399c3e..d015d40 100644 --- a/tools/jmpbuf/Makefile +++ b/tools/lifecycle/Makefile @@ -1,7 +1,7 @@ -.PHONY: default clean +.PHONY: clean -LDLIBS= -CFLAGS=-g -O0 +LDLIBS=../../obj/libst.a -lpthread +CFLAGS=-g -O0 -I../../obj -I.. -DDEBUG OS_NAME = $(shell uname -s) ST_TARGET = linux-debug @@ -12,14 +12,13 @@ CPU_ARCHS += $(shell g++ -dM -E - +#include +#include + +#include "tool.h" + +/* Long enough that a coroutine still blocked here is a failure, not a slow run. */ +#define BLOCK_US (5 * 1000 * 1000) + +static int started; + +/* Return errno of a call that failed, or 0 when it did not fail. */ +static void *result(int r0) +{ + return (void *)(long)(r0 < 0 ? errno : 0); +} + +static void *do_accept(void *arg) +{ + started++; + st_netfd_t client = st_accept((st_netfd_t)arg, NULL, NULL, BLOCK_US); + if (client) { + st_netfd_close(client); + return (void *)0; + } + return (void *)(long)errno; +} + +static void *do_read(void *arg) +{ + char buf[16]; + started++; + return result((int)st_read((st_netfd_t)arg, buf, sizeof(buf), BLOCK_US)); +} + +static void *do_cond(void *arg) +{ + started++; + return result(st_cond_timedwait((st_cond_t)arg, BLOCK_US)); +} + +static void *do_sleep(void *arg) +{ + started++; + return result(st_usleep(BLOCK_US)); +} + +/* + * Set the primordial stack to the main thread's real stack. SRS takes the whole + * RLIMIT_STACK below a local, which is mostly unmapped; with leak detection on, + * as here and unlike SRS, LeakSanitizer reads that range at exit and crashes. + */ +static int set_primordial_stack(void) +{ + char *top = NULL; + size_t size = 0; +#ifdef __APPLE__ + pthread_t self = pthread_self(); + top = (char *)pthread_get_stackaddr_np(self); + size = pthread_get_stacksize_np(self); +#else + void *addr = NULL; + pthread_attr_t attr; + CHECK(pthread_getattr_np(pthread_self(), &attr) == 0); + CHECK(pthread_attr_getstack(&attr, &addr, &size) == 0); + pthread_attr_destroy(&attr); + top = (char *)addr + size; +#endif + CHECK(top && size > 0); + st_set_primordial_stack(top, top - size); + return 0; +} + +/* The descriptor limit _st_io_init sets from the limit before st_init. */ +static int check_fdlimit(int eventsys, const struct rlimit *before) +{ + struct rlimit after; + CHECK(getrlimit(RLIMIT_NOFILE, &after) == 0); + + int limit = st_getfdlimit(); + CHECK(limit > 0); + + if (eventsys == ST_EVENTSYS_SELECT) { + /* select caps the limit at FD_SETSIZE, and raises the soft limit to it. */ + int expected = before->rlim_max > FD_SETSIZE ? FD_SETSIZE : (int)before->rlim_max; + CHECK(limit == expected); + CHECK(after.rlim_cur == (rlim_t)expected && after.rlim_max == (rlim_t)expected); + } else if ((int)before->rlim_max < 0) { + /* kqueue and epoll have no limit; an infinite rlim_max, as on macOS, uses rlim_cur. */ + CHECK(limit == (int)before->rlim_cur); + CHECK(after.rlim_cur == before->rlim_cur && after.rlim_max == before->rlim_max); + } else { + /* Otherwise the soft limit is raised to rlim_max. */ + CHECK(limit == (int)before->rlim_max); + CHECK(after.rlim_cur == before->rlim_max && after.rlim_max == before->rlim_max); + } + + printf("ST: fdlimit=%d, before cur=%llu max=%llu\n", limit, + (unsigned long long)before->rlim_cur, (unsigned long long)before->rlim_max); + return 0; +} + +/* Block four coroutines, then interrupt and join them; each must fail with EINTR at once. */ +static int interrupt_all(void) +{ + int port = 0; + st_netfd_t lfd = tool_listen(AF_INET, 16, &port); + CHECK(lfd); + + /* Accept the client here, so the accepting coroutine has nothing pending. */ + st_netfd_t client = tool_connect(AF_INET, port, BLOCK_US); + CHECK(client); + st_netfd_t server = st_accept(lfd, NULL, NULL, BLOCK_US); + CHECK(server); + + st_cond_t cond = st_cond_new(); + CHECK(cond); + + st_thread_t trds[4]; + trds[0] = st_thread_create(do_accept, lfd, 1, 0); + trds[1] = st_thread_create(do_read, client, 1, 0); + trds[2] = st_thread_create(do_cond, cond, 1, 0); + trds[3] = st_thread_create(do_sleep, NULL, 1, 0); + for (int i = 0; i < 4; i++) { + CHECK(trds[i]); + } + + /* Let every coroutine run until it blocks. */ + st_utime_t starttime = st_utime(); + CHECK(st_usleep(1000) == 0); + CHECK(started == 4); + + for (int i = 0; i < 4; i++) { + st_thread_interrupt(trds[i]); + } + for (int i = 0; i < 4; i++) { + void *retval = NULL; + CHECK(st_thread_join(trds[i], &retval) == 0); + CHECK((long)retval == EINTR); + } + CHECK(st_utime() - starttime < BLOCK_US / 10); + + CHECK(st_cond_destroy(cond) == 0); + CHECK(st_netfd_close(server) == 0); + CHECK(st_netfd_close(client) == 0); + CHECK(st_netfd_close(lfd) == 0); + return 0; +} + +int main(int argc, char **argv) +{ + int eventsys = tool_eventsys(); + CHECK(eventsys != -1); + + /* Nothing is chosen before st_set_eventsys. */ + CHECK(st_get_eventsys() == -1); + CHECK(!strcmp(st_get_eventsys_name(), "")); + + errno = 0; + CHECK(st_set_eventsys(2) == -1 && errno == EINVAL); + CHECK(st_get_eventsys() == -1); + + CHECK(st_set_eventsys(eventsys) == 0); + CHECK(st_get_eventsys() == eventsys); + CHECK(!strcmp(st_get_eventsys_name(), tool_eventsys_name(eventsys))); + + /* The event system is chosen once per process. */ + errno = 0; + CHECK(st_set_eventsys(eventsys) == -1 && errno == EBUSY); + + CHECK(set_primordial_stack() == 0); + + struct rlimit before; + CHECK(getrlimit(RLIMIT_NOFILE, &before) == 0); + + CHECK(st_init() == 0); + printf("ST: init ok, eventsys=%s\n", st_get_eventsys_name()); + + /* After st_init, setting the event system still fails, and nothing changes. */ + errno = 0; + CHECK(st_set_eventsys(ST_EVENTSYS_DEFAULT) == -1 && errno == EBUSY); + CHECK(st_get_eventsys() == eventsys); + CHECK(!strcmp(st_get_eventsys_name(), tool_eventsys_name(eventsys))); + + /* A second st_init is a no-op. */ + CHECK(st_init() == 0); + CHECK(st_get_eventsys() == eventsys); + + CHECK(check_fdlimit(eventsys, &before) == 0); + CHECK(interrupt_all() == 0); + + st_destroy(); + printf("lifecycle OK\n"); + return 0; +} diff --git a/tools/pcs/.gitignore b/tools/pcs/.gitignore deleted file mode 100644 index 42e648f..0000000 --- a/tools/pcs/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -pcs - diff --git a/tools/pcs/pcs.c b/tools/pcs/pcs.c deleted file mode 100644 index 4ac6498..0000000 --- a/tools/pcs/pcs.c +++ /dev/null @@ -1,35 +0,0 @@ -/* SPDX-License-Identifier: MIT */ -/* Copyright (c) 2026 Winlin */ - -void foo() { -} - -void foo2(char a) { -} - -void foo3(int a) { -} - -void foo4(long a) { -} - -void foo5(long long a) { -} - -long foo6(long a) { - return a + 1; -} - -// Note: Use b *main to set to the first instruction of main, -// see https://stackoverflow.com/questions/40960758/break-main-vs-break-main-in-gdb -int main(int argc, char** argv) -{ - foo(); - foo2('s'); - foo3(0x7); - foo4(0x7); - foo5(0x7); - foo6(0x7); - return 0; -} - diff --git a/tools/pipe/.gitignore b/tools/pipe/.gitignore new file mode 100644 index 0000000..6b14dd0 --- /dev/null +++ b/tools/pipe/.gitignore @@ -0,0 +1 @@ +pipe diff --git a/tools/pipe/Makefile b/tools/pipe/Makefile new file mode 100644 index 0000000..9fc148f --- /dev/null +++ b/tools/pipe/Makefile @@ -0,0 +1,24 @@ +.PHONY: clean + +LDLIBS=../../obj/libst.a +CFLAGS=-g -O0 -I../../obj -I.. -DDEBUG + +OS_NAME = $(shell uname -s) +ST_TARGET = linux-debug +ifeq ($(OS_NAME), Darwin) +ST_TARGET = darwin-debug +CPU_ARCHS = $(shell g++ -dM -E - +#include +#include +#include +#include +#include + +/* Long enough that a coroutine still blocked here is a failure, not a slow run. */ +#define BLOCK_US (5 * 1000 * 1000) + +/* A timeout that is expected to expire. */ +#define SHORT_US (5 * 1000) + +/* The paths, removed by these literal paths before and after use. */ +#define FIFO_PATH "/tmp/st-tool-pipe.fifo" +#define FILE_PATH "/tmp/st-tool-pipe.txt" +#define MISSING_PATH "/tmp/st-tool-pipe-missing.txt" + +/* Bytes sent through a pipe or FIFO, more than any pipe buffer holds. */ +#define PIPE_BYTES (512 * 1024) +#define PIPE_CHUNK (32 * 1024) + +static void remove_paths(void) +{ + unlink(FIFO_PATH); + unlink(FILE_PATH); + unlink(MISSING_PATH); +} + +/* The byte at offset off of every stream this tool sends. */ +static char stream_byte(int off) +{ + return (char)(off * 7 + off / 251); +} + +/* The size of the chunk written at offset off, from 1 to PIPE_CHUNK. */ +static int chunk_size(int off, int total) +{ + int n = 1 + (off * 13) % PIPE_CHUNK; + return n < total - off ? n : total - off; +} + +/* Read total bytes of the stream from fd, then EOF. Returns 0 on success. */ +static int read_stream(st_netfd_t fd, int total) +{ + char *buf = malloc(PIPE_CHUNK); + CHECK(buf != NULL); + int got = 0; + while (got < total) { + ssize_t n = st_read(fd, buf, PIPE_CHUNK, BLOCK_US); + CHECK(n > 0 && got + n <= total); + for (int i = 0; i < n; i++) { + CHECK(buf[i] == stream_byte(got + i)); + } + got += (int)n; + } + CHECK(st_read(fd, buf, PIPE_CHUNK, BLOCK_US) == 0); + free(buf); + return 0; +} + +/* + * The SRS signal pipe, as SrsSignalManager: the handler writes the signal + * number to the write end, and a coroutine reads it from the read end opened + * with st_netfd_open, then handles it. SIGHUP reloads, SIGTERM quits. + */ +static int sig_pipe[2] = {-1, -1}; + +static void sig_catcher(int signo) +{ + int err = errno; + if (write(sig_pipe[1], &signo, sizeof(int)) != sizeof(int)) { + /* Nothing to do in a handler; the reader then misses this signal. */ + } + errno = err; +} + +struct signal_manager { + st_netfd_t fd; + /* Where to write one byte after each reload, or NULL. */ + st_netfd_t ack; + int signals[8]; + int nb_signals; + int reloads; +}; + +static void *signal_manager_cycle(void *arg) +{ + struct signal_manager *m = (struct signal_manager *)arg; + while (m->nb_signals < (int)(sizeof(m->signals) / sizeof(m->signals[0]))) { + int signo; + if (st_read(m->fd, &signo, sizeof(int), BLOCK_US) != sizeof(int)) { + return (void *)1; + } + m->signals[m->nb_signals++] = signo; + + if (signo == SIGHUP) { + m->reloads++; + if (m->ack && st_write(m->ack, "r", 1, BLOCK_US) != 1) { + return (void *)2; + } + } else if (signo == SIGTERM) { + return NULL; + } + } + return (void *)3; +} + +/* The manager got SIGHUP then SIGTERM, reloaded once, and quit. */ +static int check_reload_then_quit(st_thread_t t, struct signal_manager *m) +{ + void *ret = (void *)-1; + CHECK(st_thread_join(t, &ret) == 0); + CHECK(ret == NULL); + CHECK(m->nb_signals == 2); + CHECK(m->signals[0] == SIGHUP && m->signals[1] == SIGTERM); + CHECK(m->reloads == 1); + return 0; +} + +static int signal_pipe(void) +{ + struct sigaction sa, old_hup, old_term; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = sig_catcher; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + + CHECK(pipe(sig_pipe) == 0); + struct signal_manager m; + memset(&m, 0, sizeof(m)); + CHECK((m.fd = st_netfd_open(sig_pipe[0])) != NULL); + CHECK(st_netfd_fileno(m.fd) == sig_pipe[0]); + CHECK(fcntl(sig_pipe[0], F_GETFL) & O_NONBLOCK); + CHECK(sigaction(SIGHUP, &sa, &old_hup) == 0); + CHECK(sigaction(SIGTERM, &sa, &old_term) == 0); + + /* + * The process signals itself. A signal sent to the calling process is + * handled before kill returns, so they are written in the order sent. + */ + st_thread_t t; + CHECK((t = st_thread_create(signal_manager_cycle, &m, 1, 0)) != NULL); + CHECK(st_usleep(1000) == 0); + CHECK(m.nb_signals == 0); + CHECK(kill(getpid(), SIGHUP) == 0); + CHECK(kill(getpid(), SIGTERM) == 0); + CHECK(check_reload_then_quit(t, &m) == 0); + + /* + * Another process signals, as an operator does, while ST waits in its + * event system with every coroutine blocked. It sends SIGTERM only after + * the reload is acknowledged, so the order is kept. + */ + int ack[2]; + CHECK(pipe(ack) == 0); + pid_t parent = getpid(); + pid_t pid = fork(); + CHECK(pid >= 0); + if (pid == 0) { + char c; + close(ack[1]); + if (kill(parent, SIGHUP) != 0 || read(ack[0], &c, 1) != 1 || kill(parent, SIGTERM) != 0) { + _exit(1); + } + _exit(0); + } + + close(ack[0]); + memset(&m.signals, 0, sizeof(m.signals)); + m.nb_signals = m.reloads = 0; + CHECK((m.ack = st_netfd_open(ack[1])) != NULL); + CHECK((t = st_thread_create(signal_manager_cycle, &m, 1, 0)) != NULL); + CHECK(check_reload_then_quit(t, &m) == 0); + + int status = -1; + CHECK(waitpid(pid, &status, 0) == pid); + CHECK(WIFEXITED(status) && WEXITSTATUS(status) == 0); + + CHECK(sigaction(SIGHUP, &old_hup, NULL) == 0); + CHECK(sigaction(SIGTERM, &old_term, NULL) == 0); + CHECK(st_netfd_close(m.ack) == 0); + CHECK(st_netfd_close(m.fd) == 0); + CHECK(close(sig_pipe[1]) == 0); + sig_pipe[0] = sig_pipe[1] = -1; + return 0; +} + +/* A writer of the whole stream to fd, then it closes fd. */ +struct writer { + st_netfd_t fd; + int total; + int done; +}; + +static void *writer_cycle(void *arg) +{ + struct writer *w = (struct writer *)arg; + char *buf = malloc(PIPE_CHUNK); + if (!buf) { + return (void *)1; + } + + void *ret = NULL; + for (int off = 0; off < w->total;) { + int n = chunk_size(off, w->total); + for (int i = 0; i < n; i++) { + buf[i] = stream_byte(off + i); + } + if (st_write(w->fd, buf, n, BLOCK_US) != n) { + ret = (void *)2; + break; + } + off += n; + } + free(buf); + + w->done = 1; + if (st_netfd_close(w->fd) != 0) { + return (void *)3; + } + return ret; +} + +/* A pipe between two coroutines: the writer fills it and waits for the reader. */ +static int pipe_fill(void) +{ + int p[2]; + st_netfd_t r, wfd; + CHECK(pipe(p) == 0); + CHECK((r = st_netfd_open(p[0])) != NULL); + CHECK((wfd = st_netfd_open(p[1])) != NULL); + + /* With nothing to read, st_read times out. */ + char c; + st_utime_t start = st_utime(); + errno = 0; + CHECK(st_read(r, &c, 1, SHORT_US) == -1 && errno == ETIME); + CHECK(st_utime() - start >= SHORT_US); + + /* The writer fills the pipe and waits, until the reader reads it all and EOF. */ + struct writer w = {wfd, PIPE_BYTES, 0}; + st_thread_t t; + CHECK((t = st_thread_create(writer_cycle, &w, 1, 0)) != NULL); + CHECK(st_usleep(1000) == 0); + CHECK(w.done == 0); + CHECK(read_stream(r, PIPE_BYTES) == 0); + void *ret = (void *)-1; + CHECK(st_thread_join(t, &ret) == 0); + CHECK(ret == NULL && w.done == 1); + CHECK(st_netfd_close(r) == 0); + + /* + * With no reader running, st_write_resid times out part way, and the + * reader then gets exactly the bytes it reports as sent. + */ + CHECK(pipe(p) == 0); + CHECK((r = st_netfd_open(p[0])) != NULL); + CHECK((wfd = st_netfd_open(p[1])) != NULL); + char *buf = malloc(PIPE_BYTES); + CHECK(buf != NULL); + for (int i = 0; i < PIPE_BYTES; i++) { + buf[i] = stream_byte(i); + } + size_t resid = PIPE_BYTES; + errno = 0; + CHECK(st_write_resid(wfd, buf, &resid, SHORT_US) == -1 && errno == ETIME); + CHECK(resid > 0 && resid < PIPE_BYTES); + int sent = PIPE_BYTES - (int)resid; + int got = 0; + for (;;) { + ssize_t n = st_read(r, buf, PIPE_BYTES, SHORT_US); + if (n < 0) { + CHECK(errno == ETIME); + break; + } + CHECK(n > 0 && got + n <= sent); + for (int i = 0; i < n; i++) { + CHECK(buf[i] == stream_byte(got + i)); + } + got += (int)n; + } + CHECK(got == sent); + free(buf); + + /* With the read end closed, a write fails with EPIPE, as SIGPIPE is ignored. */ + struct sigaction sa, old_pipe; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = SIG_IGN; + sigemptyset(&sa.sa_mask); + CHECK(sigaction(SIGPIPE, &sa, &old_pipe) == 0); + CHECK(st_netfd_close(r) == 0); + errno = 0; + CHECK(st_write(wfd, "x", 1, BLOCK_US) == -1 && errno == EPIPE); + CHECK(sigaction(SIGPIPE, &old_pipe, NULL) == 0); + CHECK(st_netfd_close(wfd) == 0); + return 0; +} + +/* A FIFO opened with st_open: a reader and a writer coroutine. */ +static int fifo(void) +{ + CHECK(mkfifo(FIFO_PATH, 0600) == 0); + + /* st_open always opens non-blocking, so the write end needs a reader first. */ + errno = 0; + CHECK(st_open(FIFO_PATH, O_WRONLY, 0) == NULL && errno == ENXIO); + + st_netfd_t r, wfd; + CHECK((r = st_open(FIFO_PATH, O_RDONLY, 0)) != NULL); + CHECK(fcntl(st_netfd_fileno(r), F_GETFL) & O_NONBLOCK); + CHECK((wfd = st_open(FIFO_PATH, O_WRONLY, 0)) != NULL); + + struct writer w = {wfd, PIPE_BYTES, 0}; + st_thread_t t; + CHECK((t = st_thread_create(writer_cycle, &w, 1, 0)) != NULL); + CHECK(st_usleep(1000) == 0); + CHECK(w.done == 0); + CHECK(read_stream(r, PIPE_BYTES) == 0); + void *ret = (void *)-1; + CHECK(st_thread_join(t, &ret) == 0); + CHECK(ret == NULL && w.done == 1); + CHECK(st_netfd_close(r) == 0); + + CHECK(unlink(FIFO_PATH) == 0); + return 0; +} + +/* A regular file opened with st_open: a write, then a read of it back. */ +static int regular_file(void) +{ + errno = 0; + CHECK(st_open(MISSING_PATH, O_RDONLY, 0) == NULL && errno == ENOENT); + + st_netfd_t fd; + CHECK((fd = st_open(FILE_PATH, O_WRONLY | O_CREAT | O_TRUNC, 0600)) != NULL); + CHECK(st_write(fd, "hello, ", 7, BLOCK_US) == 7); + struct iovec iov[2] = {{"state ", 6}, {"threads", 7}}; + CHECK(st_writev(fd, iov, 2, BLOCK_US) == 13); + CHECK(st_netfd_close(fd) == 0); + + struct stat st; + CHECK(stat(FILE_PATH, &st) == 0 && st.st_size == 20); + + char buf[32]; + CHECK((fd = st_open(FILE_PATH, O_RDONLY, 0)) != NULL); + CHECK(st_read_fully(fd, buf, 20, BLOCK_US) == 20); + CHECK(!memcmp(buf, "hello, state threads", 20)); + CHECK(st_read(fd, buf, sizeof(buf), BLOCK_US) == 0); + CHECK(st_netfd_close(fd) == 0); + + /* O_APPEND is kept: a second open appends after what is there. */ + CHECK((fd = st_open(FILE_PATH, O_WRONLY | O_APPEND, 0)) != NULL); + CHECK(st_write(fd, "!", 1, BLOCK_US) == 1); + CHECK(st_netfd_close(fd) == 0); + CHECK(stat(FILE_PATH, &st) == 0 && st.st_size == 21); + + CHECK(unlink(FILE_PATH) == 0); + return 0; +} + +static int run(void) +{ + CHECK(signal_pipe() == 0); + CHECK(pipe_fill() == 0); + CHECK(fifo() == 0); + CHECK(regular_file() == 0); + return 0; +} + +int main(int argc, char **argv) +{ + CHECK(tool_init() == 0); + + remove_paths(); + int r = run(); + remove_paths(); + CHECK(r == 0); + + printf("pipe OK\n"); + return 0; +} diff --git a/tools/poll/.gitignore b/tools/poll/.gitignore new file mode 100644 index 0000000..0aeab33 --- /dev/null +++ b/tools/poll/.gitignore @@ -0,0 +1 @@ +poll diff --git a/tools/poll/Makefile b/tools/poll/Makefile new file mode 100644 index 0000000..b32514c --- /dev/null +++ b/tools/poll/Makefile @@ -0,0 +1,24 @@ +.PHONY: clean + +LDLIBS=../../obj/libst.a +CFLAGS=-g -O0 -I../../obj -I.. -DDEBUG + +OS_NAME = $(shell uname -s) +ST_TARGET = linux-debug +ifeq ($(OS_NAME), Darwin) +ST_TARGET = darwin-debug +CPU_ARCHS = $(shell g++ -dM -E - +#include +#include + +/* Long enough that a coroutine still blocked here is a failure, not a slow run. */ +#define BLOCK_US (5 * 1000 * 1000) + +/* A timeout that is expected to expire. */ +#define SHORT_US (5 * 1000) + +/* A channel: the end that is polled, and the peer that writes to it. */ +struct channel { + st_netfd_t rd; + st_netfd_t wr; +}; + +/* The channels polled together: two pipes, a Unix socketpair and a TCP connection. */ +#define NB_CHANNELS 4 +static struct channel channels[NB_CHANNELS]; + +static int open_pipe(struct channel *c) +{ + int fds[2]; + CHECK(pipe(fds) == 0); + CHECK((c->rd = st_netfd_open(fds[0])) != NULL); + CHECK((c->wr = st_netfd_open(fds[1])) != NULL); + return 0; +} + +static int open_socketpair(struct channel *c) +{ + int fds[2]; + CHECK(socketpair(AF_UNIX, SOCK_STREAM, 0, fds) == 0); + CHECK((c->rd = st_netfd_open_socket(fds[0])) != NULL); + CHECK((c->wr = st_netfd_open_socket(fds[1])) != NULL); + return 0; +} + +static int open_tcp(struct channel *c) +{ + int port = 0; + st_netfd_t lfd; + CHECK((lfd = tool_listen(AF_INET, 16, &port)) != NULL); + CHECK((c->wr = tool_connect(AF_INET, port, BLOCK_US)) != NULL); + CHECK((c->rd = st_accept(lfd, NULL, NULL, BLOCK_US)) != NULL); + CHECK(st_netfd_close(lfd) == 0); + return 0; +} + +static int open_channels(void) +{ + CHECK(open_pipe(&channels[0]) == 0); + CHECK(open_pipe(&channels[1]) == 0); + CHECK(open_socketpair(&channels[2]) == 0); + CHECK(open_tcp(&channels[3]) == 0); + return 0; +} + +/* Close every channel; nothing is left registered, so each close succeeds. */ +static int close_channels(void) +{ + for (int i = 0; i < NB_CHANNELS; i++) { + CHECK(st_netfd_close(channels[i].rd) == 0); + CHECK(st_netfd_close(channels[i].wr) == 0); + } + return 0; +} + +/* Fill pds with the read end of every channel, for POLLIN. */ +static void poll_all(struct pollfd *pds) +{ + for (int i = 0; i < NB_CHANNELS; i++) { + pds[i].fd = st_netfd_fileno(channels[i].rd); + pds[i].events = POLLIN; + pds[i].revents = 0; + } +} + +/* Read the one byte c waits with on channel i. */ +static int read_byte(int i, char c) +{ + char ch = 0; + CHECK(st_read(channels[i].rd, &ch, 1, BLOCK_US) == 1); + CHECK(ch == c); + return 0; +} + +/* + * With data on some channels already, st_poll returns at once, counts only + * the ready descriptors, and sets revents on each entry: POLLIN where data + * waits, POLLOUT where there is room, and 0 on the quiet ones. + */ +static int ready_only(void) +{ + CHECK(st_write(channels[1].wr, "b", 1, BLOCK_US) == 1); + CHECK(st_write(channels[3].wr, "d", 1, BLOCK_US) == 1); + + /* + * A pipe or socketpair write is readable at once, but loopback TCP may + * deliver it a little later, as on macOS; wait until it has arrived. + */ + CHECK(st_netfd_poll(channels[3].rd, POLLIN, BLOCK_US) == 0); + + struct pollfd pds[NB_CHANNELS + 2]; + poll_all(pds); + /* A pipe write end with room. */ + pds[4].fd = st_netfd_fileno(channels[0].wr); + pds[4].events = POLLOUT; + pds[4].revents = 0; + /* The TCP client end, with room. */ + pds[5].fd = st_netfd_fileno(channels[3].wr); + pds[5].events = POLLOUT; + pds[5].revents = 0; + + st_utime_t start = st_utime(); + CHECK(st_poll(pds, NB_CHANNELS + 2, BLOCK_US) == 4); + CHECK(st_utime() - start < BLOCK_US / 2); + CHECK(pds[0].revents == 0); + CHECK(pds[1].revents == POLLIN); + CHECK(pds[2].revents == 0); + CHECK(pds[3].revents == POLLIN); + CHECK(pds[4].revents == POLLOUT); + CHECK(pds[5].revents == POLLOUT); + + /* The same TCP socket asked for both: data waits and there is room. */ + struct pollfd pd = {st_netfd_fileno(channels[3].rd), POLLIN | POLLOUT, 0}; + CHECK(st_poll(&pd, 1, BLOCK_US) == 1); + CHECK(pd.revents == (POLLIN | POLLOUT)); + + CHECK(read_byte(1, 'b') == 0); + CHECK(read_byte(3, 'd') == 0); + + /* Drained, only the room is left. */ + pd.revents = 0; + CHECK(st_poll(&pd, 1, BLOCK_US) == 1); + CHECK(pd.revents == POLLOUT); + return 0; +} + +/* + * A worker that waits on every channel at once, with no timeout, and keeps + * one pollfd array for its whole loop. It reads one byte from each channel + * that is ready, and quits after the byte 'q'. + */ +#define NB_MESSAGES 16 +struct worker { + int polls; + int ready[NB_MESSAGES + 1]; + int order[NB_MESSAGES + 1]; + char bytes[NB_MESSAGES + 1]; + int nb_messages; +}; + +static void *worker_cycle(void *arg) +{ + struct worker *w = (struct worker *)arg; + struct pollfd pds[NB_CHANNELS]; + poll_all(pds); + + while (w->polls <= NB_MESSAGES) { + int n = st_poll(pds, NB_CHANNELS, ST_UTIME_NO_TIMEOUT); + if (n <= 0) { + return (void *)1; + } + w->ready[w->polls++] = n; + + for (int i = 0; i < NB_CHANNELS; i++) { + if (!pds[i].revents) { + continue; + } + if (pds[i].revents != POLLIN || w->nb_messages > NB_MESSAGES) { + return (void *)2; + } + char ch; + if (st_read(channels[i].rd, &ch, 1, BLOCK_US) != 1) { + return (void *)3; + } + w->order[w->nb_messages] = i; + w->bytes[w->nb_messages++] = ch; + if (ch == 'q') { + return NULL; + } + } + } + return (void *)4; +} + +/* The worker wakes for each byte, on the channel it came on, in order. */ +static int worker(void) +{ + struct worker w; + memset(&w, 0, sizeof(w)); + st_thread_t t; + CHECK((t = st_thread_create(worker_cycle, &w, 1, 0)) != NULL); + + /* With nothing to do, the worker waits. */ + CHECK(st_usleep(1000) == 0); + CHECK(w.polls == 0); + + for (int k = 0; k < NB_MESSAGES; k++) { + int i = (k * 3 + k / NB_CHANNELS) % NB_CHANNELS; + char ch = (char)('a' + k); + CHECK(st_write(channels[i].wr, &ch, 1, BLOCK_US) == 1); + /* Wait until the worker reads it, so one byte is pending at a time. */ + for (int n = 0; n < 1000 && w.nb_messages < k + 1; n++) { + CHECK(st_usleep(100) == 0); + } + CHECK(w.nb_messages == k + 1); + CHECK(w.order[k] == i && w.bytes[k] == ch); + } + + CHECK(st_write(channels[2].wr, "q", 1, BLOCK_US) == 1); + void *ret = (void *)-1; + CHECK(st_thread_join(t, &ret) == 0); + CHECK(ret == NULL); + CHECK(w.polls == NB_MESSAGES + 1 && w.nb_messages == NB_MESSAGES + 1); + for (int k = 0; k < w.polls; k++) { + CHECK(w.ready[k] == 1); + } + CHECK(w.order[NB_MESSAGES] == 2 && w.bytes[NB_MESSAGES] == 'q'); + return 0; +} + +/* + * With nothing ready, st_poll returns 0 after the timeout and leaves errno + * alone, as poll(2) does (sched.c:115). Only st_netfd_poll, built on it, + * turns that 0 into -1 with ETIME (io.c:253). + */ +static int timeout(void) +{ + struct pollfd pds[NB_CHANNELS]; + poll_all(pds); + + /* The timeout counts from the clock reading this yield takes. */ + st_utime_t start = st_utime(); + st_thread_yield(); + errno = 0; + CHECK(st_poll(pds, NB_CHANNELS, SHORT_US) == 0); + CHECK(errno == 0); + CHECK(st_utime() - start >= SHORT_US); + + start = st_utime(); + st_thread_yield(); + errno = 0; + CHECK(st_netfd_poll(channels[2].rd, POLLIN, SHORT_US) == -1 && errno == ETIME); + CHECK(st_utime() - start >= SHORT_US); + return 0; +} + +/* A coroutine that waits in st_poll on every channel, or in st_netfd_poll on one. */ +struct poller { + int netfd; + int r0; + int err; + /* The result of a second st_poll with a short timeout, or 1 if not run. */ + int r1; + int again; +}; + +static void *poller_cycle(void *arg) +{ + struct poller *p = (struct poller *)arg; + struct pollfd pds[NB_CHANNELS]; + poll_all(pds); + + errno = 0; + if (p->netfd) { + p->r0 = st_netfd_poll(channels[3].rd, POLLIN, BLOCK_US); + } else { + p->r0 = st_poll(pds, NB_CHANNELS, BLOCK_US); + } + p->err = errno; + + p->r1 = 1; + if (p->again) { + p->r1 = st_poll(pds, NB_CHANNELS, SHORT_US); + } + return NULL; +} + +/* Both calls, waiting, are interrupted with EINTR, as a coroutine stop does. */ +static int interrupt(void) +{ + for (int netfd = 0; netfd < 2; netfd++) { + struct poller p; + memset(&p, 0, sizeof(p)); + p.netfd = netfd; + st_thread_t t; + CHECK((t = st_thread_create(poller_cycle, &p, 1, 0)) != NULL); + CHECK(st_usleep(1000) == 0); + + st_utime_t start = st_utime(); + st_thread_interrupt(t); + CHECK(st_thread_join(t, NULL) == 0); + CHECK(st_utime() - start < BLOCK_US / 2); + CHECK(p.r0 == -1 && p.err == EINTR); + } + + /* + * Interrupted before it runs, st_poll fails with EINTR at once, without + * waiting; the interrupt is used up, so the next st_poll times out. + */ + struct poller p; + memset(&p, 0, sizeof(p)); + p.again = 1; + st_thread_t t; + CHECK((t = st_thread_create(poller_cycle, &p, 1, 0)) != NULL); + st_thread_interrupt(t); + CHECK(st_thread_join(t, NULL) == 0); + CHECK(p.r0 == -1 && p.err == EINTR); + CHECK(p.r1 == 0); + return 0; +} + +/* A coroutine that writes one byte to a channel after a short sleep. */ +struct late_writer { + st_netfd_t fd; + st_utime_t delay; +}; + +static void *late_writer_cycle(void *arg) +{ + struct late_writer *w = (struct late_writer *)arg; + if (st_usleep(w->delay) != 0) { + return (void *)1; + } + if (st_write(w->fd, "x", 1, BLOCK_US) != 1) { + return (void *)2; + } + return NULL; +} + +/* A coroutine that reads total bytes from a pipe it drains. */ +struct drainer { + st_netfd_t fd; + int total; + int got; +}; + +static void *drainer_cycle(void *arg) +{ + struct drainer *d = (struct drainer *)arg; + char buf[8192]; + while (d->got < d->total) { + ssize_t n = st_read(d->fd, buf, sizeof(buf), BLOCK_US); + if (n <= 0) { + return (void *)1; + } + d->got += (int)n; + } + return NULL; +} + +/* st_netfd_poll waits for one descriptor to be readable or writable. */ +static int netfd_poll(void) +{ + /* POLLIN wakes when a byte arrives later, and the read then gets it. */ + for (int i = 0; i < NB_CHANNELS; i++) { + struct late_writer w = {channels[i].wr, 1000}; + st_thread_t t; + CHECK((t = st_thread_create(late_writer_cycle, &w, 1, 0)) != NULL); + st_utime_t start = st_utime(); + CHECK(st_netfd_poll(channels[i].rd, POLLIN, BLOCK_US) == 0); + CHECK(st_utime() - start < BLOCK_US / 2); + void *ret = (void *)-1; + CHECK(st_thread_join(t, &ret) == 0); + CHECK(ret == NULL); + CHECK(read_byte(i, 'x') == 0); + } + + /* POLLOUT on sockets and a pipe with room returns at once. */ + CHECK(st_netfd_poll(channels[0].wr, POLLOUT, SHORT_US) == 0); + CHECK(st_netfd_poll(channels[2].wr, POLLOUT, SHORT_US) == 0); + CHECK(st_netfd_poll(channels[3].rd, POLLOUT, SHORT_US) == 0); + CHECK(st_netfd_poll(channels[3].rd, POLLIN | POLLOUT, SHORT_US) == 0); + + /* A full pipe is not writable; it is again once a reader drains it. */ + struct channel c; + CHECK(open_pipe(&c) == 0); + int wfd = st_netfd_fileno(c.wr); + CHECK(fcntl(wfd, F_GETFL) & O_NONBLOCK); + char buf[4096]; + memset(buf, 'f', sizeof(buf)); + int filled = 0; + for (;;) { + ssize_t n = write(wfd, buf, sizeof(buf)); + if (n < 0) { + CHECK(errno == EAGAIN || errno == EWOULDBLOCK); + break; + } + filled += (int)n; + } + CHECK(filled > 0); + + st_utime_t start = st_utime(); + st_thread_yield(); + errno = 0; + CHECK(st_netfd_poll(c.wr, POLLOUT, SHORT_US) == -1 && errno == ETIME); + CHECK(st_utime() - start >= SHORT_US); + + struct drainer d = {c.rd, filled, 0}; + st_thread_t t; + CHECK((t = st_thread_create(drainer_cycle, &d, 1, 0)) != NULL); + CHECK(st_netfd_poll(c.wr, POLLOUT, BLOCK_US) == 0); + void *ret = (void *)-1; + CHECK(st_thread_join(t, &ret) == 0); + CHECK(ret == NULL && d.got == filled); + + /* At EOF the read end is readable, and the read returns 0. */ + CHECK(st_netfd_close(c.wr) == 0); + CHECK(st_netfd_poll(c.rd, POLLIN, BLOCK_US) == 0); + CHECK(st_read(c.rd, buf, sizeof(buf), BLOCK_US) == 0); + CHECK(st_netfd_close(c.rd) == 0); + return 0; +} + +static int run(void) +{ + CHECK(open_channels() == 0); + CHECK(ready_only() == 0); + CHECK(worker() == 0); + CHECK(timeout() == 0); + CHECK(interrupt() == 0); + CHECK(netfd_poll() == 0); + CHECK(close_channels() == 0); + return 0; +} + +int main(int argc, char **argv) +{ + CHECK(tool_init() == 0); + CHECK(run() == 0); + + printf("poll OK\n"); + return 0; +} diff --git a/tools/porting/porting.c b/tools/porting/porting.c index 127a357..9f6155f 100644 --- a/tools/porting/porting.c +++ b/tools/porting/porting.c @@ -71,6 +71,28 @@ int main(int argc, char** argv) // Not reachable code. printf("Should never be here.\n"); + return 1; + } + + // The OS and CPU pairs that md.h supports. + int supported = 0; +#if defined(__APPLE__) +#if defined(__x86_64__) || defined(__aarch64__) + supported = 1; +#endif +#elif defined(__linux__) +#if defined(__i386__) || defined(__x86_64__) || defined(__aarch64__) || defined(__arm__) || defined(__mips__) \ + || defined(__mips64) || defined(__riscv) || defined(__loongarch64) + supported = 1; +#endif +#elif defined(__CYGWIN__) +#if defined(__x86_64__) + supported = 1; +#endif +#endif + if (!supported) { + printf("\nUnsupported OS or CPU, port md.h and an md_*.S file first\n"); + return 1; } printf("\nDone\n"); diff --git a/tools/stack/.gitignore b/tools/stack/.gitignore deleted file mode 100644 index 881ae71..0000000 --- a/tools/stack/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -stack - diff --git a/tools/stack/stack.c b/tools/stack/stack.c deleted file mode 100644 index b673caf..0000000 --- a/tools/stack/stack.c +++ /dev/null @@ -1,17 +0,0 @@ -/* SPDX-License-Identifier: MIT */ -/* Copyright (c) 2026 Winlin */ - -long foo() { - char c; - int i; - long l; - long long ll; - return c + i + l + ll; -} - -int main(int argc, char** argv) -{ - foo(); - return 0; -} - diff --git a/tools/stress/.gitignore b/tools/stress/.gitignore new file mode 100644 index 0000000..def4a0c --- /dev/null +++ b/tools/stress/.gitignore @@ -0,0 +1 @@ +stress diff --git a/tools/stress/Makefile b/tools/stress/Makefile new file mode 100644 index 0000000..faeb6d6 --- /dev/null +++ b/tools/stress/Makefile @@ -0,0 +1,24 @@ +.PHONY: clean + +LDLIBS=../../obj/libst.a +CFLAGS=-g -O0 -I../../obj -I.. -DDEBUG + +OS_NAME = $(shell uname -s) +ST_TARGET = linux-debug +ifeq ($(OS_NAME), Darwin) +ST_TARGET = darwin-debug +CPU_ARCHS = $(shell g++ -dM -E - +#include +#include + +/* Long enough that a coroutine still blocked here is a failure, not a slow run. */ +#define BLOCK_US (5 * 1000 * 1000) + +#define DEFAULT_COUNT 200 + +/* Messages per connection, and the largest payload. */ +#define MESSAGES 8 +#define MAX_PAYLOAD 4096 +#define HEADER 4 + +/* Create and exit cycles, and the bytes each coroutine writes on its stack. */ +#define CYCLES 20 +#define STACK_FILL 4096 + +/* Rounds through the mutex, and passes of the token around the cond. */ +#define LOCK_ROUNDS 10 +#define RING_ROUNDS 3 + +/* A coroutine fails by returning the line it failed at. */ +#define FAIL() return (void *)(long)__LINE__ + +/* Join t, and check it returned want; a failed coroutine prints its line. */ +static int join(st_thread_t t, void *want) +{ + void *ret = (void *)-1; + CHECK(st_thread_join(t, &ret) == 0); + if (ret != want) { + printf("ST: FAILED coroutine returned %ld, want %ld\n", (long)ret, (long)want); + return 1; + } + return 0; +} + +static int count = DEFAULT_COUNT; + +/* The payload of message seq on connection id: its size and its bytes. */ +static int payload_size(int id, int seq) +{ + return 1 + (id * 7919 + seq * 3571) % MAX_PAYLOAD; +} + +static void payload_fill(int id, int seq, char *buf, int size) +{ + for (int i = 0; i < size; i++) { + buf[i] = (char)(id * 31 + seq * 7 + i); + } +} + +static int nodelay(st_netfd_t fd) +{ + int one = 1; + return setsockopt(st_netfd_fileno(fd), IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one)); +} + +static struct { + st_netfd_t listener; + int port; + st_thread_t *handlers; + int accepted; + st_cond_t accept_cond; + /* Clients wait on start_cond until every connection is up. */ + int connected; + st_cond_t start_cond; + int open; + int peak; + int server_msgs; + long server_bytes; + int client_msgs; + long client_bytes; +} c; + +/* Echo each message, a 4-byte size and its payload, until the client closes. */ +static void *handler(void *arg) +{ + st_netfd_t conn = (st_netfd_t)arg; + char buf[HEADER + MAX_PAYLOAD]; + if (++c.open > c.peak) { + c.peak = c.open; + } + + for (;;) { + ssize_t n = st_read_fully(conn, buf, HEADER, BLOCK_US); + if (n == 0) { + break; /* The client closed. */ + } + if (n != HEADER) { + FAIL(); + } + uint32_t v; + memcpy(&v, buf, HEADER); + int size = (int)ntohl(v); + if (size <= 0 || size > MAX_PAYLOAD) { + FAIL(); + } + if (st_read_fully(conn, buf + HEADER, size, BLOCK_US) != size) { + FAIL(); + } + if (st_write(conn, buf, HEADER + size, BLOCK_US) != HEADER + size) { + FAIL(); + } + c.server_msgs++; + c.server_bytes += HEADER + size; + } + + c.open--; + if (st_netfd_close(conn) != 0) { + FAIL(); + } + return NULL; +} + +/* Accept every client, each connection in its own coroutine. */ +static void *acceptor(void *arg) +{ + while (c.accepted < count) { + st_netfd_t conn = st_accept(c.listener, NULL, NULL, BLOCK_US); + if (!conn || nodelay(conn) != 0) { + FAIL(); + } + st_thread_t t = st_thread_create(handler, conn, 1, 0); + if (!t) { + FAIL(); + } + c.handlers[c.accepted++] = t; + st_cond_signal(c.accept_cond); + } + return NULL; +} + +static void *client(void *arg) +{ + int id = (int)(long)arg; + char sent[HEADER + MAX_PAYLOAD], got[HEADER + MAX_PAYLOAD]; + + st_netfd_t fd = tool_connect(AF_INET, c.port, BLOCK_US); + if (!fd || nodelay(fd) != 0) { + FAIL(); + } + + /* Exchange nothing until every connection is up, so all are open at once. */ + if (++c.connected == count) { + st_cond_broadcast(c.start_cond); + } + while (c.connected < count) { + if (st_cond_timedwait(c.start_cond, BLOCK_US) != 0) { + FAIL(); + } + } + + for (int seq = 0; seq < MESSAGES; seq++) { + int size = payload_size(id, seq); + uint32_t v = htonl((uint32_t)size); + memcpy(sent, &v, HEADER); + payload_fill(id, seq, sent + HEADER, size); + + /* The header and the payload go out as two iovecs in one write. */ + struct iovec iov[2] = {{sent, HEADER}, {sent + HEADER, size}}; + if (st_writev(fd, iov, 2, BLOCK_US) != HEADER + size) { + FAIL(); + } + memset(got, 0, HEADER + size); + if (st_read_fully(fd, got, HEADER + size, BLOCK_US) != HEADER + size) { + FAIL(); + } + if (memcmp(sent, got, HEADER + size) != 0) { + FAIL(); + } + c.client_msgs++; + c.client_bytes += HEADER + size; + } + + if (st_netfd_close(fd) != 0) { + FAIL(); + } + return NULL; +} + +static int connections(void) +{ + memset(&c, 0, sizeof(c)); + CHECK((c.handlers = calloc(count, sizeof(st_thread_t))) != NULL); + CHECK((c.accept_cond = st_cond_new()) != NULL); + CHECK((c.start_cond = st_cond_new()) != NULL); + CHECK((c.listener = tool_listen(AF_INET, count, &c.port)) != NULL); + + st_thread_t a, *clients; + CHECK((clients = calloc(count, sizeof(st_thread_t))) != NULL); + CHECK((a = st_thread_create(acceptor, NULL, 1, 0)) != NULL); + + /* + * Start the clients in batches the accept queue holds, waiting for each + * batch to be accepted, so no connect waits for a dropped SYN to retry. + */ + for (int i = 0; i < count; i++) { + CHECK((clients[i] = st_thread_create(client, (void *)(long)i, 1, 0)) != NULL); + if ((i + 1) % 64 == 0 || i == count - 1) { + while (c.accepted < i + 1) { + CHECK(st_cond_timedwait(c.accept_cond, BLOCK_US) == 0); + } + } + } + + CHECK(join(a, NULL) == 0); + for (int i = 0; i < count; i++) { + CHECK(join(clients[i], NULL) == 0); + } + for (int i = 0; i < count; i++) { + CHECK(join(c.handlers[i], NULL) == 0); + } + CHECK(st_netfd_close(c.listener) == 0); + CHECK(st_cond_destroy(c.accept_cond) == 0); + CHECK(st_cond_destroy(c.start_cond) == 0); + free(clients); + free(c.handlers); + + /* Every connection was open at once, and exchanged all its data. */ + CHECK(c.accepted == count); + CHECK(c.peak == count); + CHECK(c.open == 0); + CHECK(c.client_msgs == count * MESSAGES); + CHECK(c.server_msgs == count * MESSAGES); + long bytes = 0; + for (int id = 0; id < count; id++) { + for (int seq = 0; seq < MESSAGES; seq++) { + bytes += HEADER + payload_size(id, seq); + } + } + CHECK(c.client_bytes == bytes); + CHECK(c.server_bytes == bytes); + return 0; +} + +static int detached_done; + +/* + * Fill STACK_FILL bytes of this coroutine's stack with its own pattern, + * yield so the others run, and check the pattern is still there. + */ +static void *cycler(void *arg) +{ + long id = (long)arg; + volatile char buf[STACK_FILL]; + for (int i = 0; i < STACK_FILL; i++) { + buf[i] = (char)(id + i); + } + st_thread_yield(); + for (int i = 0; i < STACK_FILL; i++) { + if (buf[i] != (char)(id + i)) { + FAIL(); + } + } + /* A detached coroutine has no joiner, so it counts itself. */ + if (id % 4 == 3) { + detached_done++; + } + /* Every other one exits with st_thread_exit instead of returning. */ + if (id % 2) { + st_thread_exit((void *)(id + 1000)); + } + return (void *)(id + 1000); +} + +static int cycles(void) +{ + /* The default stack, and two custom sizes. */ + static const int sizes[] = {0, 32 * 1024, 128 * 1024}; + st_thread_t *threads; + CHECK((threads = calloc(count, sizeof(st_thread_t))) != NULL); + + for (int cycle = 0; cycle < CYCLES; cycle++) { + detached_done = 0; + int detached = 0; + for (long id = 0; id < count; id++) { + int joinable = (id % 4 != 3); + int size = sizes[(id + cycle) % 3]; + CHECK((threads[id] = st_thread_create(cycler, (void *)id, joinable, size)) != NULL); + detached += !joinable; + } + for (long id = 0; id < count; id++) { + if (id % 4 != 3) { + CHECK(join(threads[id], (void *)(id + 1000)) == 0); + } + } + /* The joins switch enough for every detached coroutine to finish. */ + while (detached_done < detached) { + st_thread_yield(); + } + CHECK(detached_done == detached); + } + + free(threads); + return 0; +} + +static struct { + st_mutex_t mutex; + st_cond_t cond; + int inside; + int counter; + int turn; + int passes; + int last; +} k; + +/* + * Take the mutex LOCK_ROUNDS times, yielding inside it, then pass a token + * around every coroutine through one cond, RING_ROUNDS times. + */ +static void *contender(void *arg) +{ + int id = (int)(long)arg; + + for (int i = 0; i < LOCK_ROUNDS; i++) { + if (st_mutex_lock(k.mutex) != 0) { + FAIL(); + } + if (++k.inside != 1) { + FAIL(); + } + int v = k.counter; + st_thread_yield(); + k.counter = v + 1; + k.inside--; + if (st_mutex_unlock(k.mutex) != 0) { + FAIL(); + } + } + + for (int i = 0; i < RING_ROUNDS; i++) { + while (k.turn != id) { + if (st_cond_timedwait(k.cond, BLOCK_US) != 0) { + FAIL(); + } + } + /* The token comes from the coroutine before this one. */ + if (k.last != (id + count - 1) % count) { + FAIL(); + } + k.last = id; + k.passes++; + k.turn = (id + 1) % count; + if (st_cond_broadcast(k.cond) != 0) { + FAIL(); + } + } + return NULL; +} + +static int contention(void) +{ + memset(&k, 0, sizeof(k)); + k.last = count - 1; + CHECK((k.mutex = st_mutex_new()) != NULL); + CHECK((k.cond = st_cond_new()) != NULL); + + st_thread_t *threads; + CHECK((threads = calloc(count, sizeof(st_thread_t))) != NULL); + for (long id = 0; id < count; id++) { + CHECK((threads[id] = st_thread_create(contender, (void *)id, 1, 0)) != NULL); + } + for (int id = 0; id < count; id++) { + CHECK(join(threads[id], NULL) == 0); + } + free(threads); + + CHECK(k.counter == count * LOCK_ROUNDS); + CHECK(k.inside == 0); + CHECK(k.passes == count * RING_ROUNDS); + CHECK(k.turn == 0); + CHECK(st_mutex_destroy(k.mutex) == 0); + CHECK(st_cond_destroy(k.cond) == 0); + return 0; +} + +int main(int argc, char **argv) +{ + if (argc > 1) { + count = atoi(argv[1]); + } + CHECK(count >= 2); + + /* + * Each connection has a client and a server descriptor. Raise the soft + * limit for a large count before st_init, which reads it. + */ + int need = 2 * count + 32; + struct rlimit rl; + CHECK(getrlimit(RLIMIT_NOFILE, &rl) == 0); + if (rl.rlim_cur < (rlim_t)need) { + rl.rlim_cur = need; + setrlimit(RLIMIT_NOFILE, &rl); + } + + CHECK(tool_init() == 0); + if (st_getfdlimit() < need) { + printf("ST: FAILED count %d needs %d descriptors, the limit is %d\n", count, need, st_getfdlimit()); + return 1; + } + + CHECK(connections() == 0); + CHECK(cycles() == 0); + CHECK(contention() == 0); + + printf("stress OK\n"); + return 0; +} diff --git a/tools/sync/.gitignore b/tools/sync/.gitignore new file mode 100644 index 0000000..def1d9c --- /dev/null +++ b/tools/sync/.gitignore @@ -0,0 +1 @@ +sync diff --git a/tools/sync/Makefile b/tools/sync/Makefile new file mode 100644 index 0000000..ccbaa6d --- /dev/null +++ b/tools/sync/Makefile @@ -0,0 +1,24 @@ +.PHONY: clean + +LDLIBS=../../obj/libst.a +CFLAGS=-g -O0 -I../../obj -I.. -DDEBUG + +OS_NAME = $(shell uname -s) +ST_TARGET = linux-debug +ifeq ($(OS_NAME), Darwin) +ST_TARGET = darwin-debug +CPU_ARCHS = $(shell g++ -dM -E - = TIMEOUT_US); + + /* A zero timeout also times out. */ + errno = 0; + CHECK(st_cond_timedwait(timed_cond, 0) == -1 && errno == ETIME); + + /* Signaled before the timeout, the wait returns 0 at once. */ + st_thread_t t; + CHECK((t = st_thread_create(timed_waiter, NULL, 1, 0)) != NULL); + st_usleep(1000); + CHECK(timed_rv == -2); + start = st_utime(); + CHECK(st_cond_signal(timed_cond) == 0); + CHECK(st_thread_join(t, NULL) == 0); + CHECK(timed_rv == 0); + CHECK(st_utime() - start < BLOCK_US); + + CHECK(st_cond_destroy(timed_cond) == 0); + return 0; +} + +/* + * A mutex guards a counter that each coroutine reads, then yields, then + * writes back. Without the lock, the yields would lose updates. + */ +#define LOCK_WORKERS 8 +#define LOCK_ROUNDS 50 + +static st_mutex_t lock; +static int counter; +static int inside; +static int max_inside; + +static void *lock_worker(void *arg) +{ + for (int i = 0; i < LOCK_ROUNDS; i++) { + if (st_mutex_lock(lock) != 0) { + return (void *)1; + } + if (++inside > max_inside) { + max_inside = inside; + } + + int v = counter; + if (i % 2) { + st_thread_yield(); + } else { + st_usleep(0); + } + counter = v + 1; + + inside--; + if (st_mutex_unlock(lock) != 0) { + return (void *)2; + } + st_thread_yield(); + } + return NULL; +} + +static int mutex(void) +{ + CHECK((lock = st_mutex_new()) != NULL); + counter = inside = max_inside = 0; + + st_thread_t workers[LOCK_WORKERS]; + for (int i = 0; i < LOCK_WORKERS; i++) { + CHECK((workers[i] = st_thread_create(lock_worker, NULL, 1, 0)) != NULL); + } + for (int i = 0; i < LOCK_WORKERS; i++) { + void *ret = (void *)-1; + CHECK(st_thread_join(workers[i], &ret) == 0); + CHECK(ret == NULL); + } + CHECK(counter == LOCK_WORKERS * LOCK_ROUNDS); + CHECK(max_inside == 1); + + CHECK(st_mutex_destroy(lock) == 0); + return 0; +} + +/* Only the owner unlocks, and the owner cannot lock again. */ +static st_mutex_t owned; +static int other_rv; +static int other_errno; + +static void *unlock_other(void *arg) +{ + errno = 0; + other_rv = st_mutex_unlock(owned); + other_errno = errno; + return NULL; +} + +static int ownership(void) +{ + CHECK((owned = st_mutex_new()) != NULL); + + errno = 0; + CHECK(st_mutex_unlock(owned) == -1 && errno == EPERM); + + CHECK(st_mutex_lock(owned) == 0); + errno = 0; + CHECK(st_mutex_lock(owned) == -1 && errno == EDEADLK); + + st_thread_t t; + CHECK((t = st_thread_create(unlock_other, NULL, 1, 0)) != NULL); + CHECK(st_thread_join(t, NULL) == 0); + CHECK(other_rv == -1 && other_errno == EPERM); + + CHECK(st_mutex_unlock(owned) == 0); + CHECK(st_mutex_destroy(owned) == 0); + return 0; +} + +/* st_mutex_trylock fails with EBUSY while another coroutine holds the mutex. */ +static st_mutex_t trylocked; +static int try_rv; +static int try_errno; + +static void *try_other(void *arg) +{ + errno = 0; + try_rv = st_mutex_trylock(trylocked); + try_errno = errno; + if (try_rv == 0 && st_mutex_unlock(trylocked) != 0) { + return (void *)1; + } + return NULL; +} + +static int trylock(void) +{ + CHECK((trylocked = st_mutex_new()) != NULL); + + /* A free mutex is taken, and the owner unlocks it. */ + CHECK(st_mutex_trylock(trylocked) == 0); + + st_thread_t t; + void *ret = (void *)-1; + CHECK((t = st_thread_create(try_other, NULL, 1, 0)) != NULL); + CHECK(st_thread_join(t, &ret) == 0); + CHECK(ret == NULL); + CHECK(try_rv == -1 && try_errno == EBUSY); + + CHECK(st_mutex_unlock(trylocked) == 0); + + /* Free again, another coroutine takes it. */ + ret = (void *)-1; + CHECK((t = st_thread_create(try_other, NULL, 1, 0)) != NULL); + CHECK(st_thread_join(t, &ret) == 0); + CHECK(ret == NULL); + CHECK(try_rv == 0); + + CHECK(st_mutex_destroy(trylocked) == 0); + return 0; +} + +/* + * st_thread_interrupt wakes a coroutine waiting in st_cond_wait or + * st_mutex_lock with EINTR, and the interrupted one does not get the mutex. + */ +static st_cond_t intr_cond; +static st_mutex_t intr_lock; +static int intr_rv; +static int intr_errno; + +static void *intr_cond_waiter(void *arg) +{ + errno = 0; + intr_rv = st_cond_wait(intr_cond); + intr_errno = errno; + return NULL; +} + +static void *intr_lock_waiter(void *arg) +{ + errno = 0; + intr_rv = st_mutex_lock(intr_lock); + intr_errno = errno; + return NULL; +} + +static int interrupt(void) +{ + CHECK((intr_cond = st_cond_new()) != NULL); + CHECK((intr_lock = st_mutex_new()) != NULL); + + st_thread_t t; + intr_rv = -2; + CHECK((t = st_thread_create(intr_cond_waiter, NULL, 1, 0)) != NULL); + st_usleep(0); + CHECK(intr_rv == -2); + st_thread_interrupt(t); + CHECK(st_thread_join(t, NULL) == 0); + CHECK(intr_rv == -1 && intr_errno == EINTR); + CHECK(st_cond_destroy(intr_cond) == 0); + + CHECK(st_mutex_lock(intr_lock) == 0); + intr_rv = -2; + CHECK((t = st_thread_create(intr_lock_waiter, NULL, 1, 0)) != NULL); + st_usleep(0); + CHECK(intr_rv == -2); + st_thread_interrupt(t); + CHECK(st_thread_join(t, NULL) == 0); + CHECK(intr_rv == -1 && intr_errno == EINTR); + + /* The main thread still owns it, and no one waits. */ + CHECK(st_mutex_unlock(intr_lock) == 0); + CHECK(st_mutex_trylock(intr_lock) == 0); + CHECK(st_mutex_unlock(intr_lock) == 0); + CHECK(st_mutex_destroy(intr_lock) == 0); + return 0; +} + +/* Destroying a cond or mutex still in use fails with EBUSY. */ +static st_cond_t busy_cond; +static st_mutex_t busy_lock; +static int busy_done; + +static void *busy_cond_waiter(void *arg) +{ + if (st_cond_timedwait(busy_cond, BLOCK_US) != 0) { + return (void *)1; + } + busy_done = 1; + return NULL; +} + +static void *busy_lock_waiter(void *arg) +{ + if (st_mutex_lock(busy_lock) != 0) { + return (void *)1; + } + /* Own it for a while, so destroying it is still busy. */ + st_usleep(0); + if (st_mutex_unlock(busy_lock) != 0) { + return (void *)2; + } + busy_done = 1; + return NULL; +} + +static int destroy(void) +{ + /* Free ones are destroyed at once. */ + st_cond_t c; + st_mutex_t m; + CHECK((c = st_cond_new()) != NULL); + CHECK(st_cond_destroy(c) == 0); + CHECK((m = st_mutex_new()) != NULL); + CHECK(st_mutex_destroy(m) == 0); + + /* A cond with a waiter. */ + CHECK((busy_cond = st_cond_new()) != NULL); + st_thread_t t; + void *ret = (void *)-1; + busy_done = 0; + CHECK((t = st_thread_create(busy_cond_waiter, NULL, 1, 0)) != NULL); + st_usleep(0); + errno = 0; + CHECK(st_cond_destroy(busy_cond) == -1 && errno == EBUSY); + CHECK(st_cond_signal(busy_cond) == 0); + CHECK(st_thread_join(t, &ret) == 0); + CHECK(ret == NULL && busy_done); + CHECK(st_cond_destroy(busy_cond) == 0); + + /* A mutex that is held, then held with a waiter, then held by the waiter. */ + CHECK((busy_lock = st_mutex_new()) != NULL); + CHECK(st_mutex_lock(busy_lock) == 0); + errno = 0; + CHECK(st_mutex_destroy(busy_lock) == -1 && errno == EBUSY); + + ret = (void *)-1; + busy_done = 0; + CHECK((t = st_thread_create(busy_lock_waiter, NULL, 1, 0)) != NULL); + st_usleep(0); + errno = 0; + CHECK(st_mutex_destroy(busy_lock) == -1 && errno == EBUSY); + + CHECK(st_mutex_unlock(busy_lock) == 0); + errno = 0; + CHECK(st_mutex_destroy(busy_lock) == -1 && errno == EBUSY); + + wait_for(&busy_done, 10); + CHECK(st_thread_join(t, &ret) == 0); + CHECK(ret == NULL && busy_done); + CHECK(st_mutex_destroy(busy_lock) == 0); + return 0; +} + +int main(int argc, char **argv) +{ + CHECK(tool_init() == 0); + + CHECK(queue() == 0); + CHECK(signal_one() == 0); + CHECK(broadcast() == 0); + CHECK(timedwait() == 0); + CHECK(mutex() == 0); + CHECK(ownership() == 0); + CHECK(trylock() == 0); + CHECK(interrupt() == 0); + CHECK(destroy() == 0); + + printf("sync OK\n"); + return 0; +} diff --git a/tools/tcp/.gitignore b/tools/tcp/.gitignore new file mode 100644 index 0000000..28a29e6 --- /dev/null +++ b/tools/tcp/.gitignore @@ -0,0 +1 @@ +tcp diff --git a/tools/stack/Makefile b/tools/tcp/Makefile similarity index 57% rename from tools/stack/Makefile rename to tools/tcp/Makefile index 6b736d9..517452b 100644 --- a/tools/stack/Makefile +++ b/tools/tcp/Makefile @@ -1,6 +1,7 @@ .PHONY: clean -CFLAGS=-g -O0 +LDLIBS=../../obj/libst.a +CFLAGS=-g -O0 -I../../obj -I.. -DDEBUG OS_NAME = $(shell uname -s) ST_TARGET = linux-debug @@ -11,9 +12,13 @@ CPU_ARCHS += $(shell g++ -dM -E - +#include +#include + +/* Long enough that a coroutine still blocked here is a failure, not a slow run. */ +#define BLOCK_US (5 * 1000 * 1000) + +/* A timeout that is expected to expire. */ +#define SHORT_US (5 * 1000) + +/* + * A small socket buffer. In the echo only the send buffers are small, so a + * message does not fit and writes are partial; a small receive buffer there + * would stall loopback TCP, whose segments are much bigger than the window. + */ +#define SMALL_BUF 4096 + +/* The echo workload: clients, messages per client, and the largest payload. */ +#define CLIENTS 8 +#define MESSAGES 24 +#define MAX_PAYLOAD 24000 +/* The most iovecs in one write, more than ST's 16 local ones. */ +#define MAX_IOV 40 +#define HEADER 8 + +/* Set SO_SNDBUF or SO_RCVBUF to size. */ +static int set_buf(int fd, int opt, int size) +{ + return setsockopt(fd, SOL_SOCKET, opt, &size, sizeof(size)); +} + +/* + * An echo socket: a small send buffer, and TCP_NODELAY as SRS sets with + * tcp_nodelay, so each echo goes out without waiting for a delayed ACK. + */ +static int tune(int fd) +{ + int one = 1; + if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one)) < 0) { + return -1; + } + return set_buf(fd, SO_SNDBUF, SMALL_BUF); +} + +/* The payload of message seq from client id: its size and its bytes. */ +static int payload_size(int id, int seq) +{ + return 1 + (id * 7919 + seq * 3571) % MAX_PAYLOAD; +} + +static void payload_fill(int id, int seq, char *buf, int size) +{ + for (int i = 0; i < size; i++) { + buf[i] = (char)(id * 31 + seq * 7 + i); + } +} + +static void header_fill(char *h, int size, int seq) +{ + uint32_t v = htonl((uint32_t)size); + memcpy(h, &v, 4); + v = htonl((uint32_t)seq); + memcpy(h + 4, &v, 4); +} + +static void header_parse(const char *h, int *size, int *seq) +{ + uint32_t v; + memcpy(&v, h, 4); + *size = (int)ntohl(v); + memcpy(&v, h + 4, 4); + *seq = (int)ntohl(v); +} + +/* Split buf into n iovecs after iov[0], as even as the size allows. */ +static int split(struct iovec *iov, char *buf, int size, int n) +{ + if (n > size) { + n = size; + } + int off = 0; + for (int i = 0; i < n; i++) { + int len = (i == n - 1) ? size - off : size / n; + iov[i].iov_base = buf + off; + iov[i].iov_len = len; + off += len; + } + return n; +} + +/* The server side of one connection, kept as the netfd specific data. */ +struct conn_ctx { + int msgs; + long bytes; +}; + +static struct { + st_netfd_t listener; + int accepted; + int accepts[2]; + st_thread_t handlers[CLIENTS]; + int family; + int port; + /* Added by the connection destructor, so they count only closed ones. */ + int freed; + int server_msgs; + long server_bytes; + int client_msgs; + long client_bytes; +} e; + +static void conn_ctx_free(void *arg) +{ + struct conn_ctx *ctx = (struct conn_ctx *)arg; + e.freed++; + e.server_msgs += ctx->msgs; + e.server_bytes += ctx->bytes; + free(ctx); +} + +/* Read each framed message with st_read_fully and echo it with st_writev. */ +static void *handler(void *arg) +{ + st_netfd_t conn = (st_netfd_t)arg; + struct conn_ctx *ctx = (struct conn_ctx *)st_netfd_getspecific(conn); + char *buf = malloc(MAX_PAYLOAD); + void *ret = NULL; + if (!buf) { + return (void *)1; + } + + for (;;) { + char h[HEADER]; + ssize_t n = st_read_fully(conn, h, HEADER, BLOCK_US); + if (n == 0) { + break; /* The client closed. */ + } + int size, seq; + if (n != HEADER) { + ret = (void *)2; + break; + } + header_parse(h, &size, &seq); + if (size <= 0 || size > MAX_PAYLOAD || st_read_fully(conn, buf, size, BLOCK_US) != size) { + ret = (void *)3; + break; + } + + struct iovec iov[1 + MAX_IOV]; + iov[0].iov_base = h; + iov[0].iov_len = HEADER; + int cnt = 1 + split(iov + 1, buf, size, 1 + seq % MAX_IOV); + if (st_writev(conn, iov, cnt, BLOCK_US) != HEADER + size) { + ret = (void *)4; + break; + } + ctx->msgs++; + ctx->bytes += HEADER + size; + } + + free(buf); + if (st_netfd_getspecific(conn) != ctx) { + ret = (void *)5; + } + /* Closing runs the destructor, which adds this connection's counts. */ + if (st_netfd_close(conn) != 0) { + ret = (void *)6; + } + return ret; +} + +/* Accept until every client is in; the other acceptor is then interrupted. */ +static void *acceptor(void *arg) +{ + int id = (int)(long)arg; + while (e.accepted < CLIENTS) { + struct sockaddr_storage addr; + int addrlen = sizeof(addr); + st_netfd_t conn = st_accept(e.listener, (struct sockaddr *)&addr, &addrlen, BLOCK_US); + if (!conn) { + return (errno == EINTR && e.accepted == CLIENTS) ? NULL : (void *)1; + } + if (addr.ss_family != e.family) { + return (void *)2; + } + /* An accepted connection is tuned through its descriptor. */ + if (tune(st_netfd_fileno(conn)) != 0) { + return (void *)3; + } + + struct conn_ctx *ctx = calloc(1, sizeof(*ctx)); + if (!ctx) { + return (void *)4; + } + st_netfd_setspecific(conn, ctx, conn_ctx_free); + + st_thread_t t = st_thread_create(handler, conn, 1, 0); + if (!t) { + return (void *)5; + } + e.handlers[e.accepted++] = t; + e.accepts[id]++; + } + return NULL; +} + +/* Read n bytes into iov with st_readv, moving past what each call read. */ +static int readv_all(st_netfd_t fd, struct iovec *iov, int cnt) +{ + while (cnt > 0) { + ssize_t n = st_readv(fd, iov, cnt, BLOCK_US); + if (n <= 0) { + return -1; + } + while (cnt > 0 && (size_t)n >= iov->iov_len) { + n -= iov->iov_len; + iov++; + cnt--; + } + if (cnt > 0) { + iov->iov_base = (char *)iov->iov_base + n; + iov->iov_len -= n; + } + } + return 0; +} + +/* Send one message with a write call chosen by seq. */ +static int client_send(st_netfd_t fd, char *h, char *buf, int size, int seq) +{ + struct iovec iovs[1 + MAX_IOV]; + iovs[0].iov_base = h; + iovs[0].iov_len = HEADER; + int cnt = 1 + split(iovs + 1, buf, size, 1 + (seq * 5) % MAX_IOV); + + if (seq % 4 == 2) { + /* The header with st_write, the payload with st_write_resid. */ + if (st_write(fd, h, HEADER, BLOCK_US) != HEADER) { + return -1; + } + size_t resid = size; + if (st_write_resid(fd, buf, &resid, BLOCK_US) != 0 || resid != 0) { + return -1; + } + return 0; + } + if (seq % 4 == 3) { + struct iovec *iov = iovs; + int iov_size = cnt; + if (st_writev_resid(fd, &iov, &iov_size, BLOCK_US) != 0 || iov_size != 0 || iov != iovs + cnt) { + return -1; + } + return 0; + } + return st_writev(fd, iovs, cnt, BLOCK_US) == HEADER + size ? 0 : -1; +} + +/* Read the echo of one message with a read call chosen by seq. */ +static int client_recv(st_netfd_t fd, char *h, char *buf, int size, int seq) +{ + switch (seq % 4) { + case 0: + if (st_read_fully(fd, h, HEADER, BLOCK_US) != HEADER || st_read_fully(fd, buf, size, BLOCK_US) != size) { + return -1; + } + return 0; + case 1: { + if (st_read_fully(fd, h, HEADER, BLOCK_US) != HEADER) { + return -1; + } + for (int off = 0; off < size;) { + ssize_t n = st_read(fd, buf + off, size - off, BLOCK_US); + if (n <= 0) { + return -1; + } + off += n; + } + return 0; + } + case 2: { + size_t resid = HEADER; + if (st_read_resid(fd, h, &resid, BLOCK_US) != 0 || resid != 0) { + return -1; + } + resid = size; + if (st_read_resid(fd, buf, &resid, BLOCK_US) != 0 || resid != 0) { + return -1; + } + return 0; + } + default: { + /* The header and payload in one call each of st_readv_resid and st_readv. */ + struct iovec iovs[3]; + iovs[0].iov_base = h; + iovs[0].iov_len = HEADER; + int cnt = 1 + split(iovs + 1, buf, size, 2); + if (seq % 8 == 3) { + struct iovec *iov = iovs; + int iov_size = cnt; + if (st_readv_resid(fd, &iov, &iov_size, BLOCK_US) != 0 || iov_size != 0 || iov != iovs + cnt) { + return -1; + } + return 0; + } + return readv_all(fd, iovs, cnt); + } + } +} + +static void *client(void *arg) +{ + int id = (int)(long)arg; + char *sent = malloc(MAX_PAYLOAD); + char *got = malloc(MAX_PAYLOAD); + void *ret = NULL; + st_netfd_t fd = NULL; + if (!sent || !got) { + ret = (void *)1; + goto out; + } + if (!(fd = tool_connect(e.family, e.port, BLOCK_US))) { + ret = (void *)2; + goto out; + } + if (tune(st_netfd_fileno(fd)) != 0) { + ret = (void *)3; + goto out; + } + + for (int seq = 0; seq < MESSAGES; seq++) { + int size = payload_size(id, seq); + char h[HEADER], gh[HEADER]; + header_fill(h, size, seq); + payload_fill(id, seq, sent, size); + memset(got, 0, size); + + if (client_send(fd, h, sent, size, seq) != 0) { + ret = (void *)4; + break; + } + if (client_recv(fd, gh, got, size, seq) != 0) { + ret = (void *)5; + break; + } + if (memcmp(h, gh, HEADER) != 0 || memcmp(sent, got, size) != 0) { + ret = (void *)6; + break; + } + e.client_msgs++; + e.client_bytes += HEADER + size; + } + +out: + if (fd && st_netfd_close(fd) != 0) { + ret = (void *)7; + } + free(sent); + free(got); + return ret; +} + +static int echo(void) +{ + int family = e.family; + memset(&e, 0, sizeof(e)); + e.family = family; + CHECK((e.listener = tool_listen(family, 128, &e.port)) != NULL); + /* A no-op on Linux and macOS, so two acceptors still share the listener. */ + CHECK(st_netfd_serialize_accept(e.listener) == 0); + + st_thread_t acceptors[2], clients[CLIENTS]; + for (int i = 0; i < 2; i++) { + CHECK((acceptors[i] = st_thread_create(acceptor, (void *)(long)i, 1, 0)) != NULL); + } + for (int i = 0; i < CLIENTS; i++) { + CHECK((clients[i] = st_thread_create(client, (void *)(long)i, 1, 0)) != NULL); + } + + void *ret; + for (int i = 0; i < CLIENTS; i++) { + ret = (void *)-1; + CHECK(st_thread_join(clients[i], &ret) == 0); + CHECK(ret == NULL); + } + CHECK(e.accepted == CLIENTS); + CHECK(e.accepts[0] + e.accepts[1] == CLIENTS); + for (int i = 0; i < CLIENTS; i++) { + ret = (void *)-1; + CHECK(st_thread_join(e.handlers[i], &ret) == 0); + CHECK(ret == NULL); + } + + /* One acceptor still waits in st_accept, the other may have returned. */ + for (int i = 0; i < 2; i++) { + st_thread_interrupt(acceptors[i]); + ret = (void *)-1; + CHECK(st_thread_join(acceptors[i], &ret) == 0); + CHECK(ret == NULL); + } + CHECK(st_netfd_close(e.listener) == 0); + + /* Every message and byte on both sides, and every connection closed. */ + CHECK(e.client_msgs == CLIENTS * MESSAGES); + CHECK(e.freed == CLIENTS); + CHECK(e.server_msgs == CLIENTS * MESSAGES); + CHECK(e.server_bytes == e.client_bytes); + long bytes = 0; + for (int id = 0; id < CLIENTS; id++) { + for (int seq = 0; seq < MESSAGES; seq++) { + bytes += HEADER + payload_size(id, seq); + } + } + CHECK(e.client_bytes == bytes); + return 0; +} + +/* + * A connected pair on loopback: a client with a small send buffer, and the + * server side accepted with the small receive buffer of the listener. + */ +static int pair(int family, st_netfd_t *c, st_netfd_t *s) +{ + int port; + st_netfd_t l; + CHECK((l = tool_listen(family, 8, &port)) != NULL); + CHECK(set_buf(st_netfd_fileno(l), SO_RCVBUF, SMALL_BUF) == 0); + CHECK((*c = tool_connect(family, port, BLOCK_US)) != NULL); + CHECK(set_buf(st_netfd_fileno(*c), SO_SNDBUF, SMALL_BUF) == 0); + CHECK((*s = st_accept(l, NULL, NULL, BLOCK_US)) != NULL); + CHECK(st_netfd_close(l) == 0); + return 0; +} + +/* Read exactly n bytes, and then nothing more is there. */ +static int drain(st_netfd_t fd, size_t n) +{ + char *buf = malloc(n + 1); + CHECK(buf != NULL); + ssize_t got = st_read_fully(fd, buf, n, BLOCK_US); + char extra; + errno = 0; + ssize_t more = st_read(fd, &extra, 1, ST_UTIME_NO_WAIT); + int err = errno; + free(buf); + CHECK(got == (ssize_t)n); + CHECK(more == -1 && err == ETIME); + return 0; +} + +/* The residual counts: short reads at EOF, and partial writes on a timeout. */ +static int resid(int family) +{ + st_netfd_t c, s; + char buf[100]; + + /* At EOF, st_read_fully is short and st_read_resid keeps what is left. */ + CHECK(pair(family, &c, &s) == 0); + CHECK(st_write(c, "0123456789", 10, BLOCK_US) == 10); + CHECK(st_netfd_close(c) == 0); + CHECK(st_read_fully(s, buf, 6, BLOCK_US) == 6); + CHECK(!memcmp(buf, "012345", 6)); + size_t left = sizeof(buf); + CHECK(st_read_resid(s, buf, &left, BLOCK_US) == 0); + CHECK(left == sizeof(buf) - 4 && !memcmp(buf, "6789", 4)); + CHECK(st_read_fully(s, buf, sizeof(buf), BLOCK_US) == 0); + CHECK(st_read(s, buf, sizeof(buf), BLOCK_US) == 0); + CHECK(st_netfd_close(s) == 0); + + /* At EOF, st_readv_resid moves past the filled iovecs into the partial one. */ + CHECK(pair(family, &c, &s) == 0); + CHECK(st_write(c, "0123456789", 10, BLOCK_US) == 10); + CHECK(st_netfd_close(c) == 0); + struct iovec iovs[3] = {{buf, 4}, {buf + 4, 4}, {buf + 8, 92}}; + struct iovec *iov = iovs; + int iov_size = 3; + CHECK(st_readv_resid(s, &iov, &iov_size, BLOCK_US) == 0); + CHECK(iov_size == 1 && iov == iovs + 2); + CHECK(iovs[0].iov_len == 0 && iovs[1].iov_len == 0); + CHECK(iovs[2].iov_len == 90 && iovs[2].iov_base == buf + 10); + CHECK(!memcmp(buf, "0123456789", 10)); + CHECK(st_netfd_close(s) == 0); + + /* The peer does not read, so a big write times out part way. */ + size_t big = 1024 * 1024; + char *data = calloc(1, big); + CHECK(data != NULL); + CHECK(pair(family, &c, &s) == 0); + left = big; + errno = 0; + int r = st_write_resid(c, data, &left, SHORT_US); + int err = errno; + CHECK(r == -1 && err == ETIME); + CHECK(left > 0 && left < big); + CHECK(drain(s, big - left) == 0); + + /* The same with st_writev_resid and more than 16 iovecs. */ + struct iovec wiovs[MAX_IOV]; + size_t chunk = SMALL_BUF; + for (int i = 0; i < MAX_IOV; i++) { + wiovs[i].iov_base = data + i * chunk; + wiovs[i].iov_len = chunk; + } + iov = wiovs; + iov_size = MAX_IOV; + errno = 0; + r = st_writev_resid(c, &iov, &iov_size, SHORT_US); + err = errno; + CHECK(r == -1 && err == ETIME); + CHECK(iov_size > 0 && iov_size <= MAX_IOV && iov == wiovs + MAX_IOV - iov_size); + size_t unsent = 0; + for (int i = 0; i < iov_size; i++) { + unsent += iov[i].iov_len; + } + CHECK(unsent > 0 && unsent < chunk * MAX_IOV && unsent <= chunk * iov_size); + CHECK(drain(s, chunk * MAX_IOV - unsent) == 0); + + /* st_write and st_writev fail as a whole on a timeout. */ + errno = 0; + CHECK(st_write(c, data, big, SHORT_US) == -1 && errno == ETIME); + free(data); + CHECK(st_netfd_close(c) == 0); + CHECK(st_netfd_close(s) == 0); + return 0; +} + +/* st_read and st_connect time out with ETIME. */ +static int timeouts(int family) +{ + st_netfd_t c, s; + char buf[16]; + CHECK(pair(family, &c, &s) == 0); + st_utime_t start = st_utime(); + errno = 0; + CHECK(st_read(s, buf, sizeof(buf), SHORT_US) == -1 && errno == ETIME); + CHECK(st_utime() - start >= SHORT_US); + CHECK(st_netfd_close(c) == 0); + CHECK(st_netfd_close(s) == 0); + + /* + * A listener with backlog 1 that never accepts: clients fill its queue, + * and the next connect gets no answer. How many fit depends on the OS. + */ + int port; + st_netfd_t l; + CHECK((l = tool_listen(family, 1, &port)) != NULL); + st_netfd_t clients[16]; + int n = 0, queued = 0, err = 0; + for (; n < 16; n++) { + struct sockaddr_storage addr; + socklen_t len = tool_loopback(family, port, &addr); + int fd = socket(family, SOCK_STREAM, 0); + CHECK(fd >= 0); + CHECK((clients[n] = st_netfd_open_socket(fd)) != NULL); + errno = 0; + if (st_connect(clients[n], (struct sockaddr *)&addr, len, 2 * SHORT_US) != 0) { + err = errno; + n++; + break; + } + queued++; + } + for (int i = 0; i < n; i++) { + CHECK(st_netfd_close(clients[i]) == 0); + } + CHECK(st_netfd_close(l) == 0); + CHECK(queued >= 1 && queued < 16); + CHECK(err == ETIME); + return 0; +} + +static int dtor_calls; +static void *dtor_last; + +static void dtor(void *v) +{ + dtor_calls++; + dtor_last = v; +} + +static int is_open(int fd) +{ + return fcntl(fd, F_GETFD) != -1; +} + +/* The netfd specific data, and st_netfd_free against st_netfd_close. */ +static int netfd(int family) +{ + st_netfd_t c, s; + int v1, v2, v3; + CHECK(pair(family, &c, &s) == 0); + + dtor_calls = 0; + CHECK(st_netfd_getspecific(s) == NULL); + st_netfd_setspecific(s, &v1, dtor); + CHECK(st_netfd_getspecific(s) == &v1); + /* The same value again is not freed. */ + st_netfd_setspecific(s, &v1, dtor); + CHECK(dtor_calls == 0); + /* A new value frees the old one, and NULL frees the last. */ + st_netfd_setspecific(s, &v2, dtor); + CHECK(dtor_calls == 1 && dtor_last == &v1); + CHECK(st_netfd_getspecific(s) == &v2); + st_netfd_setspecific(s, NULL, NULL); + CHECK(dtor_calls == 2 && dtor_last == &v2); + CHECK(st_netfd_getspecific(s) == NULL); + + /* st_netfd_free runs the destructor and keeps the OS descriptor open. */ + int osfd = st_netfd_fileno(s); + CHECK(osfd >= 0 && is_open(osfd)); + st_netfd_setspecific(s, &v3, dtor); + st_netfd_free(s); + CHECK(dtor_calls == 3 && dtor_last == &v3); + CHECK(is_open(osfd)); + + /* The descriptor still works when it is opened again. */ + CHECK((s = st_netfd_open_socket(osfd)) != NULL); + CHECK(st_netfd_fileno(s) == osfd); + CHECK(st_netfd_getspecific(s) == NULL); + char buf[4]; + CHECK(st_write(c, "ping", 4, BLOCK_US) == 4); + CHECK(st_read_fully(s, buf, 4, BLOCK_US) == 4 && !memcmp(buf, "ping", 4)); + + /* st_netfd_close runs the destructor and closes it, so the peer reads EOF. */ + st_netfd_setspecific(s, &v1, dtor); + CHECK(st_netfd_close(s) == 0); + CHECK(dtor_calls == 4 && dtor_last == &v1); + CHECK(!is_open(osfd) && errno == EBADF); + CHECK(st_read(c, buf, sizeof(buf), BLOCK_US) == 0); + CHECK(st_netfd_close(c) == 0); + return 0; +} + +static int run(int family) +{ + e.family = family; + CHECK(echo() == 0); + CHECK(resid(family) == 0); + CHECK(timeouts(family) == 0); + CHECK(netfd(family) == 0); + return 0; +} + +int main(int argc, char **argv) +{ + CHECK(tool_init() == 0); + + CHECK(run(AF_INET) == 0); + + /* IPv6, unless ::1 cannot be bound here. */ + int port; + st_netfd_t l = tool_listen(AF_INET6, 1, &port); + if (!l && (errno == EADDRNOTAVAIL || errno == EAFNOSUPPORT)) { + printf("SKIP ipv6\n"); + } else { + CHECK(l != NULL); + CHECK(st_netfd_close(l) == 0); + CHECK(run(AF_INET6) == 0); + } + + printf("tcp OK\n"); + return 0; +} diff --git a/tools/thread/.gitignore b/tools/thread/.gitignore new file mode 100644 index 0000000..8502cd4 --- /dev/null +++ b/tools/thread/.gitignore @@ -0,0 +1 @@ +thread diff --git a/tools/thread/Makefile b/tools/thread/Makefile new file mode 100644 index 0000000..999611c --- /dev/null +++ b/tools/thread/Makefile @@ -0,0 +1,24 @@ +.PHONY: clean + +LDLIBS=../../obj/libst.a +CFLAGS=-g -O0 -I../../obj -I.. -DDEBUG + +OS_NAME = $(shell uname -s) +ST_TARGET = linux-debug +ifeq ($(OS_NAME), Darwin) +ST_TARGET = darwin-debug +CPU_ARCHS = $(shell g++ -dM -E - = SWITCH_YIELDS + 1); + CHECK(outs[i] >= SWITCH_YIELDS + 1); + } + /* The primordial thread switches out to the join and back in after it. */ + CHECK(prim_in > 0 && prim_out > 0); + printf("ST: switches in=%d,%d out=%d,%d primordial in=%d out=%d\n", + ins[0], ins[1], outs[0], outs[1], prim_in, prim_out); + + /* Setting NULL returns the callback, and turns it off. */ + CHECK(st_set_switch_in_cb(NULL) == count_in); + CHECK(st_set_switch_out_cb(NULL) == count_out); + + CHECK(run_counted() == 0); + CHECK(ins[0] == 0 && ins[1] == 0 && outs[0] == 0 && outs[1] == 0); + CHECK(prim_in == 0 && prim_out == 0); + return 0; +} + +static void *do_block(void *arg) +{ + return (void *)(long)(st_cond_timedwait((st_cond_t)arg, BLOCK_US) == 0); +} + +/* The DEBUG functions run on live coroutines, blocked, ready and running. */ +static int debug(void) +{ + st_cond_t cond = st_cond_new(); + CHECK(cond); + + st_thread_t blocked = st_thread_create(do_block, cond, 1, 0); + CHECK(blocked); + CHECK(st_usleep(100) == 0); + st_thread_t ready = st_thread_create(do_return, (void *)1, 1, 0); + CHECK(ready); + + _st_show_thread_stack(blocked, "blocked"); + _st_show_thread_stack(ready, NULL); + _st_show_thread_stack(st_thread_self(), "self"); + _st_iterate_threads(); + + void *retval = NULL; + CHECK(st_cond_signal(cond) == 0); + CHECK(st_thread_join(blocked, &retval) == 0); + CHECK((long)retval == 1); + CHECK(st_thread_join(ready, &retval) == 0); + CHECK((long)retval == 2); + + CHECK(st_cond_destroy(cond) == 0); + return 0; +} + +int main(int argc, char **argv) +{ + CHECK(tool_init() == 0); + + CHECK(join_and_exit() == 0); + CHECK(detached() == 0); + CHECK(self() == 0); + CHECK(stacks() == 0); + CHECK(yield_order() == 0); + CHECK(interrupt() == 0); + CHECK(randomize() == 0); + CHECK(switch_callbacks() == 0); + CHECK(debug() == 0); + + printf("thread OK\n"); + return 0; +} diff --git a/tools/time/.gitignore b/tools/time/.gitignore new file mode 100644 index 0000000..6abb415 --- /dev/null +++ b/tools/time/.gitignore @@ -0,0 +1 @@ +time diff --git a/tools/time/Makefile b/tools/time/Makefile new file mode 100644 index 0000000..08fe7af --- /dev/null +++ b/tools/time/Makefile @@ -0,0 +1,24 @@ +.PHONY: clean + +LDLIBS=../../obj/libst.a +CFLAGS=-g -O0 -I../../obj -I.. -DDEBUG + +OS_NAME = $(shell uname -s) +ST_TARGET = linux-debug +ifeq ($(OS_NAME), Darwin) +ST_TARGET = darwin-debug +CPU_ARCHS = $(shell g++ -dM -E - +#include + +/* Long enough that a sleep still blocked here is a failure, not a slow run. */ +#define BLOCK_US (5 * 1000 * 1000) + +/* The real monotonic clock, independent of ST. */ +static st_utime_t real_us(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (st_utime_t)ts.tv_sec * 1000000LL + ts.tv_nsec / 1000; +} + +/* + * The custom clock: the real clock moved 1000 hours ahead, so its values + * cannot be mistaken for ST's own, plus a jump the child adds to skip time. + */ +#define CLOCK_BASE (1000LL * 3600 * 1000000) +static st_utime_t clock_jump; +static int clock_calls; + +static st_utime_t custom_utime(void) +{ + clock_calls++; + return real_us() + CLOCK_BASE + clock_jump; +} + +static int slept; + +static void *sleeper(void *arg) +{ + if (st_usleep(*(st_utime_t *)arg) == 0) { + slept = 1; + } + return NULL; +} + +/* + * In the child, before st_init: set the custom clock, then a coroutine sleeps + * 60 s on it. Jumping the clock 60 s ahead wakes it at once, in real time. + */ +static int custom_clock(void) +{ + CHECK(st_set_utime_function(custom_utime) == 0); + CHECK(tool_init() == 0); + CHECK(clock_calls > 0); + + /* After st_init, the clock cannot be changed. */ + errno = 0; + CHECK(st_set_utime_function(NULL) == -1 && errno == EINVAL); + + st_utime_t u0 = st_utime(); + CHECK(u0 >= CLOCK_BASE); + CHECK(st_utime_last_clock() >= CLOCK_BASE && st_utime_last_clock() <= u0); + + st_utime_t jump = 60LL * 1000000; + st_thread_t t = st_thread_create(sleeper, &jump, 1, 0); + CHECK(t != NULL); + CHECK(st_usleep(1000) == 0); + CHECK(!slept); + + st_utime_t r0 = real_us(); + clock_jump += jump; + /* The idle wait was sized on the old clock, so a short sleep reads the new one. */ + CHECK(st_usleep(1000) == 0); + CHECK(st_thread_join(t, NULL) == 0); + CHECK(slept); + CHECK(real_us() - r0 < BLOCK_US); + CHECK(st_utime() - u0 >= jump); + CHECK(st_utime_last_clock() - u0 >= jump); + return 0; +} + +/* st_set_utime_function works only before st_init, so it runs in a child forked before ST is set up. */ +static int fork_custom_clock(void) +{ + fflush(stdout); + pid_t pid = fork(); + CHECK(pid >= 0); + if (pid == 0) { + int r = custom_clock(); + fflush(stdout); + _exit(r); + } + + int status = 0; + CHECK(waitpid(pid, &status, 0) == pid); + CHECK(WIFEXITED(status) && WEXITSTATUS(status) == 0); + return 0; +} + +/* The clock never goes back, and moves across a sleep. */ +static int utime_increases(void) +{ + st_utime_t prev = st_utime(); + CHECK(prev > 0); + for (int i = 0; i < 1000; i++) { + st_utime_t now = st_utime(); + CHECK(now >= prev); + prev = now; + } + + CHECK(st_usleep(1000) == 0); + CHECK(st_utime() > prev); + return 0; +} + +/* Each sleep lasts at least the given time, by ST's clock and the real one; 0 returns at once. */ +static int usleep_at_least(void) +{ + CHECK(st_usleep(0) == 0); + CHECK(st_usleep(ST_UTIME_NO_WAIT) == 0); + + st_utime_t sleeps[] = {1000, 5000, 10000}; + for (int i = 0; i < (int)(sizeof(sleeps) / sizeof(sleeps[0])); i++) { + st_utime_t u0 = st_utime(); + st_utime_t r0 = real_us(); + CHECK(st_usleep(sleeps[i]) == 0); + CHECK(st_utime() - u0 >= sleeps[i]); + CHECK(real_us() - r0 >= sleeps[i]); + } + + /* st_sleep is st_usleep in whole seconds; 0 returns at once. */ + CHECK(st_sleep(0) == 0); + return 0; +} + +#define NN_SLEEPERS 3 +static int nn_woken; + +typedef struct { + st_utime_t us; + st_utime_t slept; +} sleeper_arg_t; + +static void *due_sleeper(void *arg) +{ + sleeper_arg_t *a = (sleeper_arg_t *)arg; + /* A sleep is due its time after the scheduler's last clock, which may lag st_utime. */ + st_utime_t u0 = st_utime_last_clock(); + if (st_usleep(a->us) == 0) { + a->slept = st_utime() - u0; + nn_woken++; + } + return NULL; +} + +/* + * Coroutines sleeping at once each wake, none before its own sleep ends. The + * order is not checked: sleeps that end in the same scheduler pass run in + * reverse, so a late wake on a slow host reorders them. + */ +static int wake_all(void) +{ + sleeper_arg_t args[NN_SLEEPERS] = {{15000, 0}, {5000, 0}, {10000, 0}}; + st_thread_t threads[NN_SLEEPERS]; + for (int i = 0; i < NN_SLEEPERS; i++) { + threads[i] = st_thread_create(due_sleeper, &args[i], 1, 0); + CHECK(threads[i] != NULL); + } + for (int i = 0; i < NN_SLEEPERS; i++) { + CHECK(st_thread_join(threads[i], NULL) == 0); + } + + CHECK(nn_woken == NN_SLEEPERS); + for (int i = 0; i < NN_SLEEPERS; i++) { + CHECK(args[i].slept >= args[i].us); + } + return 0; +} + +/* The scheduler's last clock moves after a sleep and a yield, and never passes st_utime. */ +static int last_clock(void) +{ + st_utime_t l0 = st_utime_last_clock(); + CHECK(l0 > 0 && l0 <= st_utime()); + + CHECK(st_usleep(2000) == 0); + st_utime_t l1 = st_utime_last_clock(); + CHECK(l1 - l0 >= 2000); + CHECK(l1 <= st_utime()); + + /* Without a sleep, the last clock stays until the next switch. */ + while (st_utime() - l1 < 1000) { + } + CHECK(st_utime_last_clock() == l1); + st_thread_yield(); + st_utime_t l2 = st_utime_last_clock(); + CHECK(l2 - l1 >= 1000); + CHECK(l2 <= st_utime()); + return 0; +} + +/* st_time follows time(NULL) with the cache off, and keeps the cached value with it on. */ +static int timecache(void) +{ + /* The cache is off by default. */ + time_t t0 = time(NULL); + time_t s = st_time(); + time_t t1 = time(NULL); + CHECK(t0 <= s && s <= t1); + + t0 = time(NULL); + CHECK(st_timecache_set(1) == 0); + t1 = time(NULL); + time_t c = st_time(); + CHECK(t0 <= c && c <= t1); + + /* The cache is refreshed about once a second, so it holds across a short sleep. */ + CHECK(st_usleep(5000) == 0); + CHECK(st_time() == c); + + CHECK(st_timecache_set(1) == 1); + CHECK(st_timecache_set(0) == 1); + CHECK(st_timecache_set(0) == 0); + + t0 = time(NULL); + s = st_time(); + t1 = time(NULL); + CHECK(t0 <= s && s <= t1); + return 0; +} + +int main(int argc, char **argv) +{ + CHECK(fork_custom_clock() == 0); + + CHECK(tool_init() == 0); + + /* The parent never set a clock, and after st_init it cannot. */ + errno = 0; + CHECK(st_set_utime_function(custom_utime) == -1 && errno == EINVAL); + CHECK(clock_calls == 0); + + CHECK(utime_increases() == 0); + CHECK(usleep_at_least() == 0); + CHECK(wake_all() == 0); + CHECK(last_clock() == 0); + CHECK(timecache() == 0); + + printf("time OK\n"); + return 0; +} diff --git a/tools/tool.h b/tools/tool.h new file mode 100644 index 0000000..3ec45f3 --- /dev/null +++ b/tools/tool.h @@ -0,0 +1,164 @@ +/* SPDX-License-Identifier: MIT */ +/* Copyright (c) 2013-2026 The SRS Authors */ + +/* + * Shared helpers for the integration tools. Each tool includes this file + * with -I.., and links obj/libst.a through st.h, as SRS does. + */ + +#ifndef ST_TOOL_H +#define ST_TOOL_H + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +/* Print the failing check and its line, then return 1 from the caller. */ +#define CHECK(cond) do { \ + if (!(cond)) { \ + printf("ST: FAILED %s at line %d, errno=%d\n", #cond, __LINE__, errno); \ + return 1; \ + } \ +} while (0) + +/* + * The event system the runner asks for in ST_TOOL_EVENTSYS: select (the + * default when unset) or alt. Returns -1 for any other value. + */ +static inline int tool_eventsys(void) +{ + const char *v = getenv("ST_TOOL_EVENTSYS"); + if (!v || !strcmp(v, "select")) { + return ST_EVENTSYS_SELECT; + } + if (!strcmp(v, "alt")) { + return ST_EVENTSYS_ALT; + } + return -1; +} + +/* The name st_get_eventsys_name() reports for an event system. */ +static inline const char *tool_eventsys_name(int eventsys) +{ + if (eventsys == ST_EVENTSYS_SELECT) { + return "select"; + } +#if defined(__APPLE__) + return "kqueue"; +#else + return "epoll"; +#endif +} + +/* + * Set the event system from ST_TOOL_EVENTSYS, then st_init, and check that + * ST reports the event system that was asked for. + */ +static inline int tool_init(void) +{ + int eventsys = tool_eventsys(); + CHECK(eventsys != -1); + CHECK(st_set_eventsys(eventsys) == 0); + CHECK(st_init() == 0); + CHECK(st_get_eventsys() == eventsys); + CHECK(!strcmp(st_get_eventsys_name(), tool_eventsys_name(eventsys))); + return 0; +} + +/* Fill addr with the loopback address of family, 127.0.0.1 or ::1, and port. */ +static inline socklen_t tool_loopback(int family, int port, struct sockaddr_storage *addr) +{ + memset(addr, 0, sizeof(*addr)); + if (family == AF_INET6) { + struct sockaddr_in6 *a = (struct sockaddr_in6 *)addr; + a->sin6_family = AF_INET6; + a->sin6_port = htons(port); + a->sin6_addr = in6addr_loopback; + return sizeof(*a); + } + + struct sockaddr_in *a = (struct sockaddr_in *)addr; + a->sin_family = AF_INET; + a->sin_port = htons(port); + a->sin_addr.s_addr = htonl(INADDR_LOOPBACK); + return sizeof(*a); +} + +/* + * A TCP listener on the loopback address of family, AF_INET or AF_INET6, at + * port 0. Sets *port to the port the OS chose. Returns NULL with errno set on + * failure; for ::1, EADDRNOTAVAIL or EAFNOSUPPORT means no IPv6 here. + */ +static inline st_netfd_t tool_listen(int family, int backlog, int *port) +{ + struct sockaddr_storage addr; + socklen_t len = tool_loopback(family, 0, &addr); + + int fd = socket(family, SOCK_STREAM, 0); + if (fd < 0) { + return NULL; + } + + if (bind(fd, (struct sockaddr *)&addr, len) < 0 || listen(fd, backlog) < 0 + || getsockname(fd, (struct sockaddr *)&addr, &len) < 0) { + int err = errno; + close(fd); + errno = err; + return NULL; + } + + st_netfd_t stfd = st_netfd_open_socket(fd); + if (!stfd) { + int err = errno; + close(fd); + errno = err; + return NULL; + } + + if (family == AF_INET6) { + *port = ntohs(((struct sockaddr_in6 *)&addr)->sin6_port); + } else { + *port = ntohs(((struct sockaddr_in *)&addr)->sin_port); + } + return stfd; +} + +/* + * A TCP client connected to the loopback address of family at port, with + * st_connect and timeout. Returns NULL with errno set on failure. + */ +static inline st_netfd_t tool_connect(int family, int port, st_utime_t timeout) +{ + struct sockaddr_storage addr; + socklen_t len = tool_loopback(family, port, &addr); + + int fd = socket(family, SOCK_STREAM, 0); + if (fd < 0) { + return NULL; + } + + st_netfd_t stfd = st_netfd_open_socket(fd); + if (!stfd) { + int err = errno; + close(fd); + errno = err; + return NULL; + } + + if (st_connect(stfd, (struct sockaddr *)&addr, len, timeout) < 0) { + int err = errno; + st_netfd_close(stfd); + errno = err; + return NULL; + } + return stfd; +} + +#endif /* ST_TOOL_H */ diff --git a/tools/udp/.gitignore b/tools/udp/.gitignore new file mode 100644 index 0000000..3b1b0ad --- /dev/null +++ b/tools/udp/.gitignore @@ -0,0 +1 @@ +udp diff --git a/tools/udp/Makefile b/tools/udp/Makefile new file mode 100644 index 0000000..d6dcc56 --- /dev/null +++ b/tools/udp/Makefile @@ -0,0 +1,24 @@ +.PHONY: clean + +LDLIBS=../../obj/libst.a +CFLAGS=-g -O0 -I../../obj -I.. -DDEBUG + +OS_NAME = $(shell uname -s) +ST_TARGET = linux-debug +ifeq ($(OS_NAME), Darwin) +ST_TARGET = darwin-debug +CPU_ARCHS = $(shell g++ -dM -E - + +/* Long enough that a coroutine still blocked here is a failure, not a slow run. */ +#define BLOCK_US (5 * 1000 * 1000) + +/* A timeout that is expected to expire. */ +#define SHORT_US (5 * 1000) + +/* The echo workload: clients, datagrams per client, and the largest payload. */ +#define CLIENTS 8 +#define MESSAGES 32 +#define MAX_PAYLOAD 4000 +#define HEADER 8 +/* The most iovecs for one datagram. */ +#define MAX_IOV 6 + +/* Which calls the echo uses. */ +#define MODE_TO 0 +#define MODE_MSG 1 + +/* A UDP socket bound to the loopback address of family at port 0. */ +static st_netfd_t udp_bind(int family, struct sockaddr_storage *addr, int *addrlen) +{ + socklen_t len = tool_loopback(family, 0, addr); + int fd = socket(family, SOCK_DGRAM, 0); + if (fd < 0) { + return NULL; + } + + if (bind(fd, (struct sockaddr *)addr, len) < 0 || getsockname(fd, (struct sockaddr *)addr, &len) < 0) { + int err = errno; + close(fd); + errno = err; + return NULL; + } + + st_netfd_t stfd = st_netfd_open_socket(fd); + if (!stfd) { + int err = errno; + close(fd); + errno = err; + return NULL; + } + *addrlen = (int)len; + return stfd; +} + +/* Whether two addresses are the same family, address and port. */ +static int same_addr(const struct sockaddr_storage *a, const struct sockaddr_storage *b) +{ + if (a->ss_family != b->ss_family) { + return 0; + } + if (a->ss_family == AF_INET6) { + const struct sockaddr_in6 *x = (const struct sockaddr_in6 *)a; + const struct sockaddr_in6 *y = (const struct sockaddr_in6 *)b; + return x->sin6_port == y->sin6_port && !memcmp(&x->sin6_addr, &y->sin6_addr, sizeof(x->sin6_addr)); + } + const struct sockaddr_in *x = (const struct sockaddr_in *)a; + const struct sockaddr_in *y = (const struct sockaddr_in *)b; + return x->sin_port == y->sin_port && x->sin_addr.s_addr == y->sin_addr.s_addr; +} + +/* The sockaddr size of family. */ +static int addr_size(int family) +{ + return family == AF_INET6 ? (int)sizeof(struct sockaddr_in6) : (int)sizeof(struct sockaddr_in); +} + +/* The payload of datagram seq from client id: its size and its bytes. */ +static int payload_size(int id, int seq) +{ + return 1 + (id * 7919 + seq * 3571) % MAX_PAYLOAD; +} + +static void payload_fill(int id, int seq, char *buf, int size) +{ + for (int i = 0; i < size; i++) { + buf[i] = (char)(id * 31 + seq * 7 + i); + } +} + +static int payload_check(int id, int seq, const char *buf, int size) +{ + for (int i = 0; i < size; i++) { + if (buf[i] != (char)(id * 31 + seq * 7 + i)) { + return -1; + } + } + return 0; +} + +static void header_fill(char *h, int id, int seq) +{ + uint32_t v = htonl((uint32_t)id); + memcpy(h, &v, 4); + v = htonl((uint32_t)seq); + memcpy(h + 4, &v, 4); +} + +static void header_parse(const char *h, int *id, int *seq) +{ + uint32_t v; + memcpy(&v, h, 4); + *id = (int)ntohl(v); + memcpy(&v, h + 4, 4); + *seq = (int)ntohl(v); +} + +/* Split buf into n iovecs, as even as the size allows. */ +static int split(struct iovec *iov, char *buf, int size, int n) +{ + if (n > size) { + n = size; + } + int off = 0; + for (int i = 0; i < n; i++) { + int len = (i == n - 1) ? size - off : size / n; + iov[i].iov_base = buf + off; + iov[i].iov_len = len; + off += len; + } + return n; +} + +/* A peer of the server, known only by its address, as SRS's UDP mux does. */ +struct peer { + struct sockaddr_storage addr; + int id; + int msgs; + long bytes; +}; + +static struct { + int family; + int mode; + st_netfd_t server; + struct sockaddr_storage server_addr; + int server_addrlen; + struct peer peers[CLIENTS]; + int npeers; + int server_msgs; + long server_bytes; + int client_msgs; + long client_bytes; +} e; + +/* Find the peer with addr, or add it as client id. NULL if a peer is wrong. */ +static struct peer *peer_of(const struct sockaddr_storage *addr, int id) +{ + for (int i = 0; i < e.npeers; i++) { + if (same_addr(&e.peers[i].addr, addr)) { + /* The address alone tells which client sent it. */ + return e.peers[i].id == id ? &e.peers[i] : NULL; + } + } + if (e.npeers == CLIENTS) { + return NULL; + } + struct peer *p = &e.peers[e.npeers++]; + memset(p, 0, sizeof(*p)); + p->addr = *addr; + p->id = id; + return p; +} + +/* Receive one datagram into buf, and its sender into from. */ +static int server_recv(char *buf, struct sockaddr_storage *from, int *fromlen) +{ + *fromlen = sizeof(*from); + if (e.mode == MODE_TO) { + return st_recvfrom(e.server, buf, HEADER + MAX_PAYLOAD, (struct sockaddr *)from, fromlen, BLOCK_US); + } + + /* The header and the payload in separate iovecs, the payload in three. */ + struct iovec iov[4]; + iov[0].iov_base = buf; + iov[0].iov_len = HEADER; + split(iov + 1, buf + HEADER, MAX_PAYLOAD, 3); + struct msghdr msg; + memset(&msg, 0, sizeof(msg)); + msg.msg_name = from; + msg.msg_namelen = sizeof(*from); + msg.msg_iov = iov; + msg.msg_iovlen = 4; + int n = st_recvmsg(e.server, &msg, 0, BLOCK_US); + *fromlen = (int)msg.msg_namelen; + if (n >= 0 && (msg.msg_flags & MSG_TRUNC)) { + errno = EMSGSIZE; + return -1; + } + return n; +} + +/* Echo n bytes of buf back to from. */ +static int server_send(char *buf, int n, int seq, const struct sockaddr_storage *from, int fromlen) +{ + if (e.mode == MODE_TO) { + return st_sendto(e.server, buf, n, (const struct sockaddr *)from, fromlen, BLOCK_US); + } + + struct iovec iov[MAX_IOV]; + struct msghdr msg; + memset(&msg, 0, sizeof(msg)); + msg.msg_name = (void *)from; + msg.msg_namelen = fromlen; + msg.msg_iov = iov; + msg.msg_iovlen = split(iov, buf, n, 1 + seq % MAX_IOV); + return st_sendmsg(e.server, &msg, 0, BLOCK_US); +} + +/* Echo every datagram to its sender, until it is interrupted. */ +static void *server(void *arg) +{ + char *buf = malloc(HEADER + MAX_PAYLOAD); + void *ret = NULL; + if (!buf) { + return (void *)1; + } + + for (;;) { + struct sockaddr_storage from; + int fromlen; + int n = server_recv(buf, &from, &fromlen); + if (n < 0) { + ret = (errno == EINTR && e.server_msgs == CLIENTS * MESSAGES) ? NULL : (void *)2; + break; + } + if (n < HEADER || fromlen != addr_size(e.family) || from.ss_family != e.family) { + ret = (void *)3; + break; + } + + int id, seq; + header_parse(buf, &id, &seq); + struct peer *p = peer_of(&from, id); + if (!p || n != HEADER + payload_size(id, seq) || payload_check(id, seq, buf + HEADER, n - HEADER) != 0) { + ret = (void *)4; + break; + } + if (server_send(buf, n, seq, &from, fromlen) != n) { + ret = (void *)5; + break; + } + p->msgs++; + p->bytes += n; + e.server_msgs++; + e.server_bytes += n; + } + + free(buf); + return ret; +} + +/* Send one datagram to the server, the header and payload in several iovecs. */ +static int client_send(st_netfd_t fd, char *h, char *buf, int size, int seq) +{ + if (e.mode == MODE_TO) { + char *d = malloc(HEADER + size); + if (!d) { + return -1; + } + memcpy(d, h, HEADER); + memcpy(d + HEADER, buf, size); + int n = st_sendto(fd, d, HEADER + size, (struct sockaddr *)&e.server_addr, e.server_addrlen, BLOCK_US); + free(d); + return n == HEADER + size ? 0 : -1; + } + + struct iovec iov[1 + MAX_IOV]; + iov[0].iov_base = h; + iov[0].iov_len = HEADER; + struct msghdr msg; + memset(&msg, 0, sizeof(msg)); + msg.msg_name = &e.server_addr; + msg.msg_namelen = e.server_addrlen; + msg.msg_iov = iov; + msg.msg_iovlen = 1 + split(iov + 1, buf, size, 1 + (seq * 5) % MAX_IOV); + return st_sendmsg(fd, &msg, 0, BLOCK_US) == HEADER + size ? 0 : -1; +} + +/* Receive the echo, and check it came from the server. */ +static int client_recv(st_netfd_t fd, char *h, char *buf, int size) +{ + struct sockaddr_storage from; + int fromlen = sizeof(from); + int n; + + if (e.mode == MODE_TO) { + char *d = malloc(HEADER + MAX_PAYLOAD); + if (!d) { + return -1; + } + n = st_recvfrom(fd, d, HEADER + MAX_PAYLOAD, (struct sockaddr *)&from, &fromlen, BLOCK_US); + if (n == HEADER + size) { + memcpy(h, d, HEADER); + memcpy(buf, d + HEADER, size); + } + free(d); + } else { + /* Exactly the expected size, in two iovecs. */ + struct iovec iov[3]; + iov[0].iov_base = h; + iov[0].iov_len = HEADER; + int cnt = 1 + split(iov + 1, buf, size, 2); + struct msghdr msg; + memset(&msg, 0, sizeof(msg)); + msg.msg_name = &from; + msg.msg_namelen = sizeof(from); + msg.msg_iov = iov; + msg.msg_iovlen = cnt; + n = st_recvmsg(fd, &msg, 0, BLOCK_US); + fromlen = (int)msg.msg_namelen; + if (msg.msg_flags & MSG_TRUNC) { + return -1; + } + } + + if (n != HEADER + size || fromlen != e.server_addrlen || !same_addr(&from, &e.server_addr)) { + return -1; + } + return 0; +} + +static void *client(void *arg) +{ + int id = (int)(long)arg; + char *sent = malloc(MAX_PAYLOAD); + char *got = malloc(MAX_PAYLOAD); + void *ret = NULL; + st_netfd_t fd = NULL; + struct sockaddr_storage addr; + int addrlen; + if (!sent || !got) { + ret = (void *)1; + goto out; + } + /* Each client has its own socket, so the server sees its own address. */ + if (!(fd = udp_bind(e.family, &addr, &addrlen))) { + ret = (void *)2; + goto out; + } + + for (int seq = 0; seq < MESSAGES; seq++) { + int size = payload_size(id, seq); + char h[HEADER], gh[HEADER]; + header_fill(h, id, seq); + payload_fill(id, seq, sent, size); + memset(got, 0, size); + + if (client_send(fd, h, sent, size, seq) != 0) { + ret = (void *)3; + break; + } + if (client_recv(fd, gh, got, size) != 0) { + ret = (void *)4; + break; + } + if (memcmp(h, gh, HEADER) != 0 || memcmp(sent, got, size) != 0) { + ret = (void *)5; + break; + } + e.client_msgs++; + e.client_bytes += HEADER + size; + /* Let the other clients interleave with this one. */ + if (seq % 3 == id % 3) { + st_thread_yield(); + } + } + +out: + if (fd && st_netfd_close(fd) != 0) { + ret = (void *)6; + } + free(sent); + free(got); + return ret; +} + +static int echo(int family, int mode) +{ + memset(&e, 0, sizeof(e)); + e.family = family; + e.mode = mode; + CHECK((e.server = udp_bind(family, &e.server_addr, &e.server_addrlen)) != NULL); + CHECK(e.server_addrlen == addr_size(family)); + + st_thread_t srv, clients[CLIENTS]; + CHECK((srv = st_thread_create(server, NULL, 1, 0)) != NULL); + for (int i = 0; i < CLIENTS; i++) { + CHECK((clients[i] = st_thread_create(client, (void *)(long)i, 1, 0)) != NULL); + } + + void *ret; + for (int i = 0; i < CLIENTS; i++) { + ret = (void *)-1; + CHECK(st_thread_join(clients[i], &ret) == 0); + CHECK(ret == NULL); + } + + /* The server waits in its receive call, and is stopped as SRS stops it. */ + st_thread_interrupt(srv); + ret = (void *)-1; + CHECK(st_thread_join(srv, &ret) == 0); + CHECK(ret == NULL); + CHECK(st_netfd_close(e.server) == 0); + + /* Every datagram and byte on both sides, and one peer per client. */ + CHECK(e.client_msgs == CLIENTS * MESSAGES); + CHECK(e.server_msgs == CLIENTS * MESSAGES); + CHECK(e.server_bytes == e.client_bytes); + CHECK(e.npeers == CLIENTS); + long bytes = 0; + for (int id = 0; id < CLIENTS; id++) { + long peer_bytes = 0; + for (int seq = 0; seq < MESSAGES; seq++) { + peer_bytes += HEADER + payload_size(id, seq); + } + int found = 0; + for (int i = 0; i < e.npeers; i++) { + if (e.peers[i].id == id) { + found++; + CHECK(e.peers[i].msgs == MESSAGES); + CHECK(e.peers[i].bytes == peer_bytes); + } + } + CHECK(found == 1); + bytes += peer_bytes; + } + CHECK(e.client_bytes == bytes); + return 0; +} + +/* Each receive gets one whole datagram, and a short buffer truncates it. */ +static int boundaries(int family) +{ + struct sockaddr_storage a_addr, b_addr, from; + int a_len, b_len, fromlen; + st_netfd_t a, b; + char buf[400], big[300]; + CHECK((a = udp_bind(family, &a_addr, &a_len)) != NULL); + CHECK((b = udp_bind(family, &b_addr, &b_len)) != NULL); + memset(big, 'x', sizeof(big)); + + /* Three datagrams queued before any is read keep their sizes. */ + CHECK(st_sendto(a, "hello", 5, (struct sockaddr *)&b_addr, b_len, BLOCK_US) == 5); + CHECK(st_sendto(a, "!", 1, (struct sockaddr *)&b_addr, b_len, BLOCK_US) == 1); + CHECK(st_sendto(a, big, sizeof(big), (struct sockaddr *)&b_addr, b_len, BLOCK_US) == (int)sizeof(big)); + fromlen = sizeof(from); + CHECK(st_recvfrom(b, buf, sizeof(buf), (struct sockaddr *)&from, &fromlen, BLOCK_US) == 5); + CHECK(!memcmp(buf, "hello", 5) && same_addr(&from, &a_addr)); + fromlen = sizeof(from); + CHECK(st_recvfrom(b, buf, sizeof(buf), (struct sockaddr *)&from, &fromlen, BLOCK_US) == 1); + CHECK(buf[0] == '!' && same_addr(&from, &a_addr)); + /* A NULL sender is allowed, as for a connected socket. */ + CHECK(st_recvfrom(b, buf, sizeof(buf), NULL, NULL, BLOCK_US) == (int)sizeof(big)); + CHECK(!memcmp(buf, big, sizeof(big))); + + /* A buffer shorter than the datagram gets its start; the rest is dropped. */ + CHECK(st_sendto(a, big, sizeof(big), (struct sockaddr *)&b_addr, b_len, BLOCK_US) == (int)sizeof(big)); + CHECK(st_sendto(a, "next", 4, (struct sockaddr *)&b_addr, b_len, BLOCK_US) == 4); + fromlen = sizeof(from); + CHECK(st_recvfrom(b, buf, 10, (struct sockaddr *)&from, &fromlen, BLOCK_US) == 10); + CHECK(!memcmp(buf, big, 10)); + fromlen = sizeof(from); + CHECK(st_recvfrom(b, buf, sizeof(buf), (struct sockaddr *)&from, &fromlen, BLOCK_US) == 4); + CHECK(!memcmp(buf, "next", 4)); + + /* st_recvmsg reports the truncation in msg_flags. */ + CHECK(st_sendto(a, big, sizeof(big), (struct sockaddr *)&b_addr, b_len, BLOCK_US) == (int)sizeof(big)); + struct iovec iov[2] = {{buf, 4}, {buf + 4, 6}}; + struct msghdr msg; + memset(&msg, 0, sizeof(msg)); + msg.msg_name = &from; + msg.msg_namelen = sizeof(from); + msg.msg_iov = iov; + msg.msg_iovlen = 2; + CHECK(st_recvmsg(b, &msg, 0, BLOCK_US) == 10); + CHECK((msg.msg_flags & MSG_TRUNC) != 0); + CHECK(!memcmp(buf, big, 10) && same_addr(&from, &a_addr)); + + /* Nothing is left. */ + errno = 0; + CHECK(st_recvfrom(b, buf, sizeof(buf), NULL, NULL, ST_UTIME_NO_WAIT) == -1 && errno == ETIME); + CHECK(st_netfd_close(a) == 0); + CHECK(st_netfd_close(b) == 0); + return 0; +} + +static void *recv_forever(void *arg) +{ + st_netfd_t fd = (st_netfd_t)arg; + char buf[16]; + if (st_recvfrom(fd, buf, sizeof(buf), NULL, NULL, BLOCK_US) != -1 || errno != EINTR) { + return (void *)1; + } + return NULL; +} + +/* With no sender, the receive calls time out with ETIME, or are interrupted. */ +static int timeouts(int family) +{ + struct sockaddr_storage addr, from; + int addrlen, fromlen = sizeof(from); + st_netfd_t fd; + char buf[16]; + CHECK((fd = udp_bind(family, &addr, &addrlen)) != NULL); + + st_utime_t start = st_utime(); + errno = 0; + CHECK(st_recvfrom(fd, buf, sizeof(buf), (struct sockaddr *)&from, &fromlen, SHORT_US) == -1 && errno == ETIME); + CHECK(st_utime() - start >= SHORT_US); + + struct iovec iov = {buf, sizeof(buf)}; + struct msghdr msg; + memset(&msg, 0, sizeof(msg)); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + start = st_utime(); + errno = 0; + CHECK(st_recvmsg(fd, &msg, 0, SHORT_US) == -1 && errno == ETIME); + CHECK(st_utime() - start >= SHORT_US); + + st_thread_t t; + void *ret = (void *)-1; + CHECK((t = st_thread_create(recv_forever, fd, 1, 0)) != NULL); + st_usleep(1000); + st_thread_interrupt(t); + CHECK(st_thread_join(t, &ret) == 0); + CHECK(ret == NULL); + + CHECK(st_netfd_close(fd) == 0); + return 0; +} + +static int run(int family) +{ + CHECK(echo(family, MODE_TO) == 0); + CHECK(echo(family, MODE_MSG) == 0); + CHECK(boundaries(family) == 0); + CHECK(timeouts(family) == 0); + return 0; +} + +int main(int argc, char **argv) +{ + CHECK(tool_init() == 0); + + CHECK(run(AF_INET) == 0); + + /* IPv6, unless ::1 cannot be bound here. */ + struct sockaddr_storage addr; + int addrlen; + st_netfd_t fd = udp_bind(AF_INET6, &addr, &addrlen); + if (!fd && (errno == EADDRNOTAVAIL || errno == EAFNOSUPPORT)) { + printf("SKIP ipv6\n"); + } else { + CHECK(fd != NULL); + CHECK(st_netfd_close(fd) == 0); + CHECK(run(AF_INET6) == 0); + } + + printf("udp OK\n"); + return 0; +} diff --git a/tools/unix/.gitignore b/tools/unix/.gitignore new file mode 100644 index 0000000..509143d --- /dev/null +++ b/tools/unix/.gitignore @@ -0,0 +1 @@ +unix diff --git a/tools/unix/Makefile b/tools/unix/Makefile new file mode 100644 index 0000000..d883d5c --- /dev/null +++ b/tools/unix/Makefile @@ -0,0 +1,24 @@ +.PHONY: clean + +LDLIBS=../../obj/libst.a +CFLAGS=-g -O0 -I../../obj -I.. -DDEBUG + +OS_NAME = $(shell uname -s) +ST_TARGET = linux-debug +ifeq ($(OS_NAME), Darwin) +ST_TARGET = darwin-debug +CPU_ARCHS = $(shell g++ -dM -E - +#include +#include + +/* Long enough that a coroutine still blocked here is a failure, not a slow run. */ +#define BLOCK_US (5 * 1000 * 1000) + +/* A timeout that is expected to expire. */ +#define SHORT_US (5 * 1000) + +/* The paths, removed by these literal paths before and after use. */ +#define STREAM_PATH "/tmp/st-tool-unix.sock" +#define DGRAM_PATH "/tmp/st-tool-unix-dgram.sock" +#define MISSING_PATH "/tmp/st-tool-unix-missing.sock" +#define DGRAM_CLIENTS 4 +static const char *dgram_client_paths[DGRAM_CLIENTS] = { + "/tmp/st-tool-unix-dgram-0.sock", + "/tmp/st-tool-unix-dgram-1.sock", + "/tmp/st-tool-unix-dgram-2.sock", + "/tmp/st-tool-unix-dgram-3.sock", +}; + +/* The stream echo: clients, messages per client, and the largest payload. */ +#define CLIENTS 8 +#define MESSAGES 24 +#define MAX_PAYLOAD 24000 +#define HEADER 8 +/* The most iovecs a client sends one message with. */ +#define MAX_IOV 8 + +/* The datagram echo: datagrams per client and the largest payload. macOS caps + * a Unix datagram at 2048 bytes by default (net.local.dgram.maxdgram). */ +#define DGRAMS 32 +#define MAX_DGRAM 2000 + +static void remove_paths(void) +{ + unlink(STREAM_PATH); + unlink(DGRAM_PATH); + unlink(MISSING_PATH); + unlink("/tmp/st-tool-unix-dgram-0.sock"); + unlink("/tmp/st-tool-unix-dgram-1.sock"); + unlink("/tmp/st-tool-unix-dgram-2.sock"); + unlink("/tmp/st-tool-unix-dgram-3.sock"); +} + +static socklen_t unix_addr(const char *path, struct sockaddr_un *addr) +{ + memset(addr, 0, sizeof(*addr)); + addr->sun_family = AF_UNIX; + strncpy(addr->sun_path, path, sizeof(addr->sun_path) - 1); + return (socklen_t)sizeof(*addr); +} + +/* A socket of type bound to path, or NULL with errno set. */ +static st_netfd_t unix_bind(int type, const char *path) +{ + struct sockaddr_un addr; + socklen_t len = unix_addr(path, &addr); + int fd = socket(AF_UNIX, type, 0); + if (fd < 0) { + return NULL; + } + + /* + * macOS gives a Unix datagram socket 4096 bytes to receive into + * (net.local.dgram.recvspace), and a send to a full one fails with + * ENOBUFS instead of waiting; make room for every client in flight. + */ + int rcvbuf = 64 * 1024; + if ((type == SOCK_DGRAM && setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf)) < 0) + || bind(fd, (struct sockaddr *)&addr, len) < 0 || (type == SOCK_STREAM && listen(fd, CLIENTS) < 0)) { + int err = errno; + close(fd); + errno = err; + return NULL; + } + + st_netfd_t stfd = st_netfd_open_socket(fd); + if (!stfd) { + int err = errno; + close(fd); + errno = err; + } + return stfd; +} + +/* A stream client connected to path with st_connect, or NULL with errno set. */ +static st_netfd_t unix_connect(const char *path) +{ + struct sockaddr_un addr; + socklen_t len = unix_addr(path, &addr); + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) { + return NULL; + } + + st_netfd_t stfd = st_netfd_open_socket(fd); + if (!stfd) { + int err = errno; + close(fd); + errno = err; + return NULL; + } + + if (st_connect(stfd, (struct sockaddr *)&addr, len, BLOCK_US) < 0) { + int err = errno; + st_netfd_close(stfd); + errno = err; + return NULL; + } + return stfd; +} + +/* The payload of message seq from client id: its size and its bytes. */ +static int payload_size(int id, int seq, int max) +{ + return 1 + (id * 7919 + seq * 3571) % max; +} + +static void payload_fill(int id, int seq, char *buf, int size) +{ + for (int i = 0; i < size; i++) { + buf[i] = (char)(id * 31 + seq * 7 + i); + } +} + +static int payload_check(int id, int seq, const char *buf, int size) +{ + for (int i = 0; i < size; i++) { + if (buf[i] != (char)(id * 31 + seq * 7 + i)) { + return -1; + } + } + return 0; +} + +/* The header: the client id, the sequence number, and the payload size. */ +static void header_fill(char *h, int id, int seq, int size) +{ + uint16_t v = htons((uint16_t)id); + memcpy(h, &v, 2); + v = htons((uint16_t)seq); + memcpy(h + 2, &v, 2); + uint32_t s = htonl((uint32_t)size); + memcpy(h + 4, &s, 4); +} + +static void header_parse(const char *h, int *id, int *seq, int *size) +{ + uint16_t v; + memcpy(&v, h, 2); + *id = ntohs(v); + memcpy(&v, h + 2, 2); + *seq = ntohs(v); + uint32_t s; + memcpy(&s, h + 4, 4); + *size = (int)ntohl(s); +} + +/* Split buf into n iovecs, as even as the size allows. */ +static int split(struct iovec *iov, char *buf, int size, int n) +{ + if (n > size) { + n = size; + } + int off = 0; + for (int i = 0; i < n; i++) { + int len = (i == n - 1) ? size - off : size / n; + iov[i].iov_base = buf + off; + iov[i].iov_len = len; + off += len; + } + return n; +} + +/* + * Read exactly n bytes with st_read, which returns what is there. Returns n, + * 0 at EOF before the first byte, or -1 (EOF part way sets EPIPE). + */ +static int read_all(st_netfd_t fd, char *buf, int n) +{ + int got = 0; + while (got < n) { + ssize_t r = st_read(fd, buf + got, n - got, BLOCK_US); + if (r < 0) { + return -1; + } + if (r == 0) { + if (got == 0) { + return 0; + } + errno = EPIPE; + return -1; + } + got += (int)r; + } + return n; +} + +static struct { + st_netfd_t listener; + int accepted; + int conns_done; + int server_msgs; + long server_bytes; + int client_msgs; + long client_bytes; +} s; + +/* Echo every message on one connection with st_write, until the client closes. */ +static void *conn(void *arg) +{ + st_netfd_t fd = (st_netfd_t)arg; + char *buf = malloc(HEADER + MAX_PAYLOAD); + void *ret = NULL; + if (!buf) { + ret = (void *)1; + goto out; + } + + for (;;) { + int r = read_all(fd, buf, HEADER); + if (r == 0) { + break; + } + if (r != HEADER) { + ret = (void *)2; + break; + } + + int id, seq, size; + header_parse(buf, &id, &seq, &size); + if (id >= CLIENTS || seq >= MESSAGES || size != payload_size(id, seq, MAX_PAYLOAD)) { + ret = (void *)3; + break; + } + if (read_all(fd, buf + HEADER, size) != size || payload_check(id, seq, buf + HEADER, size) != 0) { + ret = (void *)4; + break; + } + if (st_write(fd, buf, HEADER + size, BLOCK_US) != HEADER + size) { + ret = (void *)5; + break; + } + s.server_msgs++; + s.server_bytes += HEADER + size; + } + +out: + if (st_netfd_close(fd) != 0 && !ret) { + ret = (void *)6; + } + free(buf); + if (!ret) { + s.conns_done++; + } + return ret; +} + +/* Accept every client, each in its own coroutine, until interrupted. */ +static void *acceptor(void *arg) +{ + st_thread_t conns[CLIENTS]; + void *ret = NULL; + int n = 0; + + for (;;) { + struct sockaddr_un from; + int fromlen = sizeof(from); + st_netfd_t fd = st_accept(s.listener, (struct sockaddr *)&from, &fromlen, BLOCK_US); + if (!fd) { + if (errno != EINTR || n != CLIENTS) { + ret = (void *)1; + } + break; + } + if (n == CLIENTS || fromlen > (int)sizeof(from)) { + st_netfd_close(fd); + ret = (void *)2; + break; + } + if (!(conns[n] = st_thread_create(conn, fd, 1, 0))) { + st_netfd_close(fd); + ret = (void *)3; + break; + } + n++; + s.accepted++; + } + + for (int i = 0; i < n; i++) { + void *r = (void *)-1; + if (st_thread_join(conns[i], &r) != 0 || r != NULL) { + ret = (void *)4; + } + } + return ret; +} + +/* Send framed messages with st_writev, and read each echo back. */ +static void *client(void *arg) +{ + int id = (int)(long)arg; + char *sent = malloc(MAX_PAYLOAD); + char *got = malloc(HEADER + MAX_PAYLOAD); + void *ret = NULL; + st_netfd_t fd = NULL; + if (!sent || !got) { + ret = (void *)1; + goto out; + } + if (!(fd = unix_connect(STREAM_PATH))) { + ret = (void *)2; + goto out; + } + + for (int seq = 0; seq < MESSAGES; seq++) { + int size = payload_size(id, seq, MAX_PAYLOAD); + char h[HEADER]; + header_fill(h, id, seq, size); + payload_fill(id, seq, sent, size); + + /* The header in its own iovec, the payload in up to MAX_IOV - 1. */ + struct iovec iov[MAX_IOV]; + iov[0].iov_base = h; + iov[0].iov_len = HEADER; + int cnt = 1 + split(iov + 1, sent, size, 1 + seq % (MAX_IOV - 1)); + if (st_writev(fd, iov, cnt, BLOCK_US) != HEADER + size) { + ret = (void *)3; + break; + } + + /* Odd clients read the echo with st_read, even ones with st_read_fully. */ + memset(got, 0, HEADER + size); + int n = (id % 2) ? read_all(fd, got, HEADER + size) : (int)st_read_fully(fd, got, HEADER + size, BLOCK_US); + if (n != HEADER + size || memcmp(got, h, HEADER) != 0 || memcmp(got + HEADER, sent, size) != 0) { + ret = (void *)4; + break; + } + s.client_msgs++; + s.client_bytes += HEADER + size; + /* Let the other clients interleave with this one. */ + if (seq % 3 == id % 3) { + st_thread_yield(); + } + } + +out: + if (fd && st_netfd_close(fd) != 0) { + ret = (void *)5; + } + free(sent); + free(got); + return ret; +} + +/* A stream server on a bound path, and clients that connect to it. */ +static int stream_echo(void) +{ + memset(&s, 0, sizeof(s)); + unlink(STREAM_PATH); + CHECK((s.listener = unix_bind(SOCK_STREAM, STREAM_PATH)) != NULL); + CHECK(access(STREAM_PATH, F_OK) == 0); + + st_thread_t acc, clients[CLIENTS]; + CHECK((acc = st_thread_create(acceptor, NULL, 1, 0)) != NULL); + for (int i = 0; i < CLIENTS; i++) { + CHECK((clients[i] = st_thread_create(client, (void *)(long)i, 1, 0)) != NULL); + } + + void *ret; + for (int i = 0; i < CLIENTS; i++) { + ret = (void *)-1; + CHECK(st_thread_join(clients[i], &ret) == 0); + CHECK(ret == NULL); + } + + /* Every connection sees its client close; then the acceptor is stopped. */ + while (s.conns_done < CLIENTS) { + CHECK(st_usleep(1000) == 0); + CHECK(s.accepted == CLIENTS); + } + st_thread_interrupt(acc); + ret = (void *)-1; + CHECK(st_thread_join(acc, &ret) == 0); + CHECK(ret == NULL); + CHECK(st_netfd_close(s.listener) == 0); + CHECK(unlink(STREAM_PATH) == 0); + + long bytes = 0; + for (int id = 0; id < CLIENTS; id++) { + for (int seq = 0; seq < MESSAGES; seq++) { + bytes += HEADER + payload_size(id, seq, MAX_PAYLOAD); + } + } + CHECK(s.accepted == CLIENTS); + CHECK(s.client_msgs == CLIENTS * MESSAGES); + CHECK(s.server_msgs == CLIENTS * MESSAGES); + CHECK(s.client_bytes == bytes); + CHECK(s.server_bytes == bytes); + + /* A path with no socket, and a path whose listener is gone. */ + st_netfd_t fd; + unlink(MISSING_PATH); + errno = 0; + CHECK(unix_connect(MISSING_PATH) == NULL && errno == ENOENT); + CHECK((fd = unix_bind(SOCK_STREAM, MISSING_PATH)) != NULL); + CHECK(st_netfd_close(fd) == 0); + errno = 0; + CHECK(unix_connect(MISSING_PATH) == NULL && errno == ECONNREFUSED); + CHECK(unlink(MISSING_PATH) == 0); + return 0; +} + +static struct { + st_netfd_t server; + int server_msgs; + long server_bytes; + int client_msgs; + long client_bytes; + int seen[DGRAM_CLIENTS]; +} d; + +/* Whether a sender address is path. */ +static int from_path(const struct sockaddr_un *from, int fromlen, const char *path) +{ + if (fromlen <= (int)offsetof(struct sockaddr_un, sun_path) || from->sun_family != AF_UNIX) { + return 0; + } + return !strncmp(from->sun_path, path, sizeof(from->sun_path)); +} + +/* Echo every datagram to the path it came from, until interrupted. */ +static void *dgram_server(void *arg) +{ + char buf[MAX_DGRAM + 1]; + for (;;) { + struct sockaddr_un from; + int fromlen = sizeof(from); + int n = st_recvfrom(d.server, buf, sizeof(buf), (struct sockaddr *)&from, &fromlen, BLOCK_US); + if (n < 0) { + return (errno == EINTR && d.server_msgs == DGRAM_CLIENTS * DGRAMS) ? NULL : (void *)1; + } + + int id, seq, size; + if (n < HEADER) { + return (void *)2; + } + header_parse(buf, &id, &seq, &size); + /* The sender is known by its bound path. */ + if (id >= DGRAM_CLIENTS || !from_path(&from, fromlen, dgram_client_paths[id])) { + return (void *)3; + } + if (seq != d.seen[id] || n != HEADER + size || size != payload_size(id, seq, MAX_DGRAM - HEADER) + || payload_check(id, seq, buf + HEADER, size) != 0) { + return (void *)4; + } + if (st_sendto(d.server, buf, n, (struct sockaddr *)&from, fromlen, BLOCK_US) != n) { + return (void *)5; + } + d.seen[id]++; + d.server_msgs++; + d.server_bytes += n; + } +} + +/* Send datagrams from its own bound path, and check each echo. */ +static void *dgram_client(void *arg) +{ + int id = (int)(long)arg; + void *ret = NULL; + st_netfd_t fd; + struct sockaddr_un to; + socklen_t tolen = unix_addr(DGRAM_PATH, &to); + if (!(fd = unix_bind(SOCK_DGRAM, dgram_client_paths[id]))) { + return (void *)1; + } + + for (int seq = 0; seq < DGRAMS; seq++) { + char sent[MAX_DGRAM], got[MAX_DGRAM + 1]; + int size = payload_size(id, seq, MAX_DGRAM - HEADER); + header_fill(sent, id, seq, size); + payload_fill(id, seq, sent + HEADER, size); + if (st_sendto(fd, sent, HEADER + size, (struct sockaddr *)&to, tolen, BLOCK_US) != HEADER + size) { + ret = (void *)2; + break; + } + + struct sockaddr_un from; + int fromlen = sizeof(from); + int n = st_recvfrom(fd, got, sizeof(got), (struct sockaddr *)&from, &fromlen, BLOCK_US); + if (n != HEADER + size || memcmp(got, sent, n) != 0 || !from_path(&from, fromlen, DGRAM_PATH)) { + ret = (void *)3; + break; + } + d.client_msgs++; + d.client_bytes += n; + if (seq % 2 == id % 2) { + st_thread_yield(); + } + } + + if (st_netfd_close(fd) != 0 && !ret) { + ret = (void *)4; + } + unlink(dgram_client_paths[id]); + return ret; +} + +/* A datagram server on a bound path, and clients on their own paths. */ +static int dgram_echo(void) +{ + memset(&d, 0, sizeof(d)); + unlink(DGRAM_PATH); + for (int i = 0; i < DGRAM_CLIENTS; i++) { + unlink(dgram_client_paths[i]); + } + CHECK((d.server = unix_bind(SOCK_DGRAM, DGRAM_PATH)) != NULL); + + st_thread_t srv, clients[DGRAM_CLIENTS]; + CHECK((srv = st_thread_create(dgram_server, NULL, 1, 0)) != NULL); + for (int i = 0; i < DGRAM_CLIENTS; i++) { + CHECK((clients[i] = st_thread_create(dgram_client, (void *)(long)i, 1, 0)) != NULL); + } + + void *ret; + for (int i = 0; i < DGRAM_CLIENTS; i++) { + ret = (void *)-1; + CHECK(st_thread_join(clients[i], &ret) == 0); + CHECK(ret == NULL); + CHECK(access(dgram_client_paths[i], F_OK) != 0); + } + st_thread_interrupt(srv); + ret = (void *)-1; + CHECK(st_thread_join(srv, &ret) == 0); + CHECK(ret == NULL); + + long bytes = 0; + for (int id = 0; id < DGRAM_CLIENTS; id++) { + CHECK(d.seen[id] == DGRAMS); + for (int seq = 0; seq < DGRAMS; seq++) { + bytes += HEADER + payload_size(id, seq, MAX_DGRAM - HEADER); + } + } + CHECK(d.client_msgs == DGRAM_CLIENTS * DGRAMS); + CHECK(d.server_msgs == DGRAM_CLIENTS * DGRAMS); + CHECK(d.client_bytes == bytes); + CHECK(d.server_bytes == bytes); + + /* Queued datagrams keep their boundaries, and with none st_recvfrom times out. */ + st_netfd_t c; + char buf[64]; + struct sockaddr_un to, from; + socklen_t tolen = unix_addr(DGRAM_PATH, &to); + int fromlen = sizeof(from); + CHECK((c = unix_bind(SOCK_DGRAM, dgram_client_paths[0])) != NULL); + CHECK(st_sendto(c, "hello", 5, (struct sockaddr *)&to, tolen, BLOCK_US) == 5); + CHECK(st_sendto(c, "!", 1, (struct sockaddr *)&to, tolen, BLOCK_US) == 1); + CHECK(st_recvfrom(d.server, buf, sizeof(buf), (struct sockaddr *)&from, &fromlen, BLOCK_US) == 5); + CHECK(!memcmp(buf, "hello", 5) && from_path(&from, fromlen, dgram_client_paths[0])); + CHECK(st_recvfrom(d.server, buf, sizeof(buf), NULL, NULL, BLOCK_US) == 1 && buf[0] == '!'); + st_utime_t start = st_utime(); + errno = 0; + CHECK(st_recvfrom(d.server, buf, sizeof(buf), NULL, NULL, SHORT_US) == -1 && errno == ETIME); + CHECK(st_utime() - start >= SHORT_US); + + CHECK(st_netfd_close(c) == 0); + CHECK(st_netfd_close(d.server) == 0); + CHECK(unlink(dgram_client_paths[0]) == 0); + CHECK(unlink(DGRAM_PATH) == 0); + return 0; +} + +/* The socketpair bulk transfer: the size and the largest write. */ +#define PAIR_BYTES (512 * 1024) +#define PAIR_CHUNK 30000 + +/* Write PAIR_BYTES in chunks, so the writer waits when the socket is full. */ +static void *pair_writer(void *arg) +{ + st_netfd_t fd = (st_netfd_t)arg; + char *buf = malloc(PAIR_CHUNK); + if (!buf) { + return (void *)1; + } + void *ret = NULL; + for (int off = 0; off < PAIR_BYTES;) { + int n = 1 + (off * 13) % PAIR_CHUNK; + if (n > PAIR_BYTES - off) { + n = PAIR_BYTES - off; + } + for (int i = 0; i < n; i++) { + buf[i] = (char)((off + i) * 7); + } + if (st_write(fd, buf, n, BLOCK_US) != n) { + ret = (void *)2; + break; + } + off += n; + } + free(buf); + return ret; +} + +/* A stream socketpair through st_netfd_open_socket, both ways, then EOF. */ +static int pair_stream(void) +{ + int sv[2]; + st_netfd_t a, b; + CHECK(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0); + CHECK((a = st_netfd_open_socket(sv[0])) != NULL); + CHECK((b = st_netfd_open_socket(sv[1])) != NULL); + CHECK(st_netfd_fileno(a) == sv[0] && st_netfd_fileno(b) == sv[1]); + + /* Both ways, the reader reading with st_read while the writer fills it. */ + char *buf = malloc(PAIR_CHUNK); + CHECK(buf != NULL); + for (int dir = 0; dir < 2; dir++) { + st_netfd_t w = dir ? b : a, r = dir ? a : b; + st_thread_t t; + CHECK((t = st_thread_create(pair_writer, w, 1, 0)) != NULL); + int got = 0; + while (got < PAIR_BYTES) { + ssize_t n = st_read(r, buf, PAIR_CHUNK, BLOCK_US); + CHECK(n > 0 && got + n <= PAIR_BYTES); + for (int i = 0; i < n; i++) { + CHECK(buf[i] == (char)((got + i) * 7)); + } + got += (int)n; + } + void *ret = (void *)-1; + CHECK(st_thread_join(t, &ret) == 0); + CHECK(ret == NULL); + } + free(buf); + + /* With nothing to read, st_read times out. */ + char c; + st_utime_t start = st_utime(); + errno = 0; + CHECK(st_read(b, &c, 1, SHORT_US) == -1 && errno == ETIME); + CHECK(st_utime() - start >= SHORT_US); + + /* st_writev of several iovecs arrives as one stream; then close gives EOF. */ + char h[3] = {'a', 'b', 'c'}, p[5] = {'d', 'e', 'f', 'g', 'h'}, got[8]; + struct iovec iov[2] = {{h, sizeof(h)}, {p, sizeof(p)}}; + CHECK(st_writev(a, iov, 2, BLOCK_US) == 8); + CHECK(st_read_fully(b, got, sizeof(got), BLOCK_US) == 8); + CHECK(!memcmp(got, "abcdefgh", 8)); + CHECK(st_netfd_close(a) == 0); + CHECK(st_read(b, got, sizeof(got), BLOCK_US) == 0); + CHECK(st_netfd_close(b) == 0); + return 0; +} + +static void *pair_read_forever(void *arg) +{ + char buf[16]; + if (st_read((st_netfd_t)arg, buf, sizeof(buf), BLOCK_US) != -1 || errno != EINTR) { + return (void *)1; + } + return NULL; +} + +/* A datagram socketpair: st_write and st_read keep the boundaries. */ +static int pair_dgram(void) +{ + int sv[2]; + st_netfd_t a, b; + CHECK(socketpair(AF_UNIX, SOCK_DGRAM, 0, sv) == 0); + CHECK((a = st_netfd_open_socket(sv[0])) != NULL); + CHECK((b = st_netfd_open_socket(sv[1])) != NULL); + + char big[1000], buf[1200]; + memset(big, 'x', sizeof(big)); + CHECK(st_write(a, "hello", 5, BLOCK_US) == 5); + CHECK(st_write(a, big, sizeof(big), BLOCK_US) == (int)sizeof(big)); + CHECK(st_write(b, "back", 4, BLOCK_US) == 4); + CHECK(st_read(b, buf, sizeof(buf), BLOCK_US) == 5 && !memcmp(buf, "hello", 5)); + CHECK(st_read(b, buf, sizeof(buf), BLOCK_US) == (int)sizeof(big) && !memcmp(buf, big, sizeof(big))); + CHECK(st_read(a, buf, sizeof(buf), BLOCK_US) == 4 && !memcmp(buf, "back", 4)); + + /* A coroutine waiting in st_read is interrupted. */ + st_thread_t t; + void *ret = (void *)-1; + CHECK((t = st_thread_create(pair_read_forever, b, 1, 0)) != NULL); + CHECK(st_usleep(1000) == 0); + st_thread_interrupt(t); + CHECK(st_thread_join(t, &ret) == 0); + CHECK(ret == NULL); + + CHECK(st_netfd_close(a) == 0); + CHECK(st_netfd_close(b) == 0); + return 0; +} + +static int run(void) +{ + CHECK(stream_echo() == 0); + CHECK(dgram_echo() == 0); + CHECK(pair_stream() == 0); + CHECK(pair_dgram() == 0); + return 0; +} + +int main(int argc, char **argv) +{ + CHECK(tool_init() == 0); + + remove_paths(); + int r = run(); + remove_paths(); + CHECK(r == 0); + + printf("unix OK\n"); + return 0; +} diff --git a/tools/verify/Makefile b/tools/verify/Makefile index 3801f3b..e5acc4d 100644 --- a/tools/verify/Makefile +++ b/tools/verify/Makefile @@ -13,7 +13,7 @@ CFLAGS += -arch $(CPU_ARCHS) endif ./verify: verify.c $(LDLIBS) - $(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) -Wall -o $@ $^ $(LDLIBS) + $(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) -Wall -o $@ $< $(LDLIBS) clean: cd ../.. && make clean diff --git a/tools/verify/verify.c b/tools/verify/verify.c index 54053ac..5874096 100644 --- a/tools/verify/verify.c +++ b/tools/verify/verify.c @@ -4,61 +4,80 @@ #include #include -#include + +#define WAIT_US (100 * 1000) + +#define CHECK(cond) do { \ + if (!(cond)) { \ + printf("ST: FAILED %s at line %d\n", #cond, __LINE__); \ + return 1; \ + } \ +} while (0) st_mutex_t lock; st_cond_t cond; +st_utime_t signaled_at; void* start(void* arg) { printf("ST: thread run\n"); printf("ST: thread wait for a while\n"); - st_usleep(1.5 * 1000 * 1000); + st_usleep(WAIT_US); printf("ST: thread wait done\n"); + signaled_at = st_utime(); int r0 = st_cond_signal(cond); printf("ST: thread cond signal, r0=%d\n", r0); printf("ST: thread lock\n"); r0 = st_mutex_lock(lock); - assert(r0 == 0); + if (r0 != 0) return (void*)1; r0 = st_mutex_unlock(lock); printf("ST: thread unlock\n"); - return NULL; + return (void*)(long)r0; } int main(int argc, char** argv) { int r0 = st_init(); - assert(r0 == 0); + CHECK(r0 == 0); printf("ST: main init ok\n"); lock = st_mutex_new(); cond = st_cond_new(); + CHECK(lock && cond); + st_utime_t created_at = st_utime(); st_thread_t trd = st_thread_create(start, NULL, 1, 0); + CHECK(trd); printf("ST: main create ok\n"); printf("ST: main lock\n"); r0 = st_mutex_lock(lock); - assert(r0 == 0); + CHECK(r0 == 0); printf("ST: main cond waiting\n"); r0 = st_cond_wait(cond); printf("ST: main cond wait ok, r0=%d\n", r0); + CHECK(r0 == 0); + CHECK(signaled_at - created_at >= WAIT_US); r0 = st_mutex_unlock(lock); + CHECK(r0 == 0); printf("ST: main unlock\n"); - st_thread_join(trd, NULL); + void* res = (void*)-1; + r0 = st_thread_join(trd, &res); + CHECK(r0 == 0); + CHECK(res == NULL); printf("ST: main done\n"); - st_mutex_destroy(lock); - st_cond_destroy(cond); + CHECK(st_mutex_destroy(lock) == 0); + CHECK(st_cond_destroy(cond) == 0); + printf("ST: verify OK\n"); return 0; } -