55
66use std:: { net:: SocketAddr , sync:: Arc , time:: Instant } ;
77
8+ use std:: collections:: VecDeque ;
9+
810use arc_swap:: ArcSwap ;
9- use tokio:: sync:: { mpsc , watch} ;
11+ use tokio:: sync:: watch;
1012use wallhack_wire:: {
1113 control:: {
1214 ControlRequest , ControlResponse , ErrorResponse , PeerInfo , PingResponse , RouteInfo ,
@@ -18,13 +20,48 @@ use wallhack_wire::{
1820use crate :: NodeRole ;
1921
2022use super :: {
21- log_buffer:: LogBuffer ,
22- metrics:: SharedMetrics ,
23- node_command:: { NodeCommand , reply_channel} ,
24- peers:: SharedRegistry ,
25- routes:: SharedRouteTable ,
23+ log_buffer:: LogBuffer , metrics:: SharedMetrics , peers:: SharedRegistry , routes:: SharedRouteTable ,
2624} ;
2725
26+ /// Reply channel for a single command.
27+ ///
28+ /// Uses a standard-library sync channel so the handler side (which may be
29+ /// called from a synchronous context) can block waiting for the reply without
30+ /// needing an async runtime handle.
31+ type ReplySender < T > = std:: sync:: mpsc:: SyncSender < Result < T , crate :: node_api:: NodeApiError > > ;
32+
33+ /// Create a reply channel pair for a node command.
34+ fn reply_channel < T > ( ) -> (
35+ ReplySender < T > ,
36+ std:: sync:: mpsc:: Receiver < Result < T , crate :: node_api:: NodeApiError > > ,
37+ ) {
38+ std:: sync:: mpsc:: sync_channel ( 1 )
39+ }
40+
41+ /// A command sent from the handler/API layer to the mode task.
42+ #[ derive( Debug ) ]
43+ pub enum NodeCommand {
44+ /// Connect to a remote peer at the given address.
45+ Connect {
46+ /// Target address (host, host:port, etc.).
47+ addr : String ,
48+ /// Channel for sending the result back to the caller.
49+ reply : ReplySender < crate :: node_api:: ConnectInfo > ,
50+ } ,
51+ /// Start listening for incoming peer connections.
52+ Listen {
53+ /// Address to bind.
54+ addr : SocketAddr ,
55+ /// Channel for sending the result back to the caller.
56+ reply : ReplySender < crate :: node_api:: ListenInfo > ,
57+ } ,
58+ /// Disconnect from the currently connected peer.
59+ Disconnect {
60+ /// Channel for sending the result back to the caller.
61+ reply : ReplySender < ( ) > ,
62+ } ,
63+ }
64+
2865/// Mutable runtime state that can change after construction.
2966///
3067/// Stored behind `ArcSwap` for wait-free reads (same pattern as `Registry`).
@@ -36,31 +73,53 @@ struct NodeState {
3673 peer_addr : Option < String > ,
3774}
3875
76+ /// Inner state backing [`SharedNodeState`].
77+ struct SharedNodeStateInner {
78+ state : ArcSwap < NodeState > ,
79+ commands : std:: sync:: Mutex < VecDeque < NodeCommand > > ,
80+ command_notify : tokio:: sync:: Notify ,
81+ }
82+
3983/// Shared node state handle, cloneable and cheaply updatable.
4084///
4185/// Consumers call [`SharedNodeState::update_role`], [`SharedNodeState::update_capabilities`],
4286/// etc. after negotiation or listening starts so that `wallhack info`
4387/// reflects the real state of the daemon.
44- #[ derive( Clone , Debug ) ]
45- pub struct SharedNodeState ( Arc < ArcSwap < NodeState > > ) ;
88+ ///
89+ /// Also hosts the command queue for dynamic connect/listen/disconnect
90+ /// operations, avoiding a dedicated channel between handler and mode task.
91+ #[ derive( Clone ) ]
92+ pub struct SharedNodeState ( Arc < SharedNodeStateInner > ) ;
93+
94+ impl std:: fmt:: Debug for SharedNodeState {
95+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
96+ f. debug_struct ( "SharedNodeState" )
97+ . field ( "state" , & * self . 0 . state . load ( ) )
98+ . finish ( )
99+ }
100+ }
46101
47102impl SharedNodeState {
48103 fn new ( role : NodeRole ) -> Self {
49- Self ( Arc :: new ( ArcSwap :: from_pointee ( NodeState {
50- role,
51- capabilities : Capabilities :: default ( ) ,
52- listen_addr : None ,
53- peer_addr : None ,
54- } ) ) )
104+ Self ( Arc :: new ( SharedNodeStateInner {
105+ state : ArcSwap :: from_pointee ( NodeState {
106+ role,
107+ capabilities : Capabilities :: default ( ) ,
108+ listen_addr : None ,
109+ peer_addr : None ,
110+ } ) ,
111+ commands : std:: sync:: Mutex :: new ( VecDeque :: new ( ) ) ,
112+ command_notify : tokio:: sync:: Notify :: new ( ) ,
113+ } ) )
55114 }
56115
57116 fn load ( & self ) -> arc_swap:: Guard < Arc < NodeState > > {
58- self . 0 . load ( )
117+ self . 0 . state . load ( )
59118 }
60119
61120 /// Update the node role (e.g. after auto-negotiation resolves).
62121 pub fn update_role ( & self , role : NodeRole ) {
63- self . 0 . rcu ( |old| {
122+ self . 0 . state . rcu ( |old| {
64123 let mut new = ( * * old) . clone ( ) ;
65124 new. role = role;
66125 new
@@ -69,7 +128,7 @@ impl SharedNodeState {
69128
70129 /// Update the node's own capabilities.
71130 pub fn update_capabilities ( & self , capabilities : Capabilities ) {
72- self . 0 . rcu ( |old| {
131+ self . 0 . state . rcu ( |old| {
73132 let mut new = ( * * old) . clone ( ) ;
74133 new. capabilities = capabilities;
75134 new
@@ -78,13 +137,47 @@ impl SharedNodeState {
78137
79138 /// Record that the node is now listening on `addr`.
80139 pub fn set_listen_addr ( & self , addr : SocketAddr ) {
81- self . 0 . rcu ( |old| {
140+ self . 0 . state . rcu ( |old| {
82141 let mut new = ( * * old) . clone ( ) ;
83142 new. listen_addr = Some ( addr) ;
84143 new. capabilities . listening = true ;
85144 new
86145 } ) ;
87146 }
147+
148+ /// Enqueue a command for the mode task and wake it.
149+ ///
150+ /// # Panics
151+ ///
152+ /// Panics if the command queue mutex is poisoned.
153+ pub fn push_command ( & self , cmd : NodeCommand ) {
154+ self . 0
155+ . commands
156+ . lock ( )
157+ . expect ( "command queue mutex poisoned" )
158+ . push_back ( cmd) ;
159+ self . 0 . command_notify . notify_one ( ) ;
160+ }
161+
162+ /// Drain all pending commands from the queue.
163+ ///
164+ /// # Panics
165+ ///
166+ /// Panics if the command queue mutex is poisoned.
167+ #[ must_use]
168+ pub fn drain_commands ( & self ) -> Vec < NodeCommand > {
169+ self . 0
170+ . commands
171+ . lock ( )
172+ . expect ( "command queue mutex poisoned" )
173+ . drain ( ..)
174+ . collect ( )
175+ }
176+
177+ /// Wait until at least one command is enqueued.
178+ pub async fn notified ( & self ) {
179+ self . 0 . command_notify . notified ( ) . await ;
180+ }
88181}
89182
90183/// Configuration for the control handler.
@@ -126,14 +219,6 @@ pub struct Handler {
126219 route_updates : tokio:: sync:: broadcast:: Sender < super :: routes:: RouteUpdate > ,
127220 state : SharedNodeState ,
128221 start_time : Instant ,
129- /// Sender used by `connect/listen/disconnect` to dispatch commands to the
130- /// active mode task. `None` if the channel has been dropped (mode does not
131- /// support dynamic operations).
132- cmd_tx : mpsc:: Sender < NodeCommand > ,
133- /// Receiver extracted once by the mode task before it starts running.
134- /// Wrapped in `Mutex<Option<…>>` so it can be moved out without requiring
135- /// `&mut self`.
136- cmd_rx : std:: sync:: Mutex < Option < mpsc:: Receiver < NodeCommand > > > ,
137222}
138223
139224impl Handler {
@@ -153,7 +238,6 @@ impl Handler {
153238 ) -> Self {
154239 let state = SharedNodeState :: new ( config. node_role ) ;
155240 let ( hint_tx, _) = watch:: channel ( None ) ;
156- let ( cmd_tx, cmd_rx) = mpsc:: channel ( 8 ) ;
157241 Self {
158242 config,
159243 hint_tx,
@@ -164,8 +248,6 @@ impl Handler {
164248 route_updates,
165249 state,
166250 start_time : Instant :: now ( ) ,
167- cmd_tx,
168- cmd_rx : std:: sync:: Mutex :: new ( Some ( cmd_rx) ) ,
169251 }
170252 }
171253
@@ -185,19 +267,6 @@ impl Handler {
185267 self . hint_tx . subscribe ( )
186268 }
187269
188- /// Extracts the [`NodeCommand`] receiver so the mode task can poll it.
189- ///
190- /// May only be called once. Returns `None` if already taken.
191- ///
192- /// # Panics
193- ///
194- /// Panics if the internal mutex is poisoned (indicates a prior panic in
195- /// another thread while holding the lock, which should never happen in
196- /// normal operation).
197- pub fn take_cmd_rx ( & self ) -> Option < mpsc:: Receiver < NodeCommand > > {
198- self . cmd_rx . lock ( ) . expect ( "cmd_rx mutex poisoned" ) . take ( )
199- }
200-
201270 /// Handles a control request and returns a response.
202271 ///
203272 /// # Errors
@@ -448,23 +517,12 @@ impl crate::node_api::NodeApi for Handler {
448517 }
449518
450519 fn connect ( & self , addr : & str ) -> crate :: node_api:: Result < crate :: node_api:: ConnectInfo > {
451- let ( reply_tx, reply_rx) = reply_channel ( ) ;
452- self . cmd_tx
453- . try_send ( NodeCommand :: Connect {
454- addr : addr. to_string ( ) ,
455- reply : reply_tx,
456- } )
457- . map_err ( |err| match err {
458- tokio:: sync:: mpsc:: error:: TrySendError :: Closed ( _) => {
459- crate :: node_api:: NodeApiError :: NotSupported (
460- "dynamic connect not supported in this mode" . into ( ) ,
461- )
462- }
463- tokio:: sync:: mpsc:: error:: TrySendError :: Full ( _) => {
464- crate :: node_api:: NodeApiError :: Internal ( "command queue full, try again" . into ( ) )
465- }
466- } ) ?;
467- tokio:: task:: block_in_place ( || reply_rx. recv ( ) ) . map_err ( |_| {
520+ let ( reply_sender, reply_receiver) = reply_channel ( ) ;
521+ self . state . push_command ( NodeCommand :: Connect {
522+ addr : addr. to_string ( ) ,
523+ reply : reply_sender,
524+ } ) ;
525+ tokio:: task:: block_in_place ( || reply_receiver. recv ( ) ) . map_err ( |_| {
468526 crate :: node_api:: NodeApiError :: Internal ( "mode task dropped reply" . into ( ) )
469527 } ) ?
470528 }
@@ -473,42 +531,22 @@ impl crate::node_api::NodeApi for Handler {
473531 & self ,
474532 addr : std:: net:: SocketAddr ,
475533 ) -> crate :: node_api:: Result < crate :: node_api:: ListenInfo > {
476- let ( reply_tx, reply_rx) = reply_channel ( ) ;
477- self . cmd_tx
478- . try_send ( NodeCommand :: Listen {
479- addr,
480- reply : reply_tx,
481- } )
482- . map_err ( |err| match err {
483- tokio:: sync:: mpsc:: error:: TrySendError :: Closed ( _) => {
484- crate :: node_api:: NodeApiError :: NotSupported (
485- "dynamic listen not supported in this mode" . into ( ) ,
486- )
487- }
488- tokio:: sync:: mpsc:: error:: TrySendError :: Full ( _) => {
489- crate :: node_api:: NodeApiError :: Internal ( "command queue full, try again" . into ( ) )
490- }
491- } ) ?;
492- tokio:: task:: block_in_place ( || reply_rx. recv ( ) ) . map_err ( |_| {
534+ let ( reply_sender, reply_receiver) = reply_channel ( ) ;
535+ self . state . push_command ( NodeCommand :: Listen {
536+ addr,
537+ reply : reply_sender,
538+ } ) ;
539+ tokio:: task:: block_in_place ( || reply_receiver. recv ( ) ) . map_err ( |_| {
493540 crate :: node_api:: NodeApiError :: Internal ( "mode task dropped reply" . into ( ) )
494541 } ) ?
495542 }
496543
497544 fn disconnect ( & self ) -> crate :: node_api:: Result < ( ) > {
498- let ( reply_tx, reply_rx) = reply_channel ( ) ;
499- self . cmd_tx
500- . try_send ( NodeCommand :: Disconnect { reply : reply_tx } )
501- . map_err ( |err| match err {
502- tokio:: sync:: mpsc:: error:: TrySendError :: Closed ( _) => {
503- crate :: node_api:: NodeApiError :: NotSupported (
504- "dynamic disconnect not supported in this mode" . into ( ) ,
505- )
506- }
507- tokio:: sync:: mpsc:: error:: TrySendError :: Full ( _) => {
508- crate :: node_api:: NodeApiError :: Internal ( "command queue full, try again" . into ( ) )
509- }
510- } ) ?;
511- tokio:: task:: block_in_place ( || reply_rx. recv ( ) ) . map_err ( |_| {
545+ let ( reply_sender, reply_receiver) = reply_channel ( ) ;
546+ self . state . push_command ( NodeCommand :: Disconnect {
547+ reply : reply_sender,
548+ } ) ;
549+ tokio:: task:: block_in_place ( || reply_receiver. recv ( ) ) . map_err ( |_| {
512550 crate :: node_api:: NodeApiError :: Internal ( "mode task dropped reply" . into ( ) )
513551 } ) ?
514552 }
0 commit comments