wallet2: abort in-flight daemon request on shutdown

(cherry picked from commit 3c0980f5991a3cc27f7b3890cb68251c971a4b07)
This commit is contained in:
woodser
2026-10-04 18:50:23 -04:00
committed by such-gitea
parent 818a0067b6
commit 5fe3caa4d9
9 changed files with 374 additions and 28 deletions
+1
View File
@@ -39,6 +39,7 @@ public:
bool connect(const std::string& addr, int port, std::chrono::milliseconds timeout, bool ssl = false, const std::string& bind_ip = "0.0.0.0") { return true; }
bool connect(const std::string& addr, const std::string& port, std::chrono::milliseconds timeout, bool ssl = false, const std::string& bind_ip = "0.0.0.0") { return true; }
bool disconnect() { return true; }
bool shutdown() { return true; }
bool send(const boost::string_ref buff, std::chrono::milliseconds timeout) { return true; }
bool send(const void* data, size_t sz) { return true; }
bool is_connected() { return true; }
+1
View File
@@ -97,6 +97,7 @@ public:
bool connect(const std::string&, int, std::chrono::milliseconds, bool = false, const std::string& = "0.0.0.0") { return true; }
bool connect(const std::string&, const std::string&, std::chrono::milliseconds, bool = false, const std::string& = "0.0.0.0") { return true; }
bool disconnect() { return true; }
bool shutdown() { return true; }
bool send(const boost::string_ref, std::chrono::milliseconds) { return true; }
bool send(const void*, size_t) { return true; }
bool recv(std::string& buff, std::chrono::milliseconds)
+181
View File
@@ -53,13 +53,20 @@
#include <gtest/gtest.h>
#include <map>
#include <memory>
#include <thread>
#include <type_traits>
#ifndef _WIN32
#include <atomic>
#include <csignal>
#include <pthread.h>
#endif
#include "crypto/crypto.h"
#include "net/dandelionpp.h"
#include "net/error.h"
#include "net/host.h"
#include "net/i2p_address.h"
#include "net/net_helper.h"
#include "net/net_utils_base.h"
#include "net/socks.h"
#include "net/socks_connect.h"
@@ -89,6 +96,180 @@ namespace
"zpv4fa3szgel7vf6jdjeugizdclq2vzkelscs2bhbgnlldzzggcen3ad.onion";
}
TEST(blocked_mode_client, shutdown_aborts_blocked_recv)
{
boost::asio::io_context server_io;
boost::asio::ip::tcp::acceptor acceptor{
server_io,
{boost::asio::ip::address_v4::loopback(), 0}
};
epee::net_utils::blocked_mode_client client;
client.set_ssl(epee::net_utils::ssl_options_t{
epee::net_utils::ssl_support_t::e_ssl_support_disabled
});
const std::string port = std::to_string(acceptor.local_endpoint().port());
ASSERT_TRUE(client.connect("127.0.0.1", port, std::chrono::seconds{5}));
boost::system::error_code error;
boost::asio::ip::tcp::socket peer{server_io};
acceptor.accept(peer, error);
ASSERT_FALSE(error);
// recv blocks on the silent peer until a single shutdown from another thread
// aborts it; the abort is sticky, so the shot is valid on every timing
std::thread stopper{[&client] {
std::this_thread::sleep_for(std::chrono::milliseconds{100});
EXPECT_TRUE(client.shutdown());
}};
std::string response;
const auto start = std::chrono::steady_clock::now();
EXPECT_FALSE(client.recv(response, std::chrono::seconds{30}));
EXPECT_LT(std::chrono::steady_clock::now() - start, std::chrono::seconds{10});
stopper.join();
EXPECT_FALSE(client.is_connected());
}
#ifndef _WIN32
namespace
{
std::atomic<epee::net_utils::blocked_mode_client*> g_signal_client{nullptr};
void shutdown_from_signal(int)
{
epee::net_utils::blocked_mode_client* const client = g_signal_client.load();
if (client)
client->shutdown();
}
}
TEST(blocked_mode_client, shutdown_aborts_blocked_recv_from_signal_handler)
{
boost::asio::io_context server_io;
boost::asio::ip::tcp::acceptor acceptor{
server_io,
{boost::asio::ip::address_v4::loopback(), 0}
};
epee::net_utils::blocked_mode_client client;
client.set_ssl(epee::net_utils::ssl_options_t{
epee::net_utils::ssl_support_t::e_ssl_support_disabled
});
const std::string port = std::to_string(acceptor.local_endpoint().port());
ASSERT_TRUE(client.connect("127.0.0.1", port, std::chrono::seconds{5}));
boost::system::error_code error;
boost::asio::ip::tcp::socket peer{server_io};
acceptor.accept(peer, error);
ASSERT_FALSE(error);
struct sigaction handler {}, previous {};
handler.sa_handler = shutdown_from_signal;
sigemptyset(&handler.sa_mask);
ASSERT_EQ(0, sigaction(SIGUSR1, &handler, &previous));
g_signal_client = &client;
// the signal lands on the thread blocked in recv, so the handler's shutdown()
// runs in signal context and may only use async-signal-safe calls
const pthread_t blocked = pthread_self();
std::thread stopper{[blocked] {
std::this_thread::sleep_for(std::chrono::milliseconds{100});
pthread_kill(blocked, SIGUSR1);
}};
std::string response;
const auto start = std::chrono::steady_clock::now();
EXPECT_FALSE(client.recv(response, std::chrono::seconds{30}));
EXPECT_LT(std::chrono::steady_clock::now() - start, std::chrono::seconds{10});
stopper.join();
EXPECT_FALSE(client.is_connected());
g_signal_client = nullptr;
sigaction(SIGUSR1, &previous, nullptr);
}
#endif
TEST(blocked_mode_client, shutdown_aborts_blocked_connect)
{
// a connector whose future never becomes ready simulates an unresponsive peer
const auto never_ready = std::make_shared<boost::promise<boost::asio::ip::tcp::socket>>();
epee::net_utils::blocked_mode_client client;
client.set_ssl(epee::net_utils::ssl_options_t{
epee::net_utils::ssl_support_t::e_ssl_support_disabled
});
client.set_connector([never_ready] (const std::string&, const std::string&, boost::asio::steady_timer&) {
return never_ready->get_future();
});
// a single shutdown aborts the blocked connect; sticky, so valid on every timing
std::thread stopper{[&client] {
std::this_thread::sleep_for(std::chrono::milliseconds{100});
EXPECT_TRUE(client.shutdown());
}};
const auto start = std::chrono::steady_clock::now();
EXPECT_FALSE(client.connect("127.0.0.1", "80", std::chrono::seconds{30}));
EXPECT_LT(std::chrono::steady_clock::now() - start, std::chrono::seconds{10});
stopper.join();
}
TEST(blocked_mode_client, shutdown_aborts_stalled_ssl_handshake)
{
// the acceptor completes TCP connects in the kernel backlog but never
// answers the TLS handshake, so the handshake stalls until aborted
boost::asio::io_context server_io;
boost::asio::ip::tcp::acceptor acceptor{
server_io,
{boost::asio::ip::address_v4::loopback(), 0}
};
epee::net_utils::blocked_mode_client client;
client.set_ssl(epee::net_utils::ssl_options_t{
epee::net_utils::ssl_support_t::e_ssl_support_enabled
});
const std::string port = std::to_string(acceptor.local_endpoint().port());
std::thread stopper{[&client] {
std::this_thread::sleep_for(std::chrono::milliseconds{100});
EXPECT_TRUE(client.shutdown());
}};
const auto start = std::chrono::steady_clock::now();
EXPECT_FALSE(client.connect("127.0.0.1", port, std::chrono::seconds{30}));
EXPECT_LT(std::chrono::steady_clock::now() - start, std::chrono::seconds{10});
stopper.join();
}
TEST(blocked_mode_client, shutdown_is_permanent)
{
boost::asio::io_context server_io;
boost::asio::ip::tcp::acceptor acceptor{
server_io,
{boost::asio::ip::address_v4::loopback(), 0}
};
epee::net_utils::blocked_mode_client client;
client.set_ssl(epee::net_utils::ssl_options_t{
epee::net_utils::ssl_support_t::e_ssl_support_disabled
});
const std::string port = std::to_string(acceptor.local_endpoint().port());
ASSERT_TRUE(client.connect("127.0.0.1", port, std::chrono::seconds{5}));
boost::system::error_code error;
boost::asio::ip::tcp::socket peer{server_io};
acceptor.accept(peer, error);
ASSERT_FALSE(error);
// the abort is sticky: a shutdown with nothing in flight still fails the next
// op promptly, so a request scheduled before it can never start a doomed call
EXPECT_TRUE(client.shutdown());
EXPECT_FALSE(client.is_connected());
std::string response;
const auto start = std::chrono::steady_clock::now();
EXPECT_FALSE(client.recv(response, std::chrono::seconds{30}));
EXPECT_LT(std::chrono::steady_clock::now() - start, std::chrono::seconds{10});
// the torn-down client never reconnects
EXPECT_FALSE(client.connect("127.0.0.1", port, std::chrono::seconds{5}));
}
TEST(tor_address, constants)
{
static_assert(!net::tor_address::is_local(), "bad is_local() response");