ff90824247
* 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>
387 lines
14 KiB
C++
387 lines
14 KiB
C++
// Copyright (c) 2025-2026 Triangles developers
|
|
// Embedded Tor integration - runs Tor in-process as a library
|
|
// Distributed under the MIT/X11 software license
|
|
//
|
|
// BUILD REQUIREMENT: Link against libtor.a built from the official Tor source.
|
|
//
|
|
// This file compiles in two modes:
|
|
// 1. ENABLE_TOR_EMBEDDED defined: full embedded Tor via tor_api.h
|
|
// 2. ENABLE_TOR_EMBEDDED not defined: stubs that fall back to external tor_process
|
|
|
|
#include "tor_embedded.h"
|
|
#include "../util.h"
|
|
#include "../net.h"
|
|
|
|
#include <filesystem>
|
|
#include <thread>
|
|
#include <fstream>
|
|
#include <cstring>
|
|
#include <chrono>
|
|
#include <ctime>
|
|
#include <vector>
|
|
#include <string>
|
|
#include <signal.h>
|
|
|
|
#ifdef WIN32
|
|
#include <winsock2.h>
|
|
#include <ws2tcpip.h>
|
|
#include <windows.h>
|
|
#else
|
|
#include <sys/socket.h>
|
|
#include <netinet/in.h>
|
|
#include <unistd.h>
|
|
#include <pthread.h>
|
|
#endif
|
|
|
|
#ifdef ENABLE_TOR_EMBEDDED
|
|
// Official Tor C API (tor >= 0.4.5)
|
|
extern "C" {
|
|
#include <tor_api.h>
|
|
}
|
|
#endif
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
// Singleton
|
|
CTorEmbedded* CTorEmbedded::instance = nullptr;
|
|
|
|
CTorEmbedded* CTorEmbedded::GetInstance()
|
|
{
|
|
if (!instance)
|
|
instance = new CTorEmbedded();
|
|
return instance;
|
|
}
|
|
|
|
CTorEmbedded::CTorEmbedded()
|
|
: running(false)
|
|
, socksPort(19099)
|
|
, hiddenServicePort(24112)
|
|
, hiddenServiceEnabled(true)
|
|
{
|
|
}
|
|
|
|
CTorEmbedded::~CTorEmbedded()
|
|
{
|
|
Stop();
|
|
}
|
|
|
|
std::string CTorEmbedded::GetSocksProxy() const
|
|
{
|
|
return "127.0.0.1:" + std::to_string(socksPort);
|
|
}
|
|
|
|
#ifdef ENABLE_TOR_EMBEDDED
|
|
|
|
// ========================================================================
|
|
// Embedded mode: Tor runs in-process via libtor.a / tor_api.h
|
|
// ========================================================================
|
|
|
|
// The Tor thread entry point. tor_run_main() blocks until Tor shuts down.
|
|
static void TorThreadFunc(std::vector<std::string> argv_strings)
|
|
{
|
|
// Build a C argv array that tor_run_main expects.
|
|
std::vector<char*> argv_ptrs;
|
|
for (auto& s : argv_strings)
|
|
argv_ptrs.push_back(&s[0]);
|
|
argv_ptrs.push_back(nullptr);
|
|
|
|
tor_main_configuration_t* cfg = tor_main_configuration_new();
|
|
if (!cfg) {
|
|
printf("ERROR: tor_main_configuration_new() failed\n");
|
|
CTorEmbedded::GetInstance()->SetRunning(false);
|
|
return;
|
|
}
|
|
|
|
// argc does not count the trailing nullptr
|
|
int rc = tor_main_configuration_set_command_line(
|
|
cfg, (int)(argv_ptrs.size() - 1), argv_ptrs.data());
|
|
if (rc != 0) {
|
|
printf("ERROR: tor_main_configuration_set_command_line() returned %d\n", rc);
|
|
tor_main_configuration_free(cfg);
|
|
CTorEmbedded::GetInstance()->SetRunning(false);
|
|
return;
|
|
}
|
|
|
|
printf("Embedded Tor starting (SOCKS %d, HS port %d)...\n",
|
|
CTorEmbedded::GetInstance()->GetSocksPort(),
|
|
CTorEmbedded::GetInstance()->GetHiddenServicePort());
|
|
|
|
// This blocks until Tor exits
|
|
rc = tor_run_main(cfg);
|
|
tor_main_configuration_free(cfg);
|
|
|
|
printf("Embedded Tor exited with code %d\n", rc);
|
|
CTorEmbedded::GetInstance()->SetRunning(false);
|
|
}
|
|
|
|
bool CTorEmbedded::Start(int socks, int hsPort, bool enableHiddenService)
|
|
{
|
|
if (running.load()) return true;
|
|
|
|
lastError.clear();
|
|
socksPort = socks;
|
|
hiddenServiceEnabled = enableHiddenService;
|
|
hiddenServicePort = hiddenServiceEnabled ? hsPort : 0;
|
|
onionHostname.clear();
|
|
|
|
// Prepare Tor data directory under the wallet's data dir
|
|
torDataDir = (::GetDataDir() / "tor_data").string();
|
|
fs::create_directories(torDataDir);
|
|
// CRITICAL: Tor refuses to use a DataDirectory readable by other users.
|
|
// Without 0700, tor_run_main() returns -1 and the embedded Tor never starts.
|
|
fs::permissions(torDataDir, fs::perms::owner_all, fs::perm_options::replace);
|
|
|
|
// triangles fix: auto-repair `state`-as-file corruption (pitfall #19).
|
|
// Tor's atomic state-write pattern is: write `state.tmp` → rename to `state`.
|
|
// If the daemon is killed or the process crashes mid-write, the rename can
|
|
// fail and `state` may be left as a regular file (or a partial file). On
|
|
// next start, Tor sees "State file ... is not a file? Failing." and dies
|
|
// with code -1 ("Reading config failed"). This was hit on DNS3 on
|
|
// 2026-05-24 and on the TRI-LAPTOP GUI wallet on 2026-06-15. The user-facing
|
|
// symptom is "Tor failed to start. Triangles requires Tor to operate." and
|
|
// the only fix was manually renaming the corrupt file. Detect this state
|
|
// here and auto-rename so the daemon is self-healing.
|
|
{
|
|
fs::path statePath = fs::path(torDataDir) / "state";
|
|
std::error_code ec;
|
|
if (fs::exists(statePath, ec) && !fs::is_directory(statePath, ec)) {
|
|
// state is a file (or symlink to one) — quarantine it
|
|
auto now = std::chrono::system_clock::now();
|
|
auto t = std::chrono::system_clock::to_time_t(now);
|
|
char ts[32];
|
|
std::strftime(ts, sizeof(ts), "%Y%m%d-%H%M%S", std::gmtime(&t));
|
|
fs::path quarantine = fs::path(torDataDir) /
|
|
(std::string("state.corrupt-") + ts);
|
|
try {
|
|
fs::rename(statePath, quarantine, ec);
|
|
if (ec) {
|
|
// rename can fail on Windows if dest exists; remove then rename
|
|
fs::remove(quarantine, ec);
|
|
fs::rename(statePath, quarantine, ec);
|
|
}
|
|
printf("Tor state was a file (corrupt) — quarantined to %s for inspection. Tor will recreate state/ as a directory.\n",
|
|
quarantine.filename().string().c_str());
|
|
} catch (const std::exception& e) {
|
|
printf("WARNING: could not quarantine corrupt Tor state file %s: %s\n",
|
|
statePath.string().c_str(), e.what());
|
|
// Last resort: try to remove it so Tor can proceed
|
|
fs::remove(statePath, ec);
|
|
}
|
|
}
|
|
}
|
|
|
|
std::string hsDir;
|
|
if (hiddenServiceEnabled) {
|
|
hsDir = (fs::path(torDataDir) / "hidden_service").string();
|
|
fs::create_directories(hsDir);
|
|
// CRITICAL: Tor rejects hidden service directories that are not 0700
|
|
// ("Permissions on directory ... are too permissive") and aborts config
|
|
// validation with code -1. This was the root cause of "Embedded Tor
|
|
// exited with code -1" — fs::create_directories honors umask (0022 on
|
|
// most Linux systems), leaving the dir at 0755. Force 0700 after creation.
|
|
fs::permissions(hsDir, fs::perms::owner_all, fs::perm_options::replace);
|
|
}
|
|
|
|
// Build the argv for tor_run_main
|
|
std::vector<std::string> argv;
|
|
argv.push_back("tor"); // program name (argv[0])
|
|
argv.push_back("--SocksPort");
|
|
argv.push_back(std::to_string(socksPort));
|
|
argv.push_back("--DataDirectory");
|
|
argv.push_back(torDataDir);
|
|
if (hiddenServiceEnabled) {
|
|
argv.push_back("--HiddenServiceDir");
|
|
argv.push_back(hsDir);
|
|
argv.push_back("--HiddenServiceVersion");
|
|
argv.push_back("3");
|
|
argv.push_back("--HiddenServicePort");
|
|
argv.push_back(std::to_string(hiddenServicePort) + " 127.0.0.1:" + std::to_string(hiddenServicePort));
|
|
}
|
|
argv.push_back("--AvoidDiskWrites");
|
|
argv.push_back("1");
|
|
argv.push_back("--Log");
|
|
argv.push_back("notice stderr");
|
|
|
|
running.store(true);
|
|
|
|
// Launch Tor on a dedicated thread (tor_run_main blocks)
|
|
// We keep the handle (do NOT detach) so Stop() can join the thread.
|
|
// A detached thread that is still running will block process exit
|
|
// indefinitely on both Windows and Linux.
|
|
torThread = std::thread(TorThreadFunc, argv);
|
|
|
|
// Wait for SOCKS port to become available (up to 60s)
|
|
printf("Waiting for embedded Tor to bootstrap...\n");
|
|
for (int i = 0; i < 60; i++) {
|
|
MilliSleep(1000);
|
|
if (fShutdown) {
|
|
Stop();
|
|
return false;
|
|
}
|
|
|
|
// Quick port check
|
|
#ifdef WIN32
|
|
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
|
|
if (sock != INVALID_SOCKET) {
|
|
#else
|
|
int sock = socket(AF_INET, SOCK_STREAM, 0);
|
|
if (sock >= 0) {
|
|
#endif
|
|
struct sockaddr_in addr;
|
|
memset(&addr, 0, sizeof(addr));
|
|
addr.sin_family = AF_INET;
|
|
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
|
addr.sin_port = htons(socksPort);
|
|
bool up = (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) == 0);
|
|
#ifdef WIN32
|
|
closesocket(sock);
|
|
#else
|
|
close(sock);
|
|
#endif
|
|
if (up) {
|
|
printf("Embedded Tor SOCKS proxy ready on port %d (took %ds)\n", socksPort, i + 1);
|
|
|
|
// Read .onion hostname if available
|
|
if (hiddenServiceEnabled) {
|
|
fs::path hostnameFile = fs::path(hsDir) / "hostname";
|
|
if (fs::exists(hostnameFile)) {
|
|
std::ifstream f(hostnameFile.string().c_str());
|
|
if (f.is_open())
|
|
std::getline(f, onionHostname);
|
|
if (!onionHostname.empty())
|
|
printf("Tor hidden service: %s\n", onionHostname.c_str());
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (!running.load()) {
|
|
lastError = "Embedded Tor thread exited during bootstrap before the SOCKS proxy became available.";
|
|
printf("ERROR: Embedded Tor thread exited during bootstrap\n");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
lastError = strprintf("Embedded Tor did not expose SOCKS port %d within 60 seconds.", socksPort);
|
|
printf("WARNING: Embedded Tor started but SOCKS not ready after 60s (still bootstrapping)\n");
|
|
return true;
|
|
}
|
|
|
|
void CTorEmbedded::Stop()
|
|
{
|
|
if (!running.load()) {
|
|
// Not running; just make sure the thread handle is released
|
|
if (torThread.joinable()) torThread.join();
|
|
return;
|
|
}
|
|
printf("Requesting embedded Tor shutdown...\n");
|
|
|
|
// tor_run_main respects signals; raise SIGTERM to trigger graceful exit.
|
|
// On Linux this causes tor_run_main to return and the thread to exit.
|
|
// On Windows there is no signal mechanism in tor_api 0.4.x — we have to
|
|
// wait for tor_run_main to return on its own (the shutdown path is
|
|
// triggered by the `running` flag being observed by the calling code,
|
|
// but tor_run_main itself does not poll it). In practice Tor exits when
|
|
// the process exits, so we just join with a timeout below.
|
|
#if !defined(WIN32) || defined(__MINGW32__)
|
|
// MINGW std::thread is pthread-based, so signal-based shutdown works
|
|
// there too. Raise SIGTERM so tor_run_main can observe it.
|
|
raise(SIGTERM);
|
|
#endif
|
|
|
|
// Give Tor up to 5 seconds to shut down cleanly.
|
|
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
|
|
while (torThread.joinable() && std::chrono::steady_clock::now() < deadline) {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
|
if (!running.load()) {
|
|
// Tor's TorThreadFunc sets running=false after tor_run_main returns.
|
|
torThread.join();
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (torThread.joinable()) {
|
|
// Tor did not exit cleanly within 5 seconds. Force-terminate the
|
|
// thread as a last resort — we are about to exit the process anyway.
|
|
printf("WARNING: embedded Tor did not exit within 5s; force-terminating thread\n");
|
|
#if defined(WIN32) && !defined(__MINGW32__)
|
|
// MSVC std::thread::native_handle_type is HANDLE (a pointer) on
|
|
// Windows. TerminateThread is unsafe but the process is exiting.
|
|
TerminateThread(torThread.native_handle(), 0);
|
|
WaitForSingleObject(torThread.native_handle(), 1000);
|
|
// After TerminateThread the handle is still valid; detach to release.
|
|
torThread.detach();
|
|
#else
|
|
// Linux + MINGW: std::thread is pthread-based, native_handle() returns
|
|
// pthread_t. pthread_cancel is async: the thread exits at its next
|
|
// cancellation point (or immediately for C code with no cancellation
|
|
// points — in which case pthread_join blocks). Either way,
|
|
// pthread_join drains the thread. Detach the std::thread handle so the
|
|
// destructor doesn't call std::terminate on a still-joinable handle.
|
|
pthread_cancel(torThread.native_handle());
|
|
void* retval = nullptr;
|
|
pthread_join(torThread.native_handle(), &retval);
|
|
torThread.detach();
|
|
#endif
|
|
}
|
|
|
|
running.store(false);
|
|
}
|
|
|
|
#else // !ENABLE_TOR_EMBEDDED
|
|
|
|
// ========================================================================
|
|
// Fallback stubs: embedded Tor not compiled in, use external tor_process
|
|
// ========================================================================
|
|
|
|
#include "tor_process.h"
|
|
|
|
bool CTorEmbedded::Start(int socks, int hsPort, bool enableHiddenService)
|
|
{
|
|
printf("Embedded Tor not compiled in. Using external Tor process.\n");
|
|
// Delegate to the external process manager
|
|
socksPort = socks;
|
|
hiddenServiceEnabled = enableHiddenService;
|
|
hiddenServicePort = hiddenServiceEnabled ? hsPort : 0;
|
|
onionHostname.clear();
|
|
lastError.clear();
|
|
torDataDir = (::GetDataDir() / "tor_data").string();
|
|
running.store(StartTorProcess(torDataDir, socksPort, hiddenServicePort, hiddenServiceEnabled));
|
|
if (!running.load()) {
|
|
lastError = CTorProcess::GetInstance()->GetStartupError();
|
|
}
|
|
return running.load();
|
|
}
|
|
|
|
void CTorEmbedded::Stop()
|
|
{
|
|
StopTorProcess();
|
|
running.store(false);
|
|
}
|
|
|
|
#endif // ENABLE_TOR_EMBEDDED
|
|
|
|
// ========================================================================
|
|
// Global hooks (called from init.cpp)
|
|
// ========================================================================
|
|
|
|
bool StartEmbeddedTor()
|
|
{
|
|
if (GetBoolArg("-notor", false)) {
|
|
printf("Tor disabled by -notor flag\n");
|
|
return false;
|
|
}
|
|
|
|
bool enableHiddenService = GetBoolArg("-torhiddenservice", true);
|
|
int socksPort = GetArg("-torsocks", 19099);
|
|
int hsPort = enableHiddenService ? GetArg("-torhsport", GetListenPort()) : 0;
|
|
|
|
return CTorEmbedded::GetInstance()->Start(socksPort, hsPort, enableHiddenService);
|
|
}
|
|
|
|
void StopEmbeddedTor()
|
|
{
|
|
CTorEmbedded::GetInstance()->Stop();
|
|
}
|