66use std:: { net:: SocketAddr , sync:: Arc , time:: Instant } ;
77
88use arc_swap:: ArcSwap ;
9- use tokio:: sync:: watch;
9+ use tokio:: sync:: { mpsc , watch} ;
1010use wallhack_wire:: {
1111 control:: {
1212 ControlRequest , ControlResponse , ErrorResponse , PeerInfo , PingResponse , RouteInfo ,
@@ -18,7 +18,11 @@ use wallhack_wire::{
1818use crate :: NodeRole ;
1919
2020use super :: {
21- log_buffer:: LogBuffer , metrics:: SharedMetrics , peers:: SharedRegistry , routes:: SharedRouteTable ,
21+ log_buffer:: LogBuffer ,
22+ metrics:: SharedMetrics ,
23+ node_command:: { NodeCommand , reply_channel} ,
24+ peers:: SharedRegistry ,
25+ routes:: SharedRouteTable ,
2226} ;
2327
2428/// Mutable runtime state that can change after construction.
@@ -122,6 +126,14 @@ pub struct Handler {
122126 route_updates : tokio:: sync:: broadcast:: Sender < super :: routes:: RouteUpdate > ,
123127 state : SharedNodeState ,
124128 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 > > > ,
125137}
126138
127139impl Handler {
@@ -141,6 +153,7 @@ impl Handler {
141153 ) -> Self {
142154 let state = SharedNodeState :: new ( config. node_role ) ;
143155 let ( hint_tx, _) = watch:: channel ( None ) ;
156+ let ( cmd_tx, cmd_rx) = mpsc:: channel ( 8 ) ;
144157 Self {
145158 config,
146159 hint_tx,
@@ -151,6 +164,8 @@ impl Handler {
151164 route_updates,
152165 state,
153166 start_time : Instant :: now ( ) ,
167+ cmd_tx,
168+ cmd_rx : std:: sync:: Mutex :: new ( Some ( cmd_rx) ) ,
154169 }
155170 }
156171
@@ -170,6 +185,19 @@ impl Handler {
170185 self . hint_tx . subscribe ( )
171186 }
172187
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+
173201 /// Handles a control request and returns a response.
174202 ///
175203 /// # Errors
@@ -419,25 +447,70 @@ impl crate::node_api::NodeApi for Handler {
419447 }
420448 }
421449
422- fn connect ( & self , _addr : & str ) -> crate :: node_api:: Result < crate :: node_api:: ConnectInfo > {
423- Err ( crate :: node_api:: NodeApiError :: NotSupported (
424- "dynamic connect not yet implemented — specify --connect at startup" . into ( ) ,
425- ) )
450+ 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 ( |_| {
468+ crate :: node_api:: NodeApiError :: Internal ( "mode task dropped reply" . into ( ) )
469+ } ) ?
426470 }
427471
428472 fn listen (
429473 & self ,
430- _addr : std:: net:: SocketAddr ,
474+ addr : std:: net:: SocketAddr ,
431475 ) -> crate :: node_api:: Result < crate :: node_api:: ListenInfo > {
432- Err ( crate :: node_api:: NodeApiError :: NotSupported (
433- "dynamic listen not yet implemented — specify --listen at startup" . into ( ) ,
434- ) )
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 ( |_| {
493+ crate :: node_api:: NodeApiError :: Internal ( "mode task dropped reply" . into ( ) )
494+ } ) ?
435495 }
436496
437497 fn disconnect ( & self ) -> crate :: node_api:: Result < ( ) > {
438- Err ( crate :: node_api:: NodeApiError :: NotSupported (
439- "dynamic disconnect not yet implemented" . into ( ) ,
440- ) )
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 ( |_| {
512+ crate :: node_api:: NodeApiError :: Internal ( "mode task dropped reply" . into ( ) )
513+ } ) ?
441514 }
442515
443516 fn add_route (
0 commit comments