11#pragma once
22#include < boost/asio.hpp>
3+ #include < optional>
34
45#include < core/log.hpp>
56#include < core/socket.hpp>
@@ -19,7 +20,7 @@ class Server
1920{
2021private:
2122 INetwork* m_node;
22- io::ip::tcp::acceptor m_acceptor;
23+ std::optional< io::ip::tcp::acceptor> m_acceptor;
2324
2425protected:
2526 void accept ()
@@ -30,7 +31,7 @@ class Server
3031 // fall back to raw m_node (preserves prior behavior).
3132 auto weak_node = m_node->weak_from_this ();
3233 bool was_managed = weak_node.lock () != nullptr ;
33- m_acceptor. async_accept (
34+ m_acceptor-> async_accept (
3435 [this , weak_node, was_managed](boost::system::error_code ec, io::ip::tcp::socket io_socket)
3536 {
3637 if (ec)
@@ -57,32 +58,43 @@ class Server
5758
5859public:
5960 Server (io::io_context* context, INetwork* node, const std::string& /* label*/ = " " )
60- : m_acceptor(*context), m_node(node)
61+ : m_node(node)
6162 {
63+ // Rig-free / test construction (context == nullptr, e.g. a default-
64+ // constructed BaseNode used for share-admit unit tests) leaves the
65+ // acceptor unengaged so no null io_context is dereferenced. listen()
66+ // is only valid once a real io_context has been wired in.
67+ if (context)
68+ m_acceptor.emplace (*context);
6269 }
6370
6471 void listen (auto listen_port)
6572 {
73+ if (!m_acceptor)
74+ {
75+ LOG_ERROR << " listen() called on a context-less Server" ;
76+ return ;
77+ }
6678 io::ip::tcp::endpoint listen_ep (io::ip::tcp::v4 (), listen_port);
6779
68- m_acceptor. open (listen_ep.protocol ());
69- m_acceptor. set_option (io::socket_base::reuse_address (true ));
70- m_acceptor. bind (listen_ep);
71- m_acceptor. listen ();
80+ m_acceptor-> open (listen_ep.protocol ());
81+ m_acceptor-> set_option (io::socket_base::reuse_address (true ));
82+ m_acceptor-> bind (listen_ep);
83+ m_acceptor-> listen ();
7284 accept ();
7385
7486 LOG_INFO << " Factory started for port: " << listen_ep.port ();
7587 }
7688
77- uint16_t listen_port () const { return m_acceptor. local_endpoint ().port (); }
89+ uint16_t listen_port () const { return m_acceptor-> local_endpoint ().port (); }
7890};
7991
8092class Client
8193{
8294private:
8395 INetwork* m_node;
8496 io::io_context* m_context;
85- io::ip::tcp::resolver m_resolver;
97+ std::optional< io::ip::tcp::resolver> m_resolver;
8698 std::string m_label = " Net" ; // chain/protocol label for log messages
8799
88100 void connect_socket (boost::asio::ip::tcp::resolver::results_type endpoints)
@@ -136,7 +148,12 @@ class Client
136148 // only enforce the alive check when there WAS a shared owner to
137149 // begin with.
138150 bool was_managed = weak_node.lock () != nullptr ;
139- m_resolver.async_resolve (addr.address (), addr.port_str (),
151+ if (!m_resolver)
152+ {
153+ LOG_ERROR << " resolve() called on a context-less Client" ;
154+ return ;
155+ }
156+ m_resolver->async_resolve (addr.address (), addr.port_str (),
140157 [this , weak_node, was_managed, addr = addr](const auto & ec, auto endpoints)
141158 {
142159 if (ec)
@@ -160,8 +177,13 @@ class Client
160177
161178public:
162179 Client (io::io_context* context, INetwork* node, const std::string& label = " Net" )
163- : m_context(context ), m_resolver(* context), m_node(node ), m_label(label)
180+ : m_node(node ), m_context( context), m_label(label)
164181 {
182+ // Rig-free / test construction (context == nullptr) defers the resolver
183+ // so no null io_context is dereferenced; resolve()/connect() are only
184+ // valid once a real io_context has been wired in.
185+ if (context)
186+ m_resolver.emplace (*context);
165187 }
166188
167189 void connect (NetService addr)
0 commit comments