From f842af752c9436c15fefe8e82d871b9bb504427c Mon Sep 17 00:00:00 2001 From: winlin Date: Sat, 3 Oct 2026 09:38:37 -0400 Subject: [PATCH 01/17] Claude: Make tools/ integration tests that fail on error, remove the jmpbuf, pcs and stack studies, and repoint obj on every build. Remove tools/jmpbuf, tools/pcs and tools/stack. They did not link ST and checked nothing: pcs and stack were bodies to step through in gdb, and jmpbuf printed a copy of the md.h jmpbuf table that could drift from it. The remaining tools link obj/libst.a through st.h as SRS does, exit non-zero on failure, and each finishes in under half a second: - tools/verify: checks every return code of init, create, mutex, condition variable, join and destroy instead of assert, that the signal comes after the full 100 ms sleep, and that the coroutine returns NULL. It used to sleep 1.5 s. - tools/backtrace: main joins the coroutine and returns the result, instead of leaving through st_thread_exit. bar() checks that backtrace() returns bar, foo and start in order, so it unwinds from a coroutine stack. The __builtin_return_address fallback only requires foo, because GCC on aarch64 returns NULL for any level above 0. - tools/porting: fails when the OS and CPU pair is not one md.h supports, or when longjmp returns to the wrong place. - tools/helloworld: takes an optional count, 10 by default, sleeps 10 ms per line instead of 1 s, and fails on a bad count or a failed st_init or st_usleep. - The verify, backtrace and helloworld Makefiles link libst.a once instead of twice. Makefile: the obj link was only updated when libst.a was archived again. After a Linux build in Docker, darwin-debug-utest found the Darwin library up to date and left obj pointing at LINUX_*_DBG, so the utest and the tools failed to link on macOS. The phony obj-link target now points obj to the platform on every build. All pass on macOS arm64, 184 utests and the four tools, and on Linux aarch64 in ossrs/srs:ubuntu20, 186 utests and the four tools, including a macOS build after a Linux one. --------- Co-authored-by: Claude Fable 5 --- .gitignore | 1 + Makefile | 8 ++++-- tools/backtrace/Makefile | 2 +- tools/backtrace/backtrace.c | 49 +++++++++++++++++++++++++++++++---- tools/helloworld/Makefile | 2 +- tools/helloworld/helloworld.c | 21 ++++++++++++--- tools/jmpbuf/.gitignore | 3 --- tools/jmpbuf/Makefile | 25 ------------------ tools/jmpbuf/jmpbuf.c | 39 ---------------------------- tools/pcs/.gitignore | 2 -- tools/pcs/Makefile | 19 -------------- tools/pcs/pcs.c | 35 ------------------------- tools/porting/porting.c | 22 ++++++++++++++++ tools/stack/.gitignore | 2 -- tools/stack/Makefile | 19 -------------- tools/stack/stack.c | 17 ------------ tools/verify/Makefile | 2 +- tools/verify/verify.c | 39 +++++++++++++++++++++------- 18 files changed, 122 insertions(+), 185 deletions(-) delete mode 100644 tools/jmpbuf/.gitignore delete mode 100644 tools/jmpbuf/Makefile delete mode 100644 tools/jmpbuf/jmpbuf.c delete mode 100644 tools/pcs/.gitignore delete mode 100644 tools/pcs/Makefile delete mode 100644 tools/pcs/pcs.c delete mode 100644 tools/stack/.gitignore delete mode 100644 tools/stack/Makefile delete mode 100644 tools/stack/stack.c 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/Makefile b/Makefile index 392d972..8b3f3ef 100644 --- a/Makefile +++ b/Makefile @@ -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/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/Makefile b/tools/jmpbuf/Makefile deleted file mode 100644 index e399c3e..0000000 --- a/tools/jmpbuf/Makefile +++ /dev/null @@ -1,25 +0,0 @@ -.PHONY: default clean - -LDLIBS= -CFLAGS=-g -O0 - -OS_NAME = $(shell uname -s) -ST_TARGET = linux-debug -ifeq ($(OS_NAME), Darwin) -ST_TARGET = darwin-debug -CPU_ARCHS = $(shell g++ -dM -E - -#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/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/Makefile b/tools/pcs/Makefile deleted file mode 100644 index 83b04a8..0000000 --- a/tools/pcs/Makefile +++ /dev/null @@ -1,19 +0,0 @@ -.PHONY: clean - -CFLAGS=-g -O0 - -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 + +#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; } - From 6bb81ba8b48fbbf80fe634b4051f2a97cd4600d2 Mon Sep 17 00:00:00 2001 From: winlin Date: Sat, 3 Oct 2026 10:48:55 -0400 Subject: [PATCH 02/17] Claude: Add auto/tools.sh to build ST with given flags and run every tool under each event system. auto/tools.sh rebuilds the library with make -B -debug and EXTRA_CFLAGS from the environment, so a change of flags never links a stale library, and relinks each tool with LDFLAGS from the environment. It runs every folder in tools/, so a new tool is tested without editing a list, once with ST_TOOL_EVENTSYS=select and once with alt. It prints " OK" per run, or the tool's output and "FAILED ", and exits 1 at the first failure. It works with macOS /bin/bash 3.2. The four tools pass under both event systems in the ST default build and the SRS build (-DMALLOC_STACK) on macOS arm64, and on Linux aarch64 in ossrs/srs:ubuntu20, where the ASAN build also passes. A tool that exits 1 makes the runner exit 1. --------- Co-authored-by: Claude Fable 5 --- auto/tools.sh | 52 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100755 auto/tools.sh 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" From ba2d1ac21e15bba63c9f0cd944651b5f5a024c70 Mon Sep 17 00:00:00 2001 From: winlin Date: Sat, 3 Oct 2026 10:51:36 -0400 Subject: [PATCH 03/17] Claude: Run the tools in CI after the utests, in the ST default and SRS builds, and with ASAN on Linux x64. Every utest matrix job (Linux x64, Linux aarch64, macOS arm64) now runs ./auto/tools.sh after the utests, once in the ST default build and once in the SRS build (EXTRA_CFLAGS=-DMALLOC_STACK). The Linux x64 job also runs it in the ASAN build, with -DMALLOC_STACK -DMD_ASAN -fsanitize=address -fno-omit-frame-pointer and LDFLAGS=-fsanitize=address, as SRS --sanitizer=on builds. The steps pass on macOS arm64, and on Linux aarch64 in ossrs/srs:ubuntu20, where the ASAN step also passes. --------- Co-authored-by: Claude Fable 5 --- .github/workflows/test.yml | 9 +++++++++ 1 file changed, 9 insertions(+) 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 From 1a51fd5fd4bb808da6563da45ad7c03c213f25f7 Mon Sep 17 00:00:00 2001 From: winlin Date: Sat, 3 Oct 2026 10:55:34 -0400 Subject: [PATCH 04/17] Claude: Add the lifecycle tool and tools/tool.h, the helpers shared by the new tools. tools/tool.h holds the CHECK macro, tool_init() to set the event system from ST_TOOL_EVENTSYS (select or alt) then st_init and check st_get_eventsys and st_get_eventsys_name, and loopback TCP listener and client helpers on port 0. tools/lifecycle runs the ST lifecycle as SRS does. It checks the event system is unset before st_set_eventsys, that an invalid one fails with EINVAL, and that setting it again before or after st_init fails with EBUSY. It sets the primordial stack from the stack limit as srs_set_primordial_stack does, and checks st_getfdlimit and RLIMIT_NOFILE after st_init as _st_io_init sets them: capped at FD_SETSIZE under select, and rlim_max, or rlim_cur when rlim_max is infinite, under kqueue and epoll. It interrupts and joins coroutines blocked in st_accept, st_read, st_cond_timedwait and st_usleep, each failing with EINTR at once, then calls st_destroy and exits 0. It passes under both event systems in the ST default and SRS builds on macOS arm64, and on Linux aarch64 in ossrs/srs:ubuntu20, where the ASAN build also passes. Each run takes under 5 ms warm. --------- Co-authored-by: Claude Fable 5 --- tools/lifecycle/.gitignore | 1 + tools/lifecycle/Makefile | 24 +++++ tools/lifecycle/lifecycle.c | 187 ++++++++++++++++++++++++++++++++++++ tools/tool.h | 164 +++++++++++++++++++++++++++++++ 4 files changed, 376 insertions(+) create mode 100644 tools/lifecycle/.gitignore create mode 100644 tools/lifecycle/Makefile create mode 100644 tools/lifecycle/lifecycle.c create mode 100644 tools/tool.h 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/lifecycle/Makefile b/tools/lifecycle/Makefile new file mode 100644 index 0000000..93996be --- /dev/null +++ b/tools/lifecycle/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 "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 from the stack limit, as srs_set_primordial_stack does. */ +static int set_primordial_stack(void) +{ + void *top = NULL; + struct rlimit limit; + CHECK(getrlimit(RLIMIT_STACK, &limit) == 0); + st_set_primordial_stack(&top, (char *)&top - (unsigned long long)limit.rlim_cur); + 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/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 */ From c4a17e98fb3635a4a6bb4bce20324c6fad2537f9 Mon Sep 17 00:00:00 2001 From: winlin Date: Sat, 3 Oct 2026 10:59:52 -0400 Subject: [PATCH 05/17] Claude: Add the thread tool, for coroutines as SRS runs them, stacks, yield, interrupt, and the switch callbacks. tools/thread checks joinable coroutines that return a value or call st_thread_exit from a nested call, joined with and without a place for the value, 64 at once, and that a coroutine joining itself fails with EDEADLK. Detached coroutines run and exit by themselves, and joining one fails with EINVAL. st_thread_self is the primordial thread in main and the created handle in a coroutine. The default stack holds 16 KB of locals, and a custom stack that is not a page multiple holds 64 KB. Ready coroutines run in turn on st_thread_yield. It stops a worker sleeping in st_usleep as SrsSTCoroutine does, with st_thread_interrupt then st_thread_join: the sleep fails with EINTR at once. An interrupt before the coroutine runs fails only its first sleep, and interrupting a coroutine that already finished does nothing. Coroutines run with st_randomize_stacks on and off, which returns the previous setting. st_set_switch_in_cb and st_set_switch_out_cb count the switches of each coroutine, return the previous callback, and stop counting when set to NULL. _st_show_thread_stack and _st_iterate_threads run on blocked, ready and running coroutines. It passes under both event systems in the ST default and SRS builds on macOS arm64, and on Linux aarch64 in ossrs/srs:ubuntu20, where the ASAN build also passes. Each run takes under 15 ms warm. --------- Co-authored-by: Claude Fable 5 --- tools/thread/.gitignore | 1 + tools/thread/Makefile | 24 +++ tools/thread/thread.c | 431 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 456 insertions(+) create mode 100644 tools/thread/.gitignore create mode 100644 tools/thread/Makefile create mode 100644 tools/thread/thread.c 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; +} From 639956594f7652884b9aea05d1caccfbeec3cf39 Mon Sep 17 00:00:00 2001 From: winlin Date: Sat, 3 Oct 2026 11:03:26 -0400 Subject: [PATCH 06/17] Claude: Add the key tool, for coroutine specific data as SRS keeps its context id, the destructors, and the key limit. tools/key creates a key with a destructor, as SRS does for its context id, and one without. Before any key exists, and past the created ones, st_thread_getspecific returns NULL and st_thread_setspecific fails with EINVAL. Replacing a value frees the old one, setting the same value does not, setting NULL frees it, and a key without a destructor only drops the value. Eight coroutines each set their own heap value, yield between each other, and still read their own; each value is freed when its coroutine exits, and the primordial thread's value is not touched. st_thread_setspecific2 sets a value on a new coroutine before it runs, and replaces the value of a blocked coroutine, which frees the old one, as SrsFastCoroutine::set_cid does. Destructors run before st_thread_join returns, whether the coroutine returns, calls st_thread_exit from a nested call, or is interrupted in st_usleep, and also for a detached coroutine. A new coroutine starts with no values. Keys are created up to st_key_getlimit(), and the next one fails with EAGAIN; every key is distinct, in range, and holds its own value. It passes under both event systems in the ST default and SRS builds on macOS arm64, and on Linux aarch64 in ossrs/srs:ubuntu20, where the ASAN build also passes. Each run takes under 10 ms warm. --------- Co-authored-by: Claude Fable 5 --- tools/key/.gitignore | 1 + tools/key/Makefile | 24 +++ tools/key/key.c | 352 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 377 insertions(+) create mode 100644 tools/key/.gitignore create mode 100644 tools/key/Makefile create mode 100644 tools/key/key.c 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/key/Makefile b/tools/key/Makefile new file mode 100644 index 0000000..0ca41d0 --- /dev/null +++ b/tools/key/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 - 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; +} From b27784798067cc447ff913bdb550b60549c498eb Mon Sep 17 00:00:00 2001 From: winlin Date: Sat, 3 Oct 2026 11:06:54 -0400 Subject: [PATCH 07/17] Claude: Add the sync tool, for condition variables and mutexes as SRS uses them in its queues, fan-out and locks. tools/sync runs a task queue like SrsAsyncCallWorker: a producer appends 300 tasks and signals, and three consumers wait in st_cond_wait while the queue is empty; every task is taken once, then a broadcast stops them. Each st_cond_signal wakes exactly one waiter, and a signal or broadcast with no waiter is not kept. A fan-out like a live stream broadcasts 10 messages to 16 waiting consumers, and each sees every message once. st_cond_timedwait times out with ETIME after at least the timeout, also for a zero timeout, and returns 0 when signaled in time. A mutex guards a counter that 8 coroutines read, yield, and write back 50 times each: no update is lost and only one is ever inside. Unlocking a mutex that is free or owned by another coroutine fails with EPERM, and the owner locking it again fails with EDEADLK. st_mutex_trylock takes a free mutex and fails with EBUSY while another coroutine holds it. st_thread_interrupt wakes a coroutine in st_cond_wait or st_mutex_lock with EINTR, and the interrupted one does not get the mutex. Destroying a cond with a waiter, or a mutex that is held, held with a waiter, or handed to that waiter, fails with EBUSY; free ones are destroyed. It passes under both event systems in the ST default and SRS builds on macOS arm64, and on Linux aarch64 in ossrs/srs:ubuntu20, where the ASAN build also passes. Each run takes under 20 ms warm. --------- Co-authored-by: Claude Fable 5 --- tools/sync/.gitignore | 1 + tools/sync/Makefile | 24 ++ tools/sync/sync.c | 557 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 582 insertions(+) create mode 100644 tools/sync/.gitignore create mode 100644 tools/sync/Makefile create mode 100644 tools/sync/sync.c 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; +} From c7023d42e28dd33db416b8d7c49fe16fbe27507e Mon Sep 17 00:00:00 2001 From: winlin Date: Sat, 3 Oct 2026 11:10:34 -0400 Subject: [PATCH 08/17] Claude: Add the time tool, for the clock and sleeps as SRS uses them, the time cache, and a custom clock. tools/time checks that st_utime never goes back and moves across a sleep, and that st_usleep sleeps at least the given time by ST's clock and by the real monotonic clock, for 1, 5 and 10 ms; a zero sleep and st_sleep(0) return at once. Three coroutines started in another order wake in the order their sleeps end. st_utime_last_clock is set by st_init, moves after a sleep and after st_thread_yield, stays put without a switch, and never passes st_utime. st_time follows time(NULL) with the time cache off, keeps the cached value across a short sleep with it on, and st_timecache_set returns the previous setting. st_set_utime_function works only before st_init, so a child forked before ST is set up sets a custom clock, 1000 hours ahead of the real one, then calls st_init. A coroutine sleeps 60 s on that clock; jumping the clock 60 s ahead wakes it at once in real time. After st_init, setting the clock fails with EINVAL, in the child and in the parent, which waits for the child before setting up ST. It passes under both event systems in the ST default and SRS builds on macOS arm64, and on Linux aarch64 in ossrs/srs:ubuntu20, where the ASAN build also passes. Each run takes about 50 ms warm, most of it the sleeps. --------- Co-authored-by: Claude Fable 5 --- tools/time/.gitignore | 1 + tools/time/Makefile | 24 ++++ tools/time/time.c | 252 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 277 insertions(+) create mode 100644 tools/time/.gitignore create mode 100644 tools/time/Makefile create mode 100644 tools/time/time.c 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_ORDER 3 +static int woken[NN_ORDER]; +static int nn_woken; + +typedef struct { + int id; + st_utime_t us; +} order_arg_t; + +static void *order_sleeper(void *arg) +{ + order_arg_t *a = (order_arg_t *)arg; + if (st_usleep(a->us) == 0) { + woken[nn_woken++] = a->id; + } + return NULL; +} + +/* Coroutines started in one order wake in the order their sleeps end. */ +static int wake_order(void) +{ + order_arg_t args[NN_ORDER] = {{2, 15000}, {0, 5000}, {1, 10000}}; + st_thread_t threads[NN_ORDER]; + for (int i = 0; i < NN_ORDER; i++) { + threads[i] = st_thread_create(order_sleeper, &args[i], 1, 0); + CHECK(threads[i] != NULL); + } + for (int i = 0; i < NN_ORDER; i++) { + CHECK(st_thread_join(threads[i], NULL) == 0); + } + + CHECK(nn_woken == NN_ORDER); + for (int i = 0; i < NN_ORDER; i++) { + CHECK(woken[i] == i); + } + 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_order() == 0); + CHECK(last_clock() == 0); + CHECK(timecache() == 0); + + printf("time OK\n"); + return 0; +} From cebed9fb5f18ad28854cab00c290a49ebd5e33db Mon Sep 17 00:00:00 2001 From: winlin Date: Sat, 3 Oct 2026 11:19:05 -0400 Subject: [PATCH 09/17] Claude: Add the tcp tool, for a framed echo server and clients as SRS serves and connects, the residual counts, timeouts, and netfd data. tools/tcp runs an echo over loopback: two coroutines accept on one listener after st_netfd_serialize_accept, which returns 0, and each accepted connection gets its own coroutine and a context kept as netfd specific data. Eight clients send 24 framed messages each, a header and a payload of up to 24000 bytes, and the server reads each with st_read_fully and echoes it with st_writev, up to 40 iovecs, more than ST's 16 local ones. The clients write with st_writev, st_write, st_write_resid and st_writev_resid, and read back with st_read_fully, st_read, st_read_resid, st_readv_resid and st_readv. The send buffers are small, so writes are partial, and TCP_NODELAY is set as SRS sets with tcp_nodelay. Every byte is checked, the message and byte counts match on both sides, the context destructor runs for every connection when it closes, and every coroutine joins; the acceptor still waiting in st_accept is interrupted. At EOF, st_read_fully is short, st_read_resid and st_readv_resid keep what is left, and st_readv_resid moves past the filled iovecs. When the peer does not read, st_write_resid and st_writev_resid time out with ETIME part way, and the peer reads exactly the bytes they report as sent; st_write fails as a whole. st_read times out with ETIME, and st_connect does too on a backlog-1 listener that never accepts. st_netfd_setspecific frees a replaced value but not the same one; st_netfd_free runs the destructor and keeps the descriptor open, which works again through st_netfd_open_socket, and st_netfd_close closes it, so the peer reads EOF. All of it runs over IPv4 and IPv6, and prints SKIP ipv6 when ::1 cannot be bound. It passes under both event systems in the ST default and SRS builds on macOS arm64, and on Linux aarch64 in ossrs/srs:ubuntu20, where the ASAN build also passes. Each run takes about 100 ms warm. --------- Co-authored-by: Claude Fable 5 --- tools/tcp/.gitignore | 1 + tools/tcp/Makefile | 24 ++ tools/tcp/tcp.c | 678 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 703 insertions(+) create mode 100644 tools/tcp/.gitignore create mode 100644 tools/tcp/Makefile create mode 100644 tools/tcp/tcp.c 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/tcp/Makefile b/tools/tcp/Makefile new file mode 100644 index 0000000..517452b --- /dev/null +++ b/tools/tcp/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 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; +} From 33a410b5599aea3d885ca6e3208f2171844c3750 Mon Sep 17 00:00:00 2001 From: winlin Date: Sat, 3 Oct 2026 11:24:26 -0400 Subject: [PATCH 10/17] Claude: Add the udp tool, for one server socket that echoes datagrams to many peers told apart by address, as SRS serves WebRTC. tools/udp runs one server coroutine on one loopback socket, as SRS's UDP mux does, and eight clients, each on its own socket, that send 32 datagrams each, a header and a payload of up to 4000 bytes, and wait for the echo. The server knows a peer only by its address: the first datagram from an address adds the peer, every later one from that address must carry the same client id, and at the end there is one peer per client with all its datagrams and bytes. The echo runs once with st_recvfrom and st_sendto, and once with st_recvmsg and st_sendmsg over several iovecs on both sides, as SRS's WebRTC path does. Every byte is checked on the server and the client, the sender of each echo is the server, the counts match on both sides, and every coroutine joins; the server, still waiting in its receive call, is stopped with st_thread_interrupt. Datagrams queued before any read keep their sizes, one per receive, and a NULL sender is allowed. A buffer shorter than the datagram gets its start and drops the rest, and st_recvmsg reports it with MSG_TRUNC. With no sender, st_recvfrom and st_recvmsg time out with ETIME after at least the timeout, and a coroutine waiting in st_recvfrom is interrupted with EINTR. All of it runs over IPv4 and IPv6, and prints SKIP ipv6 when ::1 cannot be bound. It passes under both event systems in the ST default and SRS builds on macOS arm64, and on Linux aarch64 in ossrs/srs:ubuntu20, where the ASAN build also passes. Each run takes about 50 ms warm. --------- Co-authored-by: Claude Fable 5 --- tools/udp/.gitignore | 1 + tools/udp/Makefile | 24 ++ tools/udp/udp.c | 578 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 603 insertions(+) create mode 100644 tools/udp/.gitignore create mode 100644 tools/udp/Makefile create mode 100644 tools/udp/udp.c 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; +} From 8d15103e280e209600a66bc1f1d52f69fa642723 Mon Sep 17 00:00:00 2001 From: winlin Date: Sat, 3 Oct 2026 11:29:53 -0400 Subject: [PATCH 11/17] Claude: Add the unix tool, for Unix domain stream and datagram servers on bound paths, and socketpairs through st_netfd_open_socket. tools/unix runs a stream echo on a listener bound to a fixed path: one coroutine accepts eight clients that connect with st_connect, and each connection gets its own coroutine. The clients send 24 framed messages each, a header and a payload of up to 24000 bytes, with st_writev over up to 8 iovecs; the server reads them with st_read and echoes them with st_write, and the clients read the echo back with st_read or st_read_fully. Every byte is checked, the counts match on both sides, every connection sees its client close, and every coroutine joins; the acceptor still waiting in st_accept is interrupted. A connect to a path with no socket fails with ENOENT, and to a path whose listener is closed with ECONNREFUSED. A datagram server on its own path echoes to four clients bound to their own paths, with st_recvfrom and st_sendto, 32 datagrams each of up to 2000 bytes, and knows each client by the path it sent from. Queued datagrams keep their boundaries, and with none st_recvfrom times out with ETIME. macOS gives a Unix datagram socket only 4096 bytes to receive into, and a send to a full one fails with ENOBUFS instead of waiting, so the tool enlarges SO_RCVBUF. A stream socketpair opened with st_netfd_open_socket carries 512 KB each way while the writer waits for the reader, then times out an st_read with ETIME, carries an st_writev, and reads EOF after close. A datagram socketpair keeps the boundaries of st_write and st_read, and a coroutine waiting in st_read is interrupted with EINTR. The paths are removed by their literal names before and after use. It passes under both event systems in the ST default and SRS builds on macOS arm64, and on Linux aarch64 in ossrs/srs:ubuntu20, where the ASAN build also passes. Each run takes about 20 ms warm on macOS and 30 ms on Linux. --------- Co-authored-by: Claude Fable 5 --- tools/unix/.gitignore | 1 + tools/unix/Makefile | 24 ++ tools/unix/unix.c | 731 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 756 insertions(+) create mode 100644 tools/unix/.gitignore create mode 100644 tools/unix/Makefile create mode 100644 tools/unix/unix.c 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; +} From dd53f40bfe9a4d4deebda99aed5dbb2bd9ec0ea3 Mon Sep 17 00:00:00 2001 From: winlin Date: Sat, 3 Oct 2026 11:34:31 -0400 Subject: [PATCH 12/17] Claude: Add the pipe tool, for the SRS signal pipe, a pipe whose writer fills it and waits, and a FIFO and a file opened with st_open. tools/pipe runs the SRS signal pipe as SrsSignalManager does: a sigaction handler writes the signal number to a pipe, and a coroutine reads it from the read end opened with st_netfd_open, which makes it non-blocking. The process sends SIGHUP then SIGTERM to itself, and the coroutine reads both in order, reloads once, and quits. Then a child process sends them while every coroutine is blocked and ST waits in its event system; it sends SIGTERM only after the coroutine acknowledges the reload, so the order is kept. A pipe between two coroutines carries 512 KB: the writer fills the pipe and waits until the reader reads it, every byte is checked, and the reader sees EOF when the writer closes. With nothing to read, st_read times out with ETIME; with no reader running, st_write_resid times out with ETIME part way, and the reader then gets exactly the bytes it reports as sent. With the read end closed and SIGPIPE ignored, st_write fails with EPIPE. st_open always opens non-blocking, so opening a FIFO's write end with no reader fails with ENXIO; with the reader opened first, a writer coroutine sends 512 KB through the FIFO to the reader, which reads it all and then EOF. A regular file opened with st_open takes an st_write and an st_writev, reads back with st_read_fully and then EOF, and keeps O_APPEND; a missing path fails with ENOENT. The paths are removed by their literal names before and after use. It passes under both event systems in the ST default and SRS builds on macOS arm64, and on Linux aarch64 in ossrs/srs:ubuntu20, where the ASAN build also passes. Each run takes about 25 ms warm on macOS and 35 ms on Linux. --------- Co-authored-by: Claude Fable 5 --- tools/pipe/.gitignore | 1 + tools/pipe/Makefile | 24 +++ tools/pipe/pipe.c | 394 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 419 insertions(+) create mode 100644 tools/pipe/.gitignore create mode 100644 tools/pipe/Makefile create mode 100644 tools/pipe/pipe.c 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; +} From 1be17c2534deb0096579022540798d72b2319c50 Mon Sep 17 00:00:00 2001 From: winlin Date: Sat, 3 Oct 2026 11:39:09 -0400 Subject: [PATCH 13/17] Claude: Add the poll tool, for st_poll on pipes and sockets at once, a worker that waits on all of them, and st_netfd_poll for reading and writing. tools/poll polls two pipes, a Unix socketpair and a TCP connection together. With data on two of them and room on two write ends, st_poll returns at once and counts only those four, with POLLIN where data waits, POLLOUT where there is room, and 0 on the quiet ones; a TCP socket asked for both gets both, and only POLLOUT once drained. A worker keeps one pollfd array for its whole loop and waits on every channel with no timeout: for 16 bytes sent one at a time across the channels, it wakes once per byte, with exactly one descriptor ready, and reads it from the channel it came on, then quits on a stop byte. With nothing ready, st_poll returns 0 after the timeout and leaves errno alone, as poll(2) does; st_netfd_poll turns that into -1 with ETIME. A coroutine waiting in either call is interrupted with EINTR at once, and one interrupted before it runs fails its first st_poll with EINTR without waiting, while the next one times out normally. st_netfd_poll for POLLIN wakes when a byte arrives later on each channel, and the read then gets it. For POLLOUT, sockets and a pipe with room return at once; a pipe filled until EAGAIN times out with ETIME, and is writable again once a reader coroutine drains it. At EOF the read end is readable and the read returns 0. Every descriptor closes at the end, so nothing is left registered. Loopback TCP may deliver a write a little after st_write returns, as on macOS, so the tool waits for the TCP byte before the poll that expects every ready descriptor, and the sender waits for the worker to read each byte before sending the next. It passes under both event systems in the ST default and SRS builds on macOS arm64, and on Linux aarch64 in ossrs/srs:ubuntu20, where the ASAN build also passes. Each run takes about 40 ms warm on macOS, and 35 ms with select and 55 ms with epoll on Linux. --------- Co-authored-by: Claude Fable 5 --- tools/poll/.gitignore | 1 + tools/poll/Makefile | 24 +++ tools/poll/poll.c | 448 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 473 insertions(+) create mode 100644 tools/poll/.gitignore create mode 100644 tools/poll/Makefile create mode 100644 tools/poll/poll.c 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; +} From ba4126ec1d589cce141c060d1ee954afe402f069 Mon Sep 17 00:00:00 2001 From: winlin Date: Sat, 3 Oct 2026 11:44:55 -0400 Subject: [PATCH 14/17] Claude: Add the stress tool, for many loopback connections open at once, coroutine create and exit cycles that reuse stacks, and contention on one mutex and one cond. tools/stress opens 200 loopback TCP connections by default, each with a client coroutine and a server coroutine. The clients connect in batches the accept queue holds, and exchange nothing until every connection is up, so all of them are open at once. Each client then sends 8 messages, a 4-byte size and a payload of up to 4096 bytes, with st_writev, and reads the echo back with st_read_fully; the server reads with st_read_fully and echoes with st_write until the client closes. Every byte is checked, the message and byte counts match on both sides, and every coroutine joins. Twenty cycles each create 200 coroutines, with the default stack and two custom sizes, so stacks are freed and reused across cycles. Each coroutine fills 4 KB of its stack with its own pattern, yields, and checks it; half exit with st_thread_exit and half return, and a quarter are detached. Every joinable one returns its own value, and every detached one finishes. 200 coroutines take one mutex 10 times each and yield inside it, and the counter shows no lost update and no two inside at once. Then they pass a token around through one cond with st_cond_broadcast, 3 times around, in order. The count is an argument, for larger manual runs. A run needs about two descriptors per connection, so the tool raises the soft RLIMIT_NOFILE before st_init when it can, and fails with a clear message when st_getfdlimit is too small, such as 1024 for select. It passes under both event systems in the ST default and SRS builds on macOS arm64, and on Linux aarch64 in ossrs/srs:ubuntu20, where the ASAN build also passes. Each run takes about 60 ms warm on macOS and 75 ms on Linux. --------- Co-authored-by: Claude Fable 5 --- tools/stress/.gitignore | 1 + tools/stress/Makefile | 24 +++ tools/stress/stress.c | 427 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 452 insertions(+) create mode 100644 tools/stress/.gitignore create mode 100644 tools/stress/Makefile create mode 100644 tools/stress/stress.c 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; +} From e2c5ecdf49ef42dbb576497caf379011d7f29fb3 Mon Sep 17 00:00:00 2001 From: winlin Date: Sat, 3 Oct 2026 12:33:24 -0400 Subject: [PATCH 15/17] Claude: Set the primordial stack in the lifecycle tool from the real main thread stack, so LeakSanitizer does not crash at exit in the ASAN build. The tool took the whole RLIMIT_STACK below a local as the main thread's stack, as SRS does. That is 8 MB on Linux, and most of it is unmapped. ST passes the range to ASAN on every switch back to the main thread, and with use-after-return fake stacks and leak detection on, LeakSanitizer reads it at exit and fails with "Tracer caught signal 11" every run, as in the linux-x64 CI job. SRS sets detect_leaks=0, so it never runs LeakSanitizer and never hits this. The tool now reads the stack bounds with pthread_getattr_np on Linux and pthread_get_stackaddr_np on macOS, as the PrimordialStackTest utest does, and keeps leak detection on for st_destroy. It passes all three tool runs on Ubuntu 24.04 aarch64 with gcc 13, including the ASAN build that failed before, and both runs on macOS arm64. Co-Authored-By: Claude Opus 5.5 (1M context) --- tools/lifecycle/Makefile | 2 +- tools/lifecycle/lifecycle.c | 30 +++++++++++++++++++++++++----- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/tools/lifecycle/Makefile b/tools/lifecycle/Makefile index 93996be..d015d40 100644 --- a/tools/lifecycle/Makefile +++ b/tools/lifecycle/Makefile @@ -1,6 +1,6 @@ .PHONY: clean -LDLIBS=../../obj/libst.a +LDLIBS=../../obj/libst.a -lpthread CFLAGS=-g -O0 -I../../obj -I.. -DDEBUG OS_NAME = $(shell uname -s) diff --git a/tools/lifecycle/lifecycle.c b/tools/lifecycle/lifecycle.c index 7fca1aa..7d8a4b2 100644 --- a/tools/lifecycle/lifecycle.c +++ b/tools/lifecycle/lifecycle.c @@ -7,6 +7,10 @@ * every coroutine blocked in I/O, a condition and a sleep, then st_destroy. */ +/* For pthread_getattr_np on Linux. */ +#define _GNU_SOURCE + +#include #include #include @@ -53,13 +57,29 @@ static void *do_sleep(void *arg) return result(st_usleep(BLOCK_US)); } -/* Set the primordial stack from the stack limit, as srs_set_primordial_stack does. */ +/* + * 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) { - void *top = NULL; - struct rlimit limit; - CHECK(getrlimit(RLIMIT_STACK, &limit) == 0); - st_set_primordial_stack(&top, (char *)&top - (unsigned long long)limit.rlim_cur); + 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; } From 26cb00fb30670a0a61c9748f9b6bd72f06b9178a Mon Sep 17 00:00:00 2001 From: winlin Date: Sat, 3 Oct 2026 12:33:24 -0400 Subject: [PATCH 16/17] Claude: Check in the time tool that sleeping coroutines all wake, none before its sleep ends, instead of their wake order. The check expected sleeps of 5, 10 and 15 ms to wake in that order, which holds only when each wake comes in its own scheduler pass. _st_vp_check_clock puts every expired sleeper at the head of the run queue, so sleeps that end in the same pass run in reverse. On a slow host the scheduler wakes late, and the order comes out reversed, as in the darwin-arm64 CI job; a 12 ms busy wait before the joins reproduces it every run. Each sleeper now measures from st_utime_last_clock, the clock its deadline is based on, and must sleep at least its time; all of them must wake. It passes with the 12 ms busy wait, and all tool runs pass on macOS arm64 and on Ubuntu 24.04 aarch64. Co-Authored-By: Claude Opus 5.5 (1M context) --- tools/time/time.c | 44 +++++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/tools/time/time.c b/tools/time/time.c index 27e18f6..5c1a1b1 100644 --- a/tools/time/time.c +++ b/tools/time/time.c @@ -3,7 +3,7 @@ /* * Time, as SRS reads it with st_utime and waits with st_usleep: the clock - * increases, sleeps last at least the given time and wake in due order, the + * increases, sleeps last at least the given time and all wake, the * scheduler's last clock moves after a switch, and st_time follows time(NULL) * with the time cache on and off. A child process, forked before ST is set * up, replaces the clock with st_set_utime_function and checks that sleeps @@ -140,40 +140,46 @@ static int usleep_at_least(void) return 0; } -#define NN_ORDER 3 -static int woken[NN_ORDER]; +#define NN_SLEEPERS 3 static int nn_woken; typedef struct { - int id; st_utime_t us; -} order_arg_t; + st_utime_t slept; +} sleeper_arg_t; -static void *order_sleeper(void *arg) +static void *due_sleeper(void *arg) { - order_arg_t *a = (order_arg_t *)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) { - woken[nn_woken++] = a->id; + a->slept = st_utime() - u0; + nn_woken++; } return NULL; } -/* Coroutines started in one order wake in the order their sleeps end. */ -static int wake_order(void) +/* + * 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) { - order_arg_t args[NN_ORDER] = {{2, 15000}, {0, 5000}, {1, 10000}}; - st_thread_t threads[NN_ORDER]; - for (int i = 0; i < NN_ORDER; i++) { - threads[i] = st_thread_create(order_sleeper, &args[i], 1, 0); + 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_ORDER; i++) { + for (int i = 0; i < NN_SLEEPERS; i++) { CHECK(st_thread_join(threads[i], NULL) == 0); } - CHECK(nn_woken == NN_ORDER); - for (int i = 0; i < NN_ORDER; i++) { - CHECK(woken[i] == i); + CHECK(nn_woken == NN_SLEEPERS); + for (int i = 0; i < NN_SLEEPERS; i++) { + CHECK(args[i].slept >= args[i].us); } return 0; } @@ -243,7 +249,7 @@ int main(int argc, char **argv) CHECK(utime_increases() == 0); CHECK(usleep_at_least() == 0); - CHECK(wake_order() == 0); + CHECK(wake_all() == 0); CHECK(last_clock() == 0); CHECK(timecache() == 0); From bbaf45db54c1e1302296bab7f8361b5b24a2d740 Mon Sep 17 00:00:00 2001 From: winlin Date: Sat, 3 Oct 2026 12:59:26 -0400 Subject: [PATCH 17/17] Update --- CHANGELOG.md | 1 + Makefile | 2 +- public.h | 4 ++-- st.spec | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) 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 8b3f3ef..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: 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