# Bug: `SpectrogramDashboardOp` destructor calls `std::terminate` ## Summary `SpectrogramDashboardOp` spawns an HTTP server thread during setup but its destructor does not `join()` or `detach()` it. Per the C++ standard, destroying a joinable `std::thread` calls `std::terminate()` — so **any** shutdown path kills the app: init failure, Ctrl-C, or normal exit at end of `main`. ## Evidence Built app (`new_dashboard`) crashes on shutdown with this backtrace: ``` #3 __GI_raise #4 __GI_abort #5 libstdc++ (std::terminate handler) #6 libstdc++ #7 std::terminate() #8 std::thread::~thread() #9 ria::ops::SpectrogramDashboardOp::~SpectrogramDashboardOp() #10 __gnu_cxx::new_allocator::destroy(...) ... #23 ria::Pipeline::~Pipeline() #24 main ``` The stack shows the failure is entirely inside the op's own destructor — not downstream of any flow / port-wiring issue. The op's startup message `HTTP server started on port 8080` prints just before the crash, confirming the server thread is running and joinable when destruction begins. ## Reproduction 1. Build any RIA app that includes `SpectrogramDashboardOp`. 2. Run the container; it crashes with `terminate called without an active exception` regardless of whether other operators succeed or fail. ## Root cause Standard C++ invariant: > If a `std::thread` object is destroyed while still `joinable()`, the destructor > calls `std::terminate()`. > — [cppreference.com/w/cpp/thread/thread/~thread](https://en.cppreference.com/w/cpp/thread/thread/~thread) The destructor needs to (a) signal the server to stop, (b) wait for the thread to exit, and (c) join it before the `std::thread` member is destroyed. ## Fix In `SpectrogramDashboardOp`: ```cpp SpectrogramDashboardOp::~SpectrogramDashboardOp() { // 1. Tell the HTTP server / websocket server to stop accepting // and to return from its serve loop. Exact call depends on the // HTTP library in use: // - cpp-httplib: server_.stop(); // - Boost.Beast: acceptor_.close(); io_context_.stop(); // - custom: shutdown_flag_.store(true); close(listen_fd_); if (server_) { server_->stop(); } // 2. Join the thread if it was ever started. if (http_thread_.joinable()) { http_thread_.join(); } } ``` If multiple threads are owned (e.g. separate WebSocket broadcaster, update-rate timer), join **each** of them. ## Related checks While fixing this op, audit any other operator in the same repo that owns a thread: ```bash grep -rn "std::thread " src/ ``` For each match, confirm the owning class's destructor does: ```cpp if (thread_.joinable()) thread_.join(); ``` plus whatever shutdown signal is needed to make the thread actually return. ## Acceptance - `SpectrogramDashboardOp` destructor joins all spawned threads. - A RIA app containing this op exits cleanly on `Ctrl-C` with no `terminate called without an active exception` message. - Forcing an init failure (e.g. a bad `websocket_port`) produces a readable exception message instead of `SIGABRT`. --- ## Prompt to paste into Claude Code (in the op's repo) > `SpectrogramDashboardOp` has a latent bug: its destructor lets a joinable > `std::thread` (the HTTP server thread that prints "HTTP server started on > port 8080") go out of scope, which per the C++ standard calls > `std::terminate()`. This makes any built RIA app containing this op crash on > every shutdown path — init failure, normal exit, and Ctrl-C — with the > unhelpful message `terminate called without an active exception`. Stack trace > at the point of abort goes through `std::thread::~thread()` → > `SpectrogramDashboardOp::~SpectrogramDashboardOp()`. > > Fix the destructor: (a) signal the HTTP server to stop (e.g. `server_->stop()` > for cpp-httplib, or close the listening socket + set a shutdown flag), then > (b) `if (http_thread_.joinable()) http_thread_.join();`. Apply the same pattern > to any other `std::thread` members the op owns (WebSocket broadcaster, rate > timer, etc.). Then grep for other `std::thread` members in this repo and audit > their owners' destructors for the same bug. > > Acceptance: the op's destructor joins every thread it starts; a test that > constructs and immediately destroys the op exits cleanly; Ctrl-C on a running > app produces no `terminate` message.