124 lines
4.3 KiB
Markdown
124 lines
4.3 KiB
Markdown
# 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
|
|
|
|
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.
|