* fix(wallet): prevent exit-hang on Windows from detached Tor/I2P threads Embedded Tor and embedded I2P each ran on a background std::thread that was .detach()'d at startup. The teardown paths (CTorEmbedded::Stop, CI2PEmbedded::Stop) only flipped a running-flag — they did not signal the thread to exit, and on Windows there is no signal mechanism in tor_api 0.4.x. Result on Windows: when the user closed the wallet, Shutdown() completed its bookkeeping and main() returned 0, but the process could not exit because the detached thread was still in the Tor event loop / i2pd io_context. End Task (TerminateProcess) was the only escape; the GUI appeared completely stuck. Fixes: - tor_embedded.h/.cpp: keep the Tor thread handle; Stop() now raise(SIGTERM) on Linux, then joins the thread with a 5s timeout, then TerminateThread (Win) / pthread_cancel + pthread_join (Linux) as a last resort. - i2p_embedded.h/.cpp: same pattern — capture the bootstrap thread and join it in Stop() with a 5s timeout fallback. - init.cpp Shutdown(): spawn a 30s watchdog thread that calls ExitProcess(1) if the graceful teardown takes too long. Belt-and-suspenders against any future deadlock in the exit path. - trianglesgui.cpp closeEvent(): second close attempt while the first exit is still running immediately calls ExitProcess(2) / _exit(2). User escape hatch when the graceful exit hangs. All non-consensus (threading/process lifecycle only). Build via CI; not local. Notes: notes/wallet-close-hang-fix-2026-07-07.md * fix(i2p): drop leftover .detach() that broke build (lambda now joinable) * fix(i2p): clean up after .detach() removal (trailing comment, blank line) * fix(tor): MINGW std::thread is pthread-based, use pthread_cancel/join on MINGW MINGW std::thread::native_handle_type is unsigned long long (pthread_t emulation), not HANDLE. Mixing pthread handles with Win32 WaitForSingleObject/TerminateThread fails to compile on MINGW with 'invalid conversion' errors. Use the same pthread_cancel/pthread_join path on Linux and MINGW; keep TerminateThread only for MSVC builds where native_handle() returns a real Win32 HANDLE. --------- Co-authored-by: krystie <krystie>
4.0 KiB
Wallet close hang fix (2026-07-07)
Reported by: Sami
Branch: TBD (off ui/overview-color-rework or new fix/close-hang)
Severity: High — wallet process can't be closed by user on Windows
Consensus-affecting: No (threading/process lifecycle only)
Symptom
- User clicks X on Qt wallet
- Wallet appears to hang
- Task Manager → End Task does not close the process (on Windows)
- No new
debug.logoutput after the click
Root cause (Phase 1)
src/tor/tor_embedded.cpp:205-206:
std::thread torThread(TorThreadFunc, argv);
torThread.detach();
The Tor thread is detached at startup and never joined. CTorEmbedded::Stop() at line 266-278 only flips a running atomic — it has no real teardown on either platform:
- Linux:
#ifndef WIN32block is a no-op (comment-only TODO) - Windows: no block at all — function body ends after
running.store(false)
tor_run_main() blocks in the Tor event loop indefinitely. The OS process cannot exit while that thread is alive, regardless of main() returning 0. Shutdown() in init.cpp finishes its bookkeeping and sets fExit = true, main() returns, but the process keeps running because the detached Tor thread is still in the event loop.
ExitTimeout (init.cpp:143) is only useful for deadlock after Shutdown() returns — it doesn't help here.
Fix plan
1. src/tor/tor_embedded.cpp — actually stop the Tor thread
Two-part fix:
a) Store the std::thread handle (not detached):
std::thread torThread(TorThreadFunc, argv);
// do NOT detach
torThreadHandle = std::move(torThread);
b) In Stop(), on the main thread, send a Tor control command to ask the daemon to shut down. Tor's tor_api doesn't expose this in 0.4.x but the embedded Tor opens a control port by default OR we can use the simpler approach: send SIGTERM to ourselves on Linux, and on Windows post a custom event to the Tor thread.
For Windows specifically: the cleanest approach is to use Tor's tor_api_shutdown() if available, OR fall back to TerminateThread after a 5-second grace period. Since the wallet is going to exit anyway, TerminateThread is acceptable here as a last-resort — we mark the thread as unjoinable and let OS clean it up.
2. src/i2p/i2p_embedded.cpp — same fix for I2P
I2P has a cleaner API: i2p::api::StopI2P() and i2p::api::TerminateI2P() exist (line 643, 646). The background thread in the lambda at line 527 is also detached. Same fix pattern: capture the thread handle, join it (with a 5s timeout fallback) in Stop().
3. src/init.cpp Shutdown() — add an overall watchdog
Wrap the shutdown sequence in a timed watchdog. If Shutdown() doesn't return within 30 seconds, log where it got stuck and call ExitProcess(0) (Windows) / _exit(0) (Linux) to force-exit. This is the belt-and-suspenders that ensures the wallet ALWAYS closes, even if the I2P/Tor stop is partially broken in a future release.
4. Belt-and-suspenders: pre-Shutdown user signal handler
Add a WM_CLOSE handler that, on second close attempt (when one is already in progress), immediately force-exits. This is a UX improvement so users with a stuck wallet can force-close via X.
Files to modify
src/tor/tor_embedded.cpp(Stop() implementation)src/tor/tor_embedded.h(thread member + Stop() signature)src/i2p/i2p_embedded.cpp(Stop() implementation, thread capture)src/i2p/i2p_embedded.h(thread member)src/init.cpp(Shutdown() watchdog, force-exit on timeout)
Test plan
- Build CI green
- Manual Windows test: open wallet, wait for Tor/I2P ready, close, verify < 5s shutdown
- Manual Windows test: open wallet, immediately close, verify no hang
- Manual Linux test: same as #2, verify clean exit
- Stress test: open + close 5 times in a row, no resource leak
Risk
TerminateThreadon Tor is unsafe but happens only on graceful timeout path- The watchdog
_exit(0)skips destructors; acceptable because the wallet is exiting anyway - i2pd internals may have already-closed state; guarded with try/catch