Author: Andy Green Date: Fri Sep 11 06:12:09 2026 +0100 builder: windows: log a symbolised backtrace on abort() and the fatal signals A sai-builder service on Windows was dying with exit code 3 on service stop, which is abort(), with nothing to show for it: a service has no stderr for the CRT's report, its message boxes are invisible in session 0, and a debugger attached after startup gets no stop because the report-fault was already disabled. No assert text preceded the R6010 report in the debugger output, so it is a direct abort() from somewhere below the builder, and only a stack will say where. The unix build already has crash_handler() printing a backtrace on the fatal signals. Give Windows the same: abort() raises SIGABRT before exiting, so a handler on SIGABRT (and SIGSEGV, SIGILL, SIGFPE) walks the stack with CaptureStackBackTrace(), symbolises it through dbghelp with the .pdb files next to the binaries, and logs each frame with function, file and line via lwsl_err(), which for a service lands in %ProgramData%\sai\builder\sai-builder-service.log. If a debugger is attached it then breaks there, with the offending frames on the stack, before letting the default disposition exit as before. Co-Authored-By: Claude Fable 5.1 diff --git a/src/builder/CMakeLists.txt b/src/builder/CMakeLists.txt index 50f43b5..998d6d9 100644 --- a/src/builder/CMakeLists.txt +++ b/src/builder/CMakeLists.txt @@ -80,7 +80,7 @@ add_dependencies(${SUB} sai_git_hash) endif() if (MSVC OR WIN32) - target_link_libraries(${SUB} ws2_32.lib userenv.lib psapi.lib iphlpapi.lib advapi32.lib) + target_link_libraries(${SUB} ws2_32.lib userenv.lib psapi.lib iphlpapi.lib advapi32.lib dbghelp.lib) endif() if (MSVC OR WIN32) diff --git a/src/builder/b-sai.c b/src/builder/b-sai.c index 78a6d68..1ad5b3e 100644 --- a/src/builder/b-sai.c +++ b/src/builder/b-sai.c @@ -56,6 +56,7 @@ #include #include #include +#include #if !defined(PATH_MAX) @@ -539,6 +540,64 @@ crash_handler(int signum) #endif #if defined(WIN32) +/* + * The Debug CRT's abort() (an assert(), or a direct abort()) exits with + * code 3 and nothing else when there is no console: a service has no + * stderr, its message boxes are invisible in session 0, and if the + * debugger attached after startup the CRT report-fault is already off. + * abort() raises SIGABRT first, so log a symbolised backtrace from here, + * and hand a debugger the stop it was never given. + */ +static void +win_crash_handler(int signum) +{ + char symbuf[sizeof(SYMBOL_INFO) + 256]; + SYMBOL_INFO *sym = (SYMBOL_INFO *)symbuf; + HANDLE proc = GetCurrentProcess(); + IMAGEHLP_LINE64 line; + DWORD64 disp64 = 0; + void *frames[32]; + DWORD disp = 0; + USHORT n, i; + + lwsl_err("FATAL: caught signal %d, producing backtrace:\n", signum); + + SymSetOptions(SYMOPT_LOAD_LINES | SYMOPT_UNDNAME | + SYMOPT_DEFERRED_LOADS); + SymInitialize(proc, NULL, TRUE); + + n = CaptureStackBackTrace(0, (DWORD)LWS_ARRAY_SIZE(frames), frames, + NULL); + for (i = 0; i < n; i++) { + DWORD64 a = (DWORD64)(uintptr_t)frames[i]; + + memset(symbuf, 0, sizeof(symbuf)); + sym->SizeOfStruct = sizeof(SYMBOL_INFO); + sym->MaxNameLen = 255; + memset(&line, 0, sizeof(line)); + line.SizeOfStruct = sizeof(line); + + if (!SymFromAddr(proc, a, &disp64, sym)) { + lwsl_err(" #%u 0x%llx\n", i, (unsigned long long)a); + continue; + } + if (SymGetLineFromAddr64(proc, a, &disp, &line)) + lwsl_err(" #%u %s+0x%llx (%s:%lu)\n", i, sym->Name, + (unsigned long long)disp64, line.FileName, + line.LineNumber); + else + lwsl_err(" #%u %s+0x%llx\n", i, sym->Name, + (unsigned long long)disp64); + } + + if (IsDebuggerPresent()) + __debugbreak(); + + /* let the default disposition finish the job, exit code 3 for abort */ + signal(signum, SIG_DFL); + raise(signum); +} + static int log_fd = -1; static void @@ -632,6 +691,12 @@ saib_app_run(int argc, const char **argv) signal(SIGILL, crash_handler); signal(SIGFPE, crash_handler); #endif +#if defined(WIN32) + signal(SIGABRT, win_crash_handler); + signal(SIGSEGV, win_crash_handler); + signal(SIGILL, win_crash_handler); + signal(SIGFPE, win_crash_handler); +#endif lws_set_log_level(logs, NULL); }