88use std:: io;
99use std:: path:: PathBuf ;
1010use std:: sync:: atomic:: { AtomicBool , Ordering } ;
11+ use std:: sync:: mpsc;
1112use std:: sync:: Arc ;
1213use std:: time:: { Duration , Instant } ;
1314
@@ -22,7 +23,7 @@ use crate::server::capture_backend::{
2223 terminal_setup_sequences, terminal_teardown_sequences, CaptureBackend ,
2324} ;
2425use crate :: server:: input_parser:: InputParser ;
25- use crate :: server:: ipc:: { ServerConnection , ServerListener , SocketPaths } ;
26+ use crate :: server:: ipc:: { ServerConnection , ServerListener , SocketPaths , StreamWrapper } ;
2627use crate :: server:: protocol:: {
2728 ClientControl , ServerControl , ServerHello , TermSize , VersionMismatch , PROTOCOL_VERSION ,
2829} ;
@@ -60,9 +61,67 @@ pub struct EditorServer {
6061 last_input_client : Option < usize > ,
6162}
6263
64+ /// Buffered writer for sending data to a client without blocking the server loop.
65+ ///
66+ /// Spawns a background thread that receives data via a bounded channel and
67+ /// writes it to the client's data pipe. If the channel fills up (client is
68+ /// too slow to read), frames are dropped. If the pipe breaks, the `pipe_broken`
69+ /// flag is set so the main loop can disconnect the client.
70+ struct ClientDataWriter {
71+ sender : mpsc:: SyncSender < Vec < u8 > > ,
72+ pipe_broken : Arc < AtomicBool > ,
73+ }
74+
75+ impl ClientDataWriter {
76+ /// Create a new writer that spawns a background thread to write to the data stream.
77+ fn new ( data : StreamWrapper , client_id : u64 ) -> Self {
78+ // 16 frames of buffer (~270ms at 60fps before dropping frames)
79+ let ( tx, rx) = mpsc:: sync_channel :: < Vec < u8 > > ( 16 ) ;
80+ let pipe_broken = Arc :: new ( AtomicBool :: new ( false ) ) ;
81+ let pipe_broken_clone = pipe_broken. clone ( ) ;
82+
83+ std:: thread:: Builder :: new ( )
84+ . name ( format ! ( "client-{}-writer" , client_id) )
85+ . spawn ( move || {
86+ while let Ok ( buf) = rx. recv ( ) {
87+ if let Err ( e) = data. write_all ( & buf) {
88+ tracing:: debug!( "Client {} writer pipe error: {}" , client_id, e) ;
89+ pipe_broken_clone. store ( true , Ordering :: Relaxed ) ;
90+ break ;
91+ }
92+ if let Err ( e) = data. flush ( ) {
93+ tracing:: debug!( "Client {} writer flush error: {}" , client_id, e) ;
94+ pipe_broken_clone. store ( true , Ordering :: Relaxed ) ;
95+ break ;
96+ }
97+ }
98+ tracing:: debug!( "Client {} writer thread exiting" , client_id) ;
99+ } )
100+ . expect ( "Failed to spawn client writer thread" ) ;
101+
102+ Self {
103+ sender : tx,
104+ pipe_broken,
105+ }
106+ }
107+
108+ /// Try to send data without blocking. Returns false if the channel is full
109+ /// (client too slow) or the writer thread has exited.
110+ fn try_write ( & self , data : & [ u8 ] ) -> bool {
111+ self . sender . try_send ( data. to_vec ( ) ) . is_ok ( )
112+ }
113+
114+ /// Check if the writer thread detected a broken pipe.
115+ fn is_broken ( & self ) -> bool {
116+ self . pipe_broken . load ( Ordering :: Relaxed )
117+ }
118+ }
119+
63120/// A connected client with its own input parser
64121struct ConnectedClient {
65122 conn : ServerConnection ,
123+ /// Background writer for non-blocking data output
124+ data_writer : ClientDataWriter ,
66125 term_size : TermSize ,
67126 env : std:: collections:: HashMap < String , Option < String > > ,
68127 id : u64 ,
@@ -214,7 +273,7 @@ impl EditorServer {
214273 tracing:: info!( "Client {} requested detach" , self . clients[ idx] . id) ;
215274 let client = self . clients . remove ( idx) ;
216275 let teardown = terminal_teardown_sequences ( ) ;
217- let _ = client. conn . write_data ( & teardown) ;
276+ let _ = client. data_writer . try_write ( & teardown) ;
218277 let quit_msg = serde_json:: to_string ( & ServerControl :: Quit {
219278 reason : "Detached" . to_string ( ) ,
220279 } )
@@ -406,8 +465,12 @@ impl EditorServer {
406465 hello. term( )
407466 ) ;
408467
468+ // Create background writer for non-blocking render output
469+ let data_writer = ClientDataWriter :: new ( conn. data . clone ( ) , client_id) ;
470+
409471 Ok ( ConnectedClient {
410472 conn,
473+ data_writer,
411474 term_size : hello. term_size ,
412475 env : hello. env ,
413476 id : client_id,
@@ -585,12 +648,24 @@ impl EditorServer {
585648 }
586649 }
587650
651+ // Check for clients with broken write pipes
652+ for ( idx, client) in self . clients . iter ( ) . enumerate ( ) {
653+ if client. data_writer . is_broken ( ) && !disconnected. contains ( & idx) {
654+ tracing:: info!( "Client {} write pipe broken, disconnecting" , client. id) ;
655+ disconnected. push ( idx) ;
656+ }
657+ }
658+
659+ // Deduplicate and sort for safe reverse removal
660+ disconnected. sort_unstable ( ) ;
661+ disconnected. dedup ( ) ;
662+
588663 // Remove disconnected clients
589664 for idx in disconnected. into_iter ( ) . rev ( ) {
590665 let client = self . clients . remove ( idx) ;
591- // Send teardown sequences
666+ // Best-effort teardown via the non-blocking writer
592667 let teardown = terminal_teardown_sequences ( ) ;
593- let _ = client. conn . write_data ( & teardown) ;
668+ let _ = client. data_writer . try_write ( & teardown) ;
594669 tracing:: info!( "Client {} disconnected" , client. id) ;
595670 // Invalidate input source if that client disconnected
596671 if input_source_client == Some ( idx) {
@@ -684,21 +759,22 @@ impl EditorServer {
684759 return Ok ( ( ) ) ;
685760 }
686761
687- // Broadcast to all clients (pending sequences first, then rendered output)
762+ // Broadcast to all clients via non-blocking writer threads
688763 for client in & mut self . clients {
689- if !pending_sequences. is_empty ( ) {
690- if let Err ( e) = client. conn . write_data ( & pending_sequences) {
691- tracing:: warn!(
692- "Failed to send pending sequences to client {}: {}" ,
693- client. id,
694- e
695- ) ;
696- }
697- }
698- if !output. is_empty ( ) {
699- if let Err ( e) = client. conn . write_data ( & output) {
700- tracing:: warn!( "Failed to send to client {}: {}" , client. id, e) ;
701- }
764+ // Combine pending sequences and output into a single frame
765+ let frame = if !pending_sequences. is_empty ( ) && !output. is_empty ( ) {
766+ let mut combined = Vec :: with_capacity ( pending_sequences. len ( ) + output. len ( ) ) ;
767+ combined. extend_from_slice ( & pending_sequences) ;
768+ combined. extend_from_slice ( & output) ;
769+ combined
770+ } else if !pending_sequences. is_empty ( ) {
771+ pending_sequences. clone ( )
772+ } else {
773+ output. clone ( )
774+ } ;
775+
776+ if !frame. is_empty ( ) && !client. data_writer . try_write ( & frame) {
777+ tracing:: warn!( "Client {} output buffer full, dropping frame" , client. id) ;
702778 }
703779 // Clear full render flag after sending
704780 client. needs_full_render = false ;
@@ -711,7 +787,7 @@ impl EditorServer {
711787 fn disconnect_all_clients ( & mut self , reason : & str ) -> io:: Result < ( ) > {
712788 let teardown = terminal_teardown_sequences ( ) ;
713789 for client in & mut self . clients {
714- let _ = client. conn . write_data ( & teardown) ;
790+ let _ = client. data_writer . try_write ( & teardown) ;
715791 let quit_msg = serde_json:: to_string ( & ServerControl :: Quit {
716792 reason : reason. to_string ( ) ,
717793 } )
0 commit comments