4.3 KiB
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<SpectrogramDashboardOp>::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
- Build any RIA app that includes
SpectrogramDashboardOp. - Run the container; it crashes with
terminate called without an active exceptionregardless of whether other operators succeed or fail.
Root cause
Standard C++ invariant:
If a
std::threadobject is destroyed while stilljoinable(), the destructor callsstd::terminate(). — 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:
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:
grep -rn "std::thread " src/
For each match, confirm the owning class's destructor does:
if (thread_.joinable()) thread_.join();
plus whatever shutdown signal is needed to make the thread actually return.
Acceptance
SpectrogramDashboardOpdestructor joins all spawned threads.- A RIA app containing this op exits cleanly on
Ctrl-Cwith noterminate called without an active exceptionmessage. - Forcing an init failure (e.g. a bad
websocket_port) produces a readable exception message instead ofSIGABRT.
Prompt to paste into Claude Code (in the op's repo)
SpectrogramDashboardOphas a latent bug: its destructor lets a joinablestd::thread(the HTTP server thread that prints "HTTP server started on port 8080") go out of scope, which per the C++ standard callsstd::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 messageterminate called without an active exception. Stack trace at the point of abort goes throughstd::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 otherstd::threadmembers the op owns (WebSocket broadcaster, rate timer, etc.). Then grep for otherstd::threadmembers 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
terminatemessage.