66use std:: { net:: SocketAddr , sync:: Arc , time:: Instant } ;
77
88use arc_swap:: ArcSwap ;
9- use tokio:: sync:: watch ;
9+ use tokio:: sync:: mpsc ;
1010use wallhack_wire:: {
1111 control:: {
1212 ControlRequest , ControlResponse , ErrorResponse , PeerInfo , PingResponse , RouteInfo ,
@@ -21,6 +21,51 @@ use super::{
2121 log_buffer:: LogBuffer , metrics:: SharedMetrics , peers:: SharedRegistry , routes:: SharedRouteTable ,
2222} ;
2323
24+ /// Reply channel for a single command.
25+ ///
26+ /// Uses a standard-library sync channel so the handler side (which may be
27+ /// called from a synchronous context) can block waiting for the reply without
28+ /// needing an async runtime handle.
29+ type ReplySender < T > = std:: sync:: mpsc:: SyncSender < Result < T , crate :: node_api:: NodeApiError > > ;
30+
31+ /// Create a reply channel pair for a node command.
32+ fn reply_channel < T > ( ) -> (
33+ ReplySender < T > ,
34+ std:: sync:: mpsc:: Receiver < Result < T , crate :: node_api:: NodeApiError > > ,
35+ ) {
36+ std:: sync:: mpsc:: sync_channel ( 1 )
37+ }
38+
39+ /// A command sent from the handler/API layer to the mode task via the
40+ /// control watch channel.
41+ #[ derive( Debug ) ]
42+ pub enum NodeCommand {
43+ /// Set or clear the role hint.
44+ Role {
45+ /// `Some` to set a hint, `None` to clear (auto).
46+ hint : Option < RoleHint > ,
47+ } ,
48+ /// Connect to a remote peer at the given address.
49+ Connect {
50+ /// Target address (host, host:port, etc.).
51+ addr : String ,
52+ /// Channel for sending the result back to the caller.
53+ reply : ReplySender < crate :: node_api:: ConnectInfo > ,
54+ } ,
55+ /// Start listening for incoming peer connections.
56+ Listen {
57+ /// Address to bind.
58+ addr : SocketAddr ,
59+ /// Channel for sending the result back to the caller.
60+ reply : ReplySender < crate :: node_api:: ListenInfo > ,
61+ } ,
62+ /// Disconnect from the currently connected peer.
63+ Disconnect {
64+ /// Channel for sending the result back to the caller.
65+ reply : ReplySender < ( ) > ,
66+ } ,
67+ }
68+
2469/// Mutable runtime state that can change after construction.
2570///
2671/// Stored behind `ArcSwap` for wait-free reads (same pattern as `Registry`).
@@ -112,9 +157,12 @@ impl HandlerConfig {
112157/// on the current state of metrics and configuration.
113158pub struct Handler {
114159 config : HandlerConfig ,
115- /// Sender for hint changes. The mode task watches the receiver and
116- /// re-evaluates when a new hint arrives. `None` means no hint is active.
117- hint_tx : watch:: Sender < Option < RoleHint > > ,
160+ /// Command channel to the mode task. Carries role changes, connect,
161+ /// listen, and disconnect commands.
162+ command_source : mpsc:: Sender < NodeCommand > ,
163+ /// Receiver side of the command channel. Extracted once by the daemon
164+ /// before wrapping Handler in `Arc<dyn NodeApi>`.
165+ command_sink : std:: sync:: Mutex < Option < mpsc:: Receiver < NodeCommand > > > ,
118166 log_buffer : LogBuffer ,
119167 metrics : SharedMetrics ,
120168 peers : SharedRegistry ,
@@ -140,10 +188,11 @@ impl Handler {
140188 log_buffer : Option < LogBuffer > ,
141189 ) -> Self {
142190 let state = SharedNodeState :: new ( config. node_role ) ;
143- let ( hint_tx , _ ) = watch :: channel ( None ) ;
191+ let ( command_source , command_sink ) = mpsc :: channel ( 8 ) ;
144192 Self {
145193 config,
146- hint_tx,
194+ command_source,
195+ command_sink : std:: sync:: Mutex :: new ( Some ( command_sink) ) ,
147196 log_buffer : log_buffer. unwrap_or_default ( ) ,
148197 metrics,
149198 peers,
@@ -164,10 +213,19 @@ impl Handler {
164213 self . state . clone ( )
165214 }
166215
167- /// Returns a receiver that fires when the runtime hint changes.
216+ /// Extracts the command receiver. Called once by the daemon before
217+ /// wrapping Handler in `Arc<dyn NodeApi>`.
218+ ///
219+ /// # Panics
220+ ///
221+ /// Panics if the mutex is poisoned or if called more than once.
168222 #[ must_use]
169- pub fn hint_rx ( & self ) -> watch:: Receiver < Option < RoleHint > > {
170- self . hint_tx . subscribe ( )
223+ pub fn command_sink ( & self ) -> mpsc:: Receiver < NodeCommand > {
224+ self . command_sink
225+ . lock ( )
226+ . expect ( "command_sink mutex poisoned" )
227+ . take ( )
228+ . expect ( "command_sink already taken" )
171229 }
172230
173231 /// Handles a control request and returns a response.
@@ -419,25 +477,57 @@ impl crate::node_api::NodeApi for Handler {
419477 }
420478 }
421479
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- ) )
480+ fn connect ( & self , addr : & str ) -> crate :: node_api:: Result < crate :: node_api:: ConnectInfo > {
481+ let ( reply_sender, reply_receiver) = reply_channel ( ) ;
482+ self . command_source
483+ . try_send ( NodeCommand :: Connect {
484+ addr : addr. to_string ( ) ,
485+ reply : reply_sender,
486+ } )
487+ . map_err ( |_| {
488+ crate :: node_api:: NodeApiError :: NotSupported (
489+ "dynamic connect not supported in this mode" . into ( ) ,
490+ )
491+ } ) ?;
492+ tokio:: task:: block_in_place ( || reply_receiver. recv ( ) ) . map_err ( |_| {
493+ crate :: node_api:: NodeApiError :: Internal ( "mode task dropped reply" . into ( ) )
494+ } ) ?
426495 }
427496
428497 fn listen (
429498 & self ,
430- _addr : std:: net:: SocketAddr ,
499+ addr : std:: net:: SocketAddr ,
431500 ) -> 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- ) )
501+ let ( reply_sender, reply_receiver) = reply_channel ( ) ;
502+ self . command_source
503+ . try_send ( NodeCommand :: Listen {
504+ addr,
505+ reply : reply_sender,
506+ } )
507+ . map_err ( |_| {
508+ crate :: node_api:: NodeApiError :: NotSupported (
509+ "dynamic listen not supported in this mode" . into ( ) ,
510+ )
511+ } ) ?;
512+ tokio:: task:: block_in_place ( || reply_receiver. recv ( ) ) . map_err ( |_| {
513+ crate :: node_api:: NodeApiError :: Internal ( "mode task dropped reply" . into ( ) )
514+ } ) ?
435515 }
436516
437517 fn disconnect ( & self ) -> crate :: node_api:: Result < ( ) > {
438- Err ( crate :: node_api:: NodeApiError :: NotSupported (
439- "dynamic disconnect not yet implemented" . into ( ) ,
440- ) )
518+ let ( reply_sender, reply_receiver) = reply_channel ( ) ;
519+ self . command_source
520+ . try_send ( NodeCommand :: Disconnect {
521+ reply : reply_sender,
522+ } )
523+ . map_err ( |_| {
524+ crate :: node_api:: NodeApiError :: NotSupported (
525+ "dynamic disconnect not supported in this mode" . into ( ) ,
526+ )
527+ } ) ?;
528+ tokio:: task:: block_in_place ( || reply_receiver. recv ( ) ) . map_err ( |_| {
529+ crate :: node_api:: NodeApiError :: Internal ( "mode task dropped reply" . into ( ) )
530+ } ) ?
441531 }
442532
443533 fn add_route (
@@ -509,12 +599,16 @@ impl crate::node_api::NodeApi for Handler {
509599 }
510600
511601 fn hint_set ( & self , hint : RoleHint ) -> crate :: node_api:: Result < ( ) > {
512- self . hint_tx . send_replace ( Some ( hint) ) ;
602+ let _ = self
603+ . command_source
604+ . try_send ( NodeCommand :: Role { hint : Some ( hint) } ) ;
513605 Ok ( ( ) )
514606 }
515607
516608 fn hint_set_auto ( & self ) -> crate :: node_api:: Result < ( ) > {
517- self . hint_tx . send_replace ( None ) ;
609+ let _ = self
610+ . command_source
611+ . try_send ( NodeCommand :: Role { hint : None } ) ;
518612 Ok ( ( ) )
519613 }
520614
0 commit comments