aboutsummaryrefslogtreecommitdiff
path: root/src/server
diff options
context:
space:
mode:
Diffstat (limited to 'src/server')
-rw-r--r--src/server/client_transport.rs197
-rw-r--r--src/server/clients.rs17
-rw-r--r--src/server/headless.rs153
3 files changed, 301 insertions, 66 deletions
diff --git a/src/server/client_transport.rs b/src/server/client_transport.rs
index 36334fda..931f1100 100644
--- a/src/server/client_transport.rs
+++ b/src/server/client_transport.rs
@@ -14,8 +14,8 @@ use tokio::sync::mpsc;
use tracing::{debug, warn};
use crate::protocol::{
- self, AttachScrollDirection, AttachScrollSource, ClientKeybindings, ClientMessage,
- RenderEncoding, ServerMessage, MAX_CLIPBOARD_IMAGE_PAYLOAD, MAX_FRAME_SIZE,
+ self, AttachScrollDirection, AttachScrollSource, ClientKeybindings, ClientLaunchMode,
+ ClientMessage, RenderEncoding, ServerMessage, MAX_CLIPBOARD_IMAGE_PAYLOAD, MAX_FRAME_SIZE,
MAX_GRAPHICS_FRAME_SIZE, PROTOCOL_VERSION,
};
@@ -57,6 +57,7 @@ pub(crate) enum ServerEvent {
cell_height_px: u32,
render_encoding: RenderEncoding,
keybindings: Option<Box<crate::config::LiveKeybindConfig>>,
+ direct_attach_requested: bool,
writer: ClientWriter,
},
/// A client sent an input message.
@@ -159,68 +160,77 @@ pub(crate) fn handle_client_handshake(
}
};
- let (client_cols, client_rows, cell_width_px, cell_height_px, render_encoding, keybindings) =
- match hello {
- ClientMessage::Hello {
- version,
- cols,
- rows,
+ let (
+ client_cols,
+ client_rows,
+ cell_width_px,
+ cell_height_px,
+ render_encoding,
+ keybindings,
+ direct_attach_requested,
+ ) = match hello {
+ ClientMessage::Hello {
+ version,
+ cols,
+ rows,
+ cell_width_px,
+ cell_height_px,
+ requested_encoding,
+ keybindings,
+ launch_mode,
+ } => {
+ // Version check.
+ match protocol::check_client_version(version) {
+ protocol::VersionCheck::Compatible => {}
+ protocol::VersionCheck::Incompatible(reason) => {
+ // Send rejection Welcome.
+ let welcome = ServerMessage::Welcome {
+ version: PROTOCOL_VERSION,
+ encoding: RenderEncoding::SemanticFrame,
+ error: Some(reason),
+ };
+ let _ = protocol::write_message(&mut stream, &welcome);
+ return Ok(());
+ }
+ }
+
+ let keybindings = match parse_client_keybindings(keybindings) {
+ Ok(keybindings) => keybindings,
+ Err(error) => {
+ let welcome = ServerMessage::Welcome {
+ version: PROTOCOL_VERSION,
+ encoding: RenderEncoding::SemanticFrame,
+ error: Some(error),
+ };
+ let _ = protocol::write_message(&mut stream, &welcome);
+ return Ok(());
+ }
+ };
+
+ // Clamp size.
+ let (clamped_cols, clamped_rows) = clamp_terminal_size(cols, rows);
+ (
+ clamped_cols,
+ clamped_rows,
cell_width_px,
cell_height_px,
requested_encoding,
keybindings,
- } => {
- // Version check.
- match protocol::check_client_version(version) {
- protocol::VersionCheck::Compatible => {}
- protocol::VersionCheck::Incompatible(reason) => {
- // Send rejection Welcome.
- let welcome = ServerMessage::Welcome {
- version: PROTOCOL_VERSION,
- encoding: RenderEncoding::SemanticFrame,
- error: Some(reason),
- };
- let _ = protocol::write_message(&mut stream, &welcome);
- return Ok(());
- }
- }
-
- let keybindings = match parse_client_keybindings(keybindings) {
- Ok(keybindings) => keybindings,
- Err(error) => {
- let welcome = ServerMessage::Welcome {
- version: PROTOCOL_VERSION,
- encoding: RenderEncoding::SemanticFrame,
- error: Some(error),
- };
- let _ = protocol::write_message(&mut stream, &welcome);
- return Ok(());
- }
- };
-
- // Clamp size.
- let (clamped_cols, clamped_rows) = clamp_terminal_size(cols, rows);
- (
- clamped_cols,
- clamped_rows,
- cell_width_px,
- cell_height_px,
- requested_encoding,
- keybindings,
- )
- }
- _ => {
- // First message must be Hello.
- debug!(client_id, "first message was not Hello, closing");
- let welcome = ServerMessage::Welcome {
- version: PROTOCOL_VERSION,
- encoding: RenderEncoding::SemanticFrame,
- error: Some("expected Hello as first message".to_owned()),
- };
- let _ = protocol::write_message(&mut stream, &welcome);
- return Ok(());
- }
- };
+ launch_mode == ClientLaunchMode::TerminalAttach,
+ )
+ }
+ _ => {
+ // First message must be Hello.
+ debug!(client_id, "first message was not Hello, closing");
+ let welcome = ServerMessage::Welcome {
+ version: PROTOCOL_VERSION,
+ encoding: RenderEncoding::SemanticFrame,
+ error: Some("expected Hello as first message".to_owned()),
+ };
+ let _ = protocol::write_message(&mut stream, &welcome);
+ return Ok(());
+ }
+ };
// Send Welcome.
let welcome = ServerMessage::Welcome {
@@ -250,6 +260,7 @@ pub(crate) fn handle_client_handshake(
cell_height_px,
render_encoding,
keybindings,
+ direct_attach_requested,
writer,
});
@@ -576,6 +587,7 @@ new_tab = "ctrl+notakey"
cell_height_px: 16,
requested_encoding: RenderEncoding::TerminalAnsi,
keybindings: ClientKeybindings::Server,
+ launch_mode: ClientLaunchMode::App,
},
)
.expect("write hello");
@@ -607,6 +619,7 @@ new_tab = "ctrl+notakey"
cell_height_px,
render_encoding,
keybindings,
+ direct_attach_requested,
writer,
} => {
assert_eq!(client_id, 42);
@@ -614,6 +627,70 @@ new_tab = "ctrl+notakey"
assert_eq!((cell_width_px, cell_height_px), (8, 16));
assert_eq!(render_encoding, RenderEncoding::TerminalAnsi);
assert!(keybindings.is_none());
+ assert!(!direct_attach_requested);
+ drop(writer);
+ }
+ other => panic!("expected ClientConnected, got {other:?}"),
+ }
+
+ drop(client_stream);
+ should_quit.store(true, Ordering::Release);
+ handle
+ .join()
+ .expect("handshake thread join")
+ .expect("handshake thread result");
+ }
+
+ #[test]
+ fn handshake_marks_terminal_attach_launch_mode() {
+ let (mut client_stream, server_stream) = UnixStream::pair().expect("socket pair");
+ let (server_event_tx, mut server_event_rx) = mpsc::channel(4);
+ let should_quit = Arc::new(AtomicBool::new(false));
+ let handshake_quit = should_quit.clone();
+ let handle = std::thread::spawn(move || {
+ handle_client_handshake(server_stream, 42, &server_event_tx, &handshake_quit)
+ });
+
+ protocol::write_message(
+ &mut client_stream,
+ &ClientMessage::Hello {
+ version: PROTOCOL_VERSION,
+ cols: 100,
+ rows: 30,
+ cell_width_px: 8,
+ cell_height_px: 16,
+ requested_encoding: RenderEncoding::TerminalAnsi,
+ keybindings: ClientKeybindings::Server,
+ launch_mode: ClientLaunchMode::TerminalAttach,
+ },
+ )
+ .expect("write hello");
+
+ let welcome: ServerMessage =
+ protocol::read_message(&mut client_stream, MAX_FRAME_SIZE).expect("read welcome");
+ match welcome {
+ ServerMessage::Welcome {
+ version,
+ encoding,
+ error,
+ } => {
+ assert_eq!(version, PROTOCOL_VERSION);
+ assert_eq!(encoding, RenderEncoding::TerminalAnsi);
+ assert_eq!(error, None);
+ }
+ other => panic!("expected Welcome, got {other:?}"),
+ }
+
+ match server_event_rx
+ .blocking_recv()
+ .expect("client connected event")
+ {
+ ServerEvent::ClientConnected {
+ direct_attach_requested,
+ writer,
+ ..
+ } => {
+ assert!(direct_attach_requested);
drop(writer);
}
other => panic!("expected ClientConnected, got {other:?}"),
diff --git a/src/server/clients.rs b/src/server/clients.rs
index 026dd378..611d87eb 100644
--- a/src/server/clients.rs
+++ b/src/server/clients.rs
@@ -23,6 +23,8 @@ pub(crate) type RenderTarget = (
pub(crate) struct ClientConnection {
/// Whether this connection is the full app client or a direct terminal attach.
pub(crate) mode: ClientConnectionMode,
+ /// True after the handshake for clients that will switch into direct terminal attach mode.
+ pub(crate) pending_terminal_attach: bool,
/// Client-local app keybindings. None means use the server's keybindings.
pub(crate) keybindings: Option<Box<crate::config::LiveKeybindConfig>>,
/// The client's terminal size after clamping.
@@ -73,6 +75,7 @@ impl ClientConnection {
outer_terminal_focus,
last_activity,
render_encoding,
+ false,
writer,
)
}
@@ -86,10 +89,12 @@ impl ClientConnection {
outer_terminal_focus: Option<bool>,
last_activity: u64,
render_encoding: RenderEncoding,
+ pending_terminal_attach: bool,
writer: Option<ClientWriter>,
) -> Self {
Self {
mode,
+ pending_terminal_attach,
keybindings,
terminal_size,
cell_size,
@@ -112,6 +117,10 @@ impl ClientConnection {
self.graphics_surface_reset_pending = true;
}
+ pub(crate) fn is_full_app_client(&self) -> bool {
+ matches!(self.mode, ClientConnectionMode::App) && !self.pending_terminal_attach
+ }
+
pub(crate) fn request_semantic_redraw_after_input(&mut self) {
self.render_state.reset_semantic_input_baseline();
}
@@ -168,7 +177,7 @@ pub(crate) fn events_include_interaction(events: &[crate::raw_input::RawInputEve
pub(crate) fn latest_app_client(clients: &HashMap<u64, ClientConnection>) -> Option<u64> {
clients
.iter()
- .filter(|(_, client)| matches!(client.mode, ClientConnectionMode::App))
+ .filter(|(_, client)| client.is_full_app_client())
.max_by_key(|(_, client)| client.last_activity)
.map(|(&client_id, _)| client_id)
}
@@ -194,7 +203,11 @@ pub(crate) fn render_targets(
) -> Vec<RenderTarget> {
let mut targets: Vec<RenderTarget> = clients
.iter()
- .filter(|(_, client)| client.writer.is_some())
+ .filter(|(_, client)| {
+ client.writer.is_some()
+ && (client.is_full_app_client()
+ || matches!(client.mode, ClientConnectionMode::TerminalAttach { .. }))
+ })
.map(|(&client_id, client)| {
(
client_id,
diff --git a/src/server/headless.rs b/src/server/headless.rs
index f59201e0..3d273b1c 100644
--- a/src/server/headless.rs
+++ b/src/server/headless.rs
@@ -423,7 +423,11 @@ impl HeadlessServer {
// 8. Wait for next event.
let next_deadline = self
.app
- .next_headless_loop_deadline(now, needs_render)
+ .next_headless_loop_deadline_with_git_refresh(
+ now,
+ needs_render,
+ self.has_app_client(),
+ )
.map(|deadline| deadline.min(now + CLIENT_ACCEPT_POLL_INTERVAL))
.or(Some(now + CLIENT_ACCEPT_POLL_INTERVAL));
let event = {
@@ -901,6 +905,17 @@ impl HeadlessServer {
changed
}
+ fn app_client_count(&self) -> usize {
+ self.clients
+ .values()
+ .filter(|client| client.is_full_app_client() && client.writer.is_some())
+ .count()
+ }
+
+ fn has_app_client(&self) -> bool {
+ self.app_client_count() > 0
+ }
+
fn remove_client(&mut self, client_id: u64) -> bool {
let was_foreground = self.foreground_client_id == Some(client_id);
self.send_client_graphics_cleanup(client_id);
@@ -1619,6 +1634,7 @@ impl HeadlessServer {
client.mode = ClientConnectionMode::TerminalAttach {
terminal_id: terminal_id.clone(),
};
+ client.pending_terminal_attach = false;
client.render_state.reset_baseline();
client.last_activity = stamp;
let was_foreground = self.foreground_client_id == Some(client_id);
@@ -1655,6 +1671,7 @@ impl HeadlessServer {
keybindings,
writer,
render_encoding,
+ direct_attach_requested,
} => {
if self.handoff_in_progress {
if let Ok(message) =
@@ -1669,6 +1686,7 @@ impl HeadlessServer {
}
return false;
}
+ let first_app_client = !direct_attach_requested && self.app_client_count() == 0;
info!(
client_id,
cols,
@@ -1693,10 +1711,16 @@ impl HeadlessServer {
None,
last_activity,
render_encoding,
+ direct_attach_requested,
Some(writer),
),
);
- self.foreground_client_id = Some(client_id);
+ if !direct_attach_requested {
+ self.foreground_client_id = Some(client_id);
+ }
+ if first_app_client {
+ self.app.mark_git_status_refresh_due(Instant::now());
+ }
self.sync_foreground_client_state();
self.resize_shared_runtime_to_effective_size();
self.nudge_handoff_panes_on_first_client_attach();
@@ -2190,7 +2214,7 @@ impl HeadlessServer {
let mut broken_clients: Vec<u64> = Vec::new();
for (&client_id, client) in &mut self.clients {
- if !matches!(client.mode, ClientConnectionMode::App) {
+ if !client.is_full_app_client() {
continue;
}
if client.host_mouse_capture_active == Some(enabled) {
@@ -2484,7 +2508,9 @@ impl HeadlessServer {
changed |= self.app.clear_due_selection_highlight(now);
- self.app.start_git_status_refresh_if_due(now);
+ if self.has_app_client() {
+ self.app.start_git_status_refresh_if_due(now);
+ }
if self
.app
@@ -3011,6 +3037,7 @@ new_tab = "prefix+t"
cell_height_px: 0,
render_encoding: RenderEncoding::SemanticFrame,
keybindings: Some(Box::new(local_keybindings)),
+ direct_attach_requested: false,
writer: writer_a,
}));
assert_eq!(
@@ -3034,6 +3061,7 @@ new_tab = "prefix+t"
cell_height_px: 0,
render_encoding: RenderEncoding::SemanticFrame,
keybindings: None,
+ direct_attach_requested: false,
writer: writer_b,
}));
assert_eq!(
@@ -3073,6 +3101,7 @@ new_tab = "prefix+t"
cell_height_px: 0,
render_encoding: RenderEncoding::SemanticFrame,
keybindings: Some(Box::new(local_keybindings)),
+ direct_attach_requested: false,
writer: writer_a,
}));
assert_eq!(server.app.state.config_diagnostic, without_keybindings);
@@ -3085,6 +3114,7 @@ new_tab = "prefix+t"
cell_height_px: 0,
render_encoding: RenderEncoding::SemanticFrame,
keybindings: None,
+ direct_attach_requested: false,
writer: writer_b,
}));
assert_eq!(
@@ -3127,6 +3157,7 @@ next_tab = ""
cell_height_px: 0,
render_encoding: RenderEncoding::SemanticFrame,
keybindings: Some(Box::new(local_keybindings)),
+ direct_attach_requested: false,
writer,
}));
server.app.state.mode = crate::app::Mode::Settings;
@@ -3200,6 +3231,7 @@ next_tab = ""
cell_height_px: 0,
render_encoding: RenderEncoding::SemanticFrame,
keybindings: Some(Box::new(local_config.live_keybinds().unwrap())),
+ direct_attach_requested: false,
writer: writer_a,
}));
server.app.state.mode = crate::app::Mode::Settings;
@@ -3219,6 +3251,7 @@ next_tab = ""
cell_height_px: 0,
render_encoding: RenderEncoding::SemanticFrame,
keybindings: None,
+ direct_attach_requested: false,
writer: writer_b,
}));
assert_eq!(
@@ -3251,6 +3284,7 @@ next_tab = ""
cell_height_px: 0,
render_encoding: RenderEncoding::TerminalAnsi,
keybindings: None,
+ direct_attach_requested: true,
writer,
}));
assert!(server.clients.contains_key(&7));
@@ -3270,6 +3304,115 @@ next_tab = ""
);
}
+ fn app_client_marks_git_refresh_due_on_first_attach(render_encoding: RenderEncoding) {
+ let mut server = test_headless_server();
+ server
+ .app
+ .state
+ .workspaces
+ .push(crate::workspace::Workspace::test_new("test"));
+ let future = Instant::now() + Duration::from_secs(60);
+ server.app.last_git_remote_status_refresh = future;
+ let (writer, _control_rx, _render_rx) = test_client_writer();
+
+ assert!(server.handle_server_event(ServerEvent::ClientConnected {
+ client_id: 7,
+ cols: 80,
+ rows: 24,
+ cell_width_px: 0,
+ cell_height_px: 0,
+ render_encoding,
+ keybindings: None,
+ direct_attach_requested: false,
+ writer,
+ }));
+
+ assert!(server.has_app_client());
+ assert!(server
+ .app
+ .git_refresh_deadline()
+ .is_some_and(|deadline| deadline <= Instant::now()));
+ }
+
+ #[test]
+ fn terminal_ansi_app_client_enables_headless_git_refresh() {
+ app_client_marks_git_refresh_due_on_first_attach(RenderEncoding::TerminalAnsi);
+ }
+
+ #[test]
+ fn pending_terminal_attach_client_does_not_enable_headless_git_refresh() {
+ let mut server = test_headless_server();
+ server
+ .app
+ .state
+ .workspaces
+ .push(crate::workspace::Workspace::test_new("test"));
+ let (writer, _control_rx, _render_rx) = test_client_writer();
+
+ assert!(server.handle_server_event(ServerEvent::ClientConnected {
+ client_id: 7,
+ cols: 80,
+ rows: 24,
+ cell_width_px: 0,
+ cell_height_px: 0,
+ render_encoding: RenderEncoding::TerminalAnsi,
+ keybindings: None,
+ direct_attach_requested: true,
+ writer,
+ }));
+
+ assert!(!server.has_app_client());
+ assert_eq!(
+ server.app.next_headless_loop_deadline_with_git_refresh(
+ Instant::now(),
+ false,
+ server.has_app_client()
+ ),
+ None
+ );
+ }
+
+ #[test]
+ fn writerless_app_client_does_not_enable_headless_git_refresh() {
+ let mut server = test_headless_server();
+ server
+ .app
+ .state
+ .workspaces
+ .push(crate::workspace::Workspace::test_new("test"));
+ let (writer, _control_rx, _render_rx) = test_client_writer();
+
+ assert!(server.handle_server_event(ServerEvent::ClientConnected {
+ client_id: 7,
+ cols: 80,
+ rows: 24,
+ cell_width_px: 0,
+ cell_height_px: 0,
+ render_encoding: RenderEncoding::SemanticFrame,
+ keybindings: None,
+ direct_attach_requested: false,
+ writer,
+ }));
+ assert!(server.has_app_client());
+
+ server.clients.get_mut(&7).expect("client").writer = None;
+
+ assert!(!server.has_app_client());
+ assert_eq!(
+ server.app.next_headless_loop_deadline_with_git_refresh(
+ Instant::now(),
+ false,
+ server.has_app_client()
+ ),
+ None
+ );
+ }
+
+ #[test]
+ fn semantic_app_client_marks_git_refresh_due_on_first_attach() {
+ app_client_marks_git_refresh_due_on_first_attach(RenderEncoding::SemanticFrame);
+ }
+
#[test]
fn terminal_attach_client_exits_when_attached_pane_dies() {
let mut server = test_headless_server();
@@ -3292,6 +3435,7 @@ next_tab = ""
cell_height_px: 0,
render_encoding: RenderEncoding::TerminalAnsi,
keybindings: None,
+ direct_attach_requested: true,
writer,
}));
assert!(
@@ -4334,6 +4478,7 @@ next_tab = ""
cell_height_px: 0,
render_encoding: RenderEncoding::TerminalAnsi,
keybindings: None,
+ direct_attach_requested: true,
writer,
}));
assert!(