fix: a WebSocket program hung instead of exiting (and why the UCRT throw crash is still open) - #638
Merged
Merged
Conversation
Handing a WebSocketServer to spawn() produced correct output and then left
the process alive forever, in the interpreter and in compiled binaries
alike. The service thread loops
while (!shutdown) lws_service(ctx, 50);
and that timeout argument has been ignored since libwebsockets 3.2 --
under the libuv event loop MSYS2 builds against, lws_service() blocks until
something happens, so setting shutdown was never noticed and the
pthread_join() in close() waited forever.
It reproduced with no client connected at all, which is what ruled out the
traffic and the shutdown ordering as causes:
server + close, no spawn ............... exits
server + spawn + client round trip ..... HANGS
server + spawn, no client at all ....... HANGS
server + spawn, no await ............... HANGS
Both close paths now call lws_cancel_service() before joining, the
documented way to interrupt a blocked lws_service() from another thread.
Verified rc=0 over five runs in each backend, against rc=124 (killed) every
time before. Same root cause as the connect timeout fixed in 2.10.0: that
ignored argument is worth suspecting whenever lws appears to ignore a
deadline.
The UCRT throw-from-a-native-callback crash is NOT fixed. A third mechanism
was tried and rejected: calling the CRT's non-ex _setjmp directly through an
__asm__ alias, bypassing mingw's redirect to __intrinsic_setjmpex. It
survives an isolated 400-frame test and then makes the real runtime worse --
array_sort goes from intermittent to 30/30, nested_exceptions to 21/30 --
so it is reverted. docs/advanced/windows.md now carries the UCRT backtrace
(RtlVirtualUnwind <- RtlUnwindEx <- ucrtbase!.intrinsic_setjmpex <-
hml_throw) and measurements for all four mechanisms tried. Three failures
in a row point away from a setjmp spelling: throw longjmps out of native
frames (sort comparators, map, filter) and this target's unwinder cannot
survive that. A pending-exception convention that lets native callback
sites return instead of being jumped out of would remove the unwinding
entirely, and would also fix the scratch-buffer leak the sort code already
documents -- a runtime design change rather than a platform shim.
CI keeps its bounded WebSocket invocations, now as a regression guard
rather than a workaround, with the comments updated to say so.
Parity 295/323 with 0 compiler-only failures and compiler suite 47/54, both
unchanged from before; gn.hml 52/52.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0135hioXD4pvKZUCWmv929sZ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two known Windows issues were on the table. One is fixed; the other is not, and the failed attempt is the more useful half of this PR.
Fixed: a WebSocket program hung instead of exiting
Handing a
WebSocketServertospawn()produced correct output and then left the process alive forever — interpreter and compiled binaries alike.The service thread loops:
That timeout argument has been ignored since libwebsockets 3.2. Under the libuv event loop MSYS2 builds against,
lws_service()blocks until something happens, so settingshutdownwas never noticed and thepthread_join()inclose()waited forever.Narrowed by elimination, which is what ruled out the traffic and the shutdown ordering:
close(), nospawnspawn+ client round tripspawn, no client at allspawn, noawaitBoth close paths now call
lws_cancel_service()before joining — the documented way to interrupt a blockedlws_service()from another thread. rc=0 over five runs in each backend, againstrc=124(killed) every time before.This is the same root cause as the connect timeout fixed in 2.10.0. That ignored argument is worth suspecting whenever libwebsockets appears to ignore a deadline — two separate bugs from it so far.
Not fixed: the UCRT throw crash, and why
I got a real backtrace this time:
longjmpunwinds and faults partway through. I tried a third mechanism — calling the CRT's non-ex_setjmpdirectly through an__asm__("_setjmp")alias, bypassing mingw's redirect to the intrinsic. It passes an isolated 400-frame test and then makes the real runtime worse:setjmpFrame = 0(shipped)__builtin_setjmp/longjmpex_setjmp(buf, NULL)array_sort30 in 30Reverted. Three failures in a row point away from a
setjmpspelling being the answer at all: Hemlock'sthrowlongjmps out of native frames — sort comparators,map,filter— and this target's unwinder cannot survive that.The fix that would work is a propagation convention where native callback sites check a pending-exception flag and return, rather than being jumped out of. That removes the unwinding entirely, and would also fix the scratch-buffer leak the sort code already documents. It is a runtime design change rather than a platform shim, so it is written up in
docs/advanced/windows.mdrather than attempted here.Also
CI keeps its bounded WebSocket invocations, now as a regression guard rather than a workaround; the comments say so.
Parity 295/323 with 0 compiler-only failures and compiler suite 47/54 — both unchanged from before this PR (the spread comes from the intermittent throw crash above). gn.hml 52/52.
🤖 Generated with Claude Code
https://claude.ai/code/session_0135hioXD4pvKZUCWmv929sZ