From 3e470a43dcdbe548d91429bf63d6a9488efdd7c5 Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Thu, 24 Sep 2026 18:52:13 +0200 Subject: [PATCH] fix: stop host terminal replies from leaking into the shell prompt (#5655) * fix: stop host terminal replies from leaking into the shell prompt as text * add pr --- CHANGELOG.md | 1 + zellij-client/src/lib.rs | 79 +++++---- zellij-client/src/stdin_ansi_parser.rs | 154 ++++++++++++++---- zellij-client/src/stdin_ansi_parser_tests.rs | 131 +++++++++++++++ zellij-client/src/stdin_handler.rs | 49 ++++-- .../src/unit/ipc_pipe_length_tests.rs | 40 +++++ zellij-client/src/unit/mod.rs | 3 + zellij-integration-tests/src/client_screen.rs | 79 +++++++++ .../src/fake_client_os_api.rs | 3 +- zellij-integration-tests/src/lib.rs | 1 + zellij-integration-tests/src/runner.rs | 17 +- .../tests/kitty_graphics.rs | 10 +- .../tests/nested_sessions.rs | 10 +- ...scended_after_clicking_new_tab_button.snap | 2 +- .../tests/startup_host_query.rs | 72 +++++++- zellij-integration-tests/tests/tabs.rs | 1 + zellij-server/src/route.rs | 63 +++++-- 17 files changed, 613 insertions(+), 102 deletions(-) create mode 100644 zellij-client/src/unit/ipc_pipe_length_tests.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 348faee61..22f19f818 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) * feat: distributions -> allow using Zellij as a library (https://github.com/zellij-org/zellij/pull/5630) * fix: do not reuse client ids (https://github.com/zellij-org/zellij/pull/5630) * feat: nested sessions input-mode + keybindings reporting (https://github.com/zellij-org/zellij/issues/5561) +* fix: control characters occasionally appearing in panes on startup (https://github.com/zellij-org/zellij/pull/5655) * fix: group bar-plugins on viewport edge for layout purposes (https://github.com/zellij-org/zellij/pull/5653) ## [0.45.1] - 2026-08-28 diff --git a/zellij-client/src/lib.rs b/zellij-client/src/lib.rs index 9cd63c173..2890b8ff4 100644 --- a/zellij-client/src/lib.rs +++ b/zellij-client/src/lib.rs @@ -354,21 +354,32 @@ fn spawn_web_server(_cli_args: &CliArgs) -> Result { Ok("".to_owned()) } -fn check_ipc_pipe_length(ipc_pipe: &Path) { +fn ipc_pipe_length_error(ipc_pipe: &Path) -> Option { use zellij_utils::consts::ZELLIJ_SOCK_MAX_LENGTH; let path_len = ipc_pipe.as_os_str().len(); - if path_len >= ZELLIJ_SOCK_MAX_LENGTH { - eprintln!( - "Error: the IPC socket path is too long ({} bytes, max {}):\n {}\n\n\ - This is usually caused by a long $TMPDIR path.\n\ - To fix this, set a shorter socket directory, eg.:\n \ - ZELLIJ_SOCKET_DIR=/tmp/{} {}", - path_len, - ZELLIJ_SOCK_MAX_LENGTH - 1, - ipc_pipe.display(), - zellij_utils::distribution::name(), - zellij_utils::distribution::name() - ); + if path_len < ZELLIJ_SOCK_MAX_LENGTH { + return None; + } + Some(format!( + "Error: the IPC socket path is too long ({} bytes, max {}):\n {}\n\n\ + This is usually caused by a long $TMPDIR path.\n\ + To fix this, set a shorter socket directory, eg.:\n \ + ZELLIJ_SOCKET_DIR=/tmp/{} {}", + path_len, + ZELLIJ_SOCK_MAX_LENGTH - 1, + ipc_pipe.display(), + zellij_utils::distribution::name(), + zellij_utils::distribution::name() + )) +} + +fn session_ipc_pipe_length_error(sock_dir: &Path, session_name: &str) -> Option { + ipc_pipe_length_error(&sock_dir.join(session_name)) +} + +fn check_ipc_pipe_length(ipc_pipe: &Path) { + if let Some(message) = ipc_pipe_length_error(ipc_pipe) { + eprintln!("{}", message); std::process::exit(1); } } @@ -464,8 +475,9 @@ fn create_ipc_pipe(teardown: Option) -> PathBuf { ), ); } - sock_dir.push(envs::get_session_name().unwrap()); - check_ipc_pipe_length(&sock_dir); + let session_name = envs::get_session_name().unwrap(); + check_ipc_pipe_length(&sock_dir.join(&session_name)); + sock_dir.push(session_name); sock_dir } @@ -972,6 +984,10 @@ pub fn start_client( info!("Starting Zellij client!"); let own_session_name = info.get_session_name().to_owned(); + if let Some(message) = session_ipc_pipe_length_error(&ZELLIJ_SOCK_DIR, &own_session_name) { + eprintln!("{}", message); + std::process::exit(1); + } let explicitly_disable_kitty_keyboard_protocol = config_options .support_kitty_keyboard_protocol @@ -1005,7 +1021,6 @@ pub fn start_client( stdout .write_all(ENABLE_HOST_THEME_NOTIFY.as_bytes()) .unwrap(); - stdout.write_all(QUERY_HOST_THEME.as_bytes()).unwrap(); } envs::set_zellij("0".to_string()); config.env.set_vars(); @@ -1263,6 +1278,7 @@ pub fn start_client( stdin_ansi_parser, explicitly_disable_kitty_keyboard_protocol, support_kitty_graphics_protocol, + !is_a_reconnect, Some(resize_sender), ) } @@ -1312,18 +1328,28 @@ pub fn start_client( .name("signal_listener".to_string()) .spawn({ let os_input = os_input.clone(); + let stdin_ansi_parser = stdin_ansi_parser.clone(); move || { os_input.handle_signals( Box::new({ let os_api = os_input.clone(); + let stdin_ansi_parser = stdin_ansi_parser.clone(); move || { os_api.send_to_server(ClientToServerMsg::TerminalResize { new_size: os_api.get_terminal_size(), }); #[cfg(not(windows))] - let _ = os_api - .get_stdout_writer() - .write(crate::stdin_handler::PIXEL_SIZE_QUERY.as_bytes()); + { + let mut stdin_ansi_parser = stdin_ansi_parser.lock().unwrap(); + stdin_ansi_parser.open_own_query_batch(); + let mut stdout = os_api.get_stdout_writer(); + let _ = stdout + .write_all(crate::stdin_handler::PIXEL_SIZE_QUERY.as_bytes()); + let _ = stdout.write_all(stdin_ansi_parser::PRIMARY_DA_QUERY); + let _ = stdout.flush(); + } + #[cfg(windows)] + let _ = &stdin_ansi_parser; } }), Box::new({ @@ -1559,6 +1585,11 @@ pub fn start_client( let mut stdin_ansi_parser = stdin_ansi_parser.lock().unwrap(); let stale_forward = stdin_ansi_parser.take_active_forward(); stdin_ansi_parser.open_forward(token); + let mut blob = query_bytes; + blob.extend_from_slice(stdin_ansi_parser::PRIMARY_DA_QUERY); + let mut out = os_input.get_stdout_writer(); + let _ = out.write_all(&blob); + let _ = out.flush(); stale_forward }; if let Some((stale_token, stale_reply_bytes)) = stale_forward { @@ -1596,16 +1627,6 @@ pub fn start_client( ); }, ); - // 3. Write the query + Primary-DA barrier in a single - // write_all. The barrier closes the window on the - // parser side when its reply arrives — the timer - // task's eventual wake-up finds an empty slot for - // this token and no-ops. - let mut blob = query_bytes; - blob.extend_from_slice(b"\x1b[c"); - let mut out = os_input.get_stdout_writer(); - let _ = out.write_all(&blob); - let _ = out.flush(); }, ClientInstruction::EmitNestedSessionFrame(payload_bytes) => { if nested_reannounce.host_contacted() { diff --git a/zellij-client/src/stdin_ansi_parser.rs b/zellij-client/src/stdin_ansi_parser.rs index 6ecddb8ea..8b3ffb2c1 100644 --- a/zellij-client/src/stdin_ansi_parser.rs +++ b/zellij-client/src/stdin_ansi_parser.rs @@ -9,6 +9,8 @@ use lazy_static::lazy_static; use regex::Regex; use serde::{Deserialize, Serialize}; +use std::collections::VecDeque; +use std::time::{Duration, Instant}; use zellij_utils::{ data::HostTerminalThemeMode, ipc::PixelDimensions, @@ -185,8 +187,25 @@ impl HostReply { pub struct ForwardSlot { pub token: u32, pub reply_bytes: Vec, + abandoned: bool, } +#[derive(Debug, Clone)] +enum BatchOwner { + Zellij, + Pane(ForwardSlot), +} + +#[derive(Debug, Clone)] +struct QueryBatch { + owner: BatchOwner, + sent_at: Instant, +} + +pub const PRIMARY_DA_QUERY: &[u8] = b"\x1b[c"; + +const UNANSWERED_BATCH_EXPIRY: Duration = Duration::from_secs(5); + #[derive(Debug, Clone)] pub struct ClipboardForwardSlot { pub token: u32, @@ -306,9 +325,7 @@ enum StartupKittyProbe { /// Continuous host-reply parser. Lives for the whole client session. pub struct StdinAnsiParser { inner: InputParser, - /// Active forwarding slot: `Some` while a forwarded query is in - /// flight, `None` otherwise. - active_forward: Option, + batches: VecDeque, active_clipboard_forward: Option, /// Bytes of an OSC sequence whose terminator hasn't arrived yet. /// Carried across feed() calls so the next chunk can complete it. @@ -325,7 +342,7 @@ pub struct StdinAnsiParser { impl std::fmt::Debug for StdinAnsiParser { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("StdinAnsiParser") - .field("active_forward", &self.active_forward) + .field("batches", &self.batches) .field("partial_osc_len", &self.partial_osc.len()) .field("partial_csi_len", &self.partial_csi.len()) .finish() @@ -336,7 +353,7 @@ impl StdinAnsiParser { pub fn new() -> Self { StdinAnsiParser { inner: InputParser::new(), - active_forward: None, + batches: VecDeque::new(), active_clipboard_forward: None, partial_osc: Vec::new(), partial_csi: Vec::new(), @@ -366,35 +383,82 @@ impl StdinAnsiParser { /// log and clobber the previous slot (whose accumulated bytes would /// otherwise silently leak). pub fn open_forward(&mut self, token: u32) { + let existing = self.live_forward().map(|slot| slot.token); debug_assert!( - self.active_forward.is_none(), + existing.is_none(), "open_forward({}) called while slot for token {:?} is still active", token, - self.active_forward.as_ref().map(|s| s.token), + existing, ); - if let Some(existing) = self.active_forward.as_ref() { + if let Some(existing_token) = existing { log::warn!( - "open_forward({}) re-entered with existing slot token={} ({} accumulated bytes \ - will be dropped); server serialization should have prevented this", + "open_forward({}) re-entered with existing slot token={}; abandoning it", token, - existing.token, - existing.reply_bytes.len(), + existing_token, ); + let _ = self.take_active_forward(); } - self.active_forward = Some(ForwardSlot { - token, - reply_bytes: Vec::new(), + self.batches.push_back(QueryBatch { + owner: BatchOwner::Pane(ForwardSlot { + token, + reply_bytes: Vec::new(), + abandoned: false, + }), + sent_at: Instant::now(), }); } + pub fn open_own_query_batch(&mut self) { + self.batches.push_back(QueryBatch { + owner: BatchOwner::Zellij, + sent_at: Instant::now(), + }); + } + + pub fn awaiting_replies(&self) -> bool { + self.batches + .iter() + .any(|batch| batch.sent_at.elapsed() < UNANSWERED_BATCH_EXPIRY) + } + + fn expire_unanswered_batches(&mut self) { + self.batches + .retain(|batch| batch.sent_at.elapsed() < UNANSWERED_BATCH_EXPIRY); + } + + fn live_forward(&self) -> Option<&ForwardSlot> { + self.batches.iter().find_map(|batch| match &batch.owner { + BatchOwner::Pane(slot) if !slot.abandoned => Some(slot), + _ => None, + }) + } + + fn live_forward_mut(&mut self) -> Option<&mut ForwardSlot> { + self.batches + .iter_mut() + .find_map(|batch| match &mut batch.owner { + BatchOwner::Pane(slot) if !slot.abandoned => Some(slot), + _ => None, + }) + } + + fn collecting_forward_mut(&mut self) -> Option<&mut ForwardSlot> { + match self.batches.front_mut().map(|batch| &mut batch.owner) { + Some(BatchOwner::Pane(slot)) if !slot.abandoned => Some(slot), + _ => None, + } + } + + fn abandon(slot: &mut ForwardSlot) -> (u32, Vec) { + slot.abandoned = true; + (slot.token, std::mem::take(&mut slot.reply_bytes)) + } + /// Close an active forwarding window without a barrier (timeout path). /// Returns the accumulated reply bytes and the token, if any. pub fn close_forward_on_timeout(&mut self, token: u32) -> Option<(u32, Vec)> { - match &self.active_forward { - Some(slot) if slot.token == token => { - let slot = self.active_forward.take().unwrap(); - Some((slot.token, slot.reply_bytes)) - }, + match self.live_forward_mut() { + Some(slot) if slot.token == token => Some(Self::abandon(slot)), _ => None, } } @@ -404,9 +468,7 @@ impl StdinAnsiParser { /// accumulated. Used to hand a slot the server has already given up on /// over to the forward that replaced it, instead of clobbering it. pub fn take_active_forward(&mut self) -> Option<(u32, Vec)> { - self.active_forward - .take() - .map(|slot| (slot.token, slot.reply_bytes)) + self.live_forward_mut().map(Self::abandon) } pub fn open_clipboard_forward(&mut self, token: u32) { @@ -445,7 +507,7 @@ impl StdinAnsiParser { /// `close_forward_on_timeout`, and `feed()` directly. #[cfg(test)] pub fn active_forward_token(&self) -> Option { - self.active_forward.as_ref().map(|s| s.token) + self.live_forward().map(|s| s.token) } /// Consume a chunk of raw stdin bytes. Returns classified host replies @@ -455,6 +517,7 @@ impl StdinAnsiParser { /// are the bytes the caller should feed to the keyboard parser. pub fn feed(&mut self, bytes: &[u8]) -> ParseOutput { let mut out = ParseOutput::default(); + self.expire_unanswered_batches(); let (bytes, nested_frames) = self.nested_frame_extractor.extract(bytes); let bytes = &bytes[..]; out.nested_frames = nested_frames; @@ -495,7 +558,7 @@ impl StdinAnsiParser { continue; } } - if let Some(slot) = self.active_forward.as_mut() { + if let Some(slot) = self.collecting_forward_mut() { // Re-serialize so the pane's pty sees a legal OSC. // Terminators vary by host; ST (ESC \) is always safe. slot.reply_bytes.extend_from_slice(b"\x1b]"); @@ -520,18 +583,29 @@ impl StdinAnsiParser { } // Primary-DA — the barrier. Close the slot and // emit the completed forwarded reply if active. - if let Some(slot) = self.active_forward.take() { - out.completed_forward = Some((slot.token, slot.reply_bytes)); + if let Some(QueryBatch { + owner: BatchOwner::Pane(slot), + .. + }) = self.batches.pop_front() + { + if !slot.abandoned { + out.completed_forward = Some((slot.token, slot.reply_bytes)); + } } // Primary-DA is NOT double-dispatched — it has // no cached-state counterpart. }, _ => { - if let Some(reply) = HostReply::from_csi_report(&raw) { + let reply = HostReply::from_csi_report(&raw); + let is_theme_notification = + matches!(reply, Some(HostReply::HostTerminalThemeChanged(_))); + if let Some(reply) = reply { out.replies.push(reply); } - if let Some(slot) = self.active_forward.as_mut() { - slot.reply_bytes.extend_from_slice(&raw); + if !is_theme_notification { + if let Some(slot) = self.collecting_forward_mut() { + slot.reply_bytes.extend_from_slice(&raw); + } } // Suppress unused-variable warning for params. let _ = params; @@ -574,6 +648,15 @@ impl StdinAnsiParser { } pub fn pending_partial(&self) -> PendingPartial { + match self.classify_partial() { + PendingPartial::LoneEsc | PendingPartial::BareIntroducer if self.awaiting_replies() => { + PendingPartial::ReplyInProgress + }, + partial => partial, + } + } + + fn classify_partial(&self) -> PendingPartial { if !self.partial_paste.is_empty() || !self.partial_apc.is_empty() { PendingPartial::ReplyInProgress } else if self.partial_csi.is_empty() @@ -597,6 +680,14 @@ impl StdinAnsiParser { } pub fn finalize_fast_partial(&mut self) -> Vec { + let drained = self.take_fast_partial(); + if !drained.is_empty() { + self.inner = InputParser::new(); + } + drained + } + + fn take_fast_partial(&mut self) -> Vec { match self.pending_partial() { PendingPartial::LoneEsc | PendingPartial::BareIntroducer => { if !self.nested_frame_extractor.partial_bytes().is_empty() { @@ -626,6 +717,9 @@ impl StdinAnsiParser { out.append(&mut self.partial_paste); out.append(&mut partial_nested_frame); out.append(&mut self.partial_apc); + if !out.is_empty() { + self.inner = InputParser::new(); + } out } diff --git a/zellij-client/src/stdin_ansi_parser_tests.rs b/zellij-client/src/stdin_ansi_parser_tests.rs index 9df052e9c..daf019709 100644 --- a/zellij-client/src/stdin_ansi_parser_tests.rs +++ b/zellij-client/src/stdin_ansi_parser_tests.rs @@ -1185,6 +1185,137 @@ fn a_bare_csi_introducer_is_fast_flushed_as_alt_open_bracket() { ); } +#[test] +fn a_bare_csi_introducer_waits_for_the_reply_body_while_replies_are_expected() { + let mut p = StdinAnsiParser::new(); + p.open_own_query_batch(); + let r1 = p.feed(b"\x1b[4;600;800t\x1b["); + assert!(r1.residue.is_empty()); + assert!( + p.pending_partial().uses_reply_flush_guard(), + "an introducer held while replies are expected must get the long guard" + ); + assert!( + p.finalize_fast_partial().is_empty(), + "the fast flush must not release the introducer of an expected reply" + ); + + let r2 = p.feed(b"6;25;12t"); + assert!( + r2.residue.is_empty(), + "the reply body must not reach the keyboard as text: {:?}", + String::from_utf8_lossy(&r2.residue) + ); + assert!(r2 + .replies + .iter() + .any(|reply| matches!(reply, HostReply::PixelDimensions(_)))); +} + +#[test] +fn a_bare_csi_introducer_is_fast_flushed_once_expected_replies_have_arrived() { + let mut p = StdinAnsiParser::new(); + p.open_own_query_batch(); + let _ = p.feed(b"\x1b[?62;22c"); + assert!(!p.awaiting_replies()); + let _ = p.feed(b"\x1b["); + assert_eq!(p.pending_partial(), PendingPartial::BareIntroducer); + assert_eq!(p.finalize_fast_partial(), b"\x1b[".to_vec()); +} + +#[test] +fn a_fast_flushed_introducer_does_not_complete_a_reply_in_the_classifier() { + let mut p = StdinAnsiParser::new(); + let _ = p.feed(b"\x1b["); + assert_eq!(p.finalize_fast_partial(), b"\x1b[".to_vec()); + let out = p.feed(b"6;25;12t"); + assert!( + out.replies.is_empty(), + "bytes already handed to the keyboard must not also be applied as a reply: {:?}", + out.replies + ); +} + +#[test] +fn startup_replies_arriving_while_a_pane_query_is_forwarded_stay_with_zellij() { + let mut p = StdinAnsiParser::new(); + p.open_own_query_batch(); + p.open_forward(7); + + let startup = p.feed( + b"\x1b[?997;1n\x1b[4;600;800t\x1b[6;25;12t\x1b]11;rgb:1111/1313/1313\x1b\\\x1b]10;rgb:ffff/ffff/ffff\x1b\\\x1b[?2026;2$y\x1b[?62;22c", + ); + assert!(startup.residue.is_empty()); + assert!( + startup.completed_forward.is_none(), + "zellij's own barrier must not close the pane's slot" + ); + assert!( + startup.replies.len() >= 5, + "zellij still consumes its own replies" + ); + assert_eq!(p.active_forward_token(), Some(7)); + + let pane = p.feed(b"\x1b]11;rgb:2222/2323/2323\x1b\\\x1b[?62;22c"); + assert_eq!( + pane.completed_forward, + Some((7, b"\x1b]11;rgb:2222/2323/2323\x1b\\".to_vec())), + "the pane receives exactly the reply to its own query" + ); + assert!(!p.awaiting_replies()); +} + +#[test] +fn resize_pixel_requery_replies_are_not_handed_to_a_forwarding_pane() { + let mut p = StdinAnsiParser::new(); + p.open_own_query_batch(); + p.open_forward(3); + let first = p.feed(b"\x1b[4;600;800t\x1b[6;25;12t\x1b[?62;22c"); + assert!(first.completed_forward.is_none()); + let second = p.feed(b"\x1b[6;25;12t\x1b[?62;22c"); + assert_eq!( + second.completed_forward, + Some((3, b"\x1b[6;25;12t".to_vec())) + ); +} + +#[test] +fn a_late_reply_for_an_abandoned_forward_is_dropped_not_given_to_the_next_pane() { + let mut p = StdinAnsiParser::new(); + p.open_forward(1); + assert_eq!(p.close_forward_on_timeout(1), Some((1, Vec::new()))); + p.open_forward(2); + + let late = p.feed(b"\x1b]11;rgb:1111/1313/1313\x1b\\\x1b[?62;22c"); + assert!(late.residue.is_empty()); + assert!( + late.completed_forward.is_none(), + "the abandoned slot's barrier must not complete another pane's slot" + ); + assert_eq!(p.active_forward_token(), Some(2)); + + let own = p.feed(b"\x1b]10;rgb:ffff/ffff/ffff\x1b\\\x1b[?62;22c"); + assert_eq!( + own.completed_forward, + Some((2, b"\x1b]10;rgb:ffff/ffff/ffff\x1b\\".to_vec())) + ); +} + +#[test] +fn a_host_theme_notification_is_not_given_to_the_forwarding_pane() { + let mut p = StdinAnsiParser::new(); + p.open_forward(5); + let out = p.feed(b"\x1b[?997;2n\x1b]11;rgb:1111/1313/1313\x1b\\\x1b[?62;22c"); + assert!(out + .replies + .iter() + .any(|reply| matches!(reply, HostReply::HostTerminalThemeChanged(_)))); + assert_eq!( + out.completed_forward, + Some((5, b"\x1b]11;rgb:1111/1313/1313\x1b\\".to_vec())) + ); +} + #[test] fn an_introducer_with_a_body_byte_keeps_the_long_reply_guard() { for prefix in [b"\x1b]4".as_ref(), b"\x1b[?".as_ref()] { diff --git a/zellij-client/src/stdin_handler.rs b/zellij-client/src/stdin_handler.rs index 7a1927071..4b6bb678c 100644 --- a/zellij-client/src/stdin_handler.rs +++ b/zellij-client/src/stdin_handler.rs @@ -22,6 +22,7 @@ pub(crate) fn stdin_loop( stdin_ansi_parser: Arc>, explicitly_disable_kitty_keyboard_protocol: bool, support_kitty_graphics_protocol: bool, + query_host_theme: bool, resize_sender: Option>, ) { // On Windows we choose between the VT byte path (termwiz/kitty parsing) @@ -45,14 +46,19 @@ pub(crate) fn stdin_loop( let can_query_terminal = true; if can_query_terminal { - let query_string = build_startup_query_string(support_kitty_graphics_protocol); - let _ = os_input - .get_stdout_writer() - .write(query_string.as_bytes()) - .unwrap(); - if support_kitty_graphics_protocol { - stdin_ansi_parser.lock().unwrap().expect_kitty_probe_reply(); - } else { + let query_string = + build_startup_query_string(support_kitty_graphics_protocol, query_host_theme); + { + let mut stdin_ansi_parser = stdin_ansi_parser.lock().unwrap(); + stdin_ansi_parser.open_own_query_batch(); + if support_kitty_graphics_protocol { + stdin_ansi_parser.expect_kitty_probe_reply(); + } + let mut stdout = os_input.get_stdout_writer(); + let _ = stdout.write_all(query_string.as_bytes()); + let _ = stdout.flush(); + } + if !support_kitty_graphics_protocol { let _ = send_input_instructions.send(InputInstruction::AnsiStdinInstructions(vec![ HostReply::KittyGraphicsSupport(false), @@ -384,7 +390,10 @@ fn realign_current_buffer(current_buffer: &mut Vec, input_parser: &InputPars /// Build the fire-and-forget host-query batch sent at client startup. /// The host's replies refine `Screen`'s cached state asynchronously as /// they arrive; the UI does not block on them. -fn build_startup_query_string(support_kitty_graphics_protocol: bool) -> String { +fn build_startup_query_string( + support_kitty_graphics_protocol: bool, + query_host_theme: bool, +) -> String { // [14t => get text area size in pixels, // [16t => get character cell size in pixels // ]11;?\ => get background color @@ -399,9 +408,14 @@ fn build_startup_query_string(support_kitty_graphics_protocol: bool) -> String { } else { "" }; + let host_theme_query = if query_host_theme { + crate::QUERY_HOST_THEME + } else { + "" + }; format!( - "{}\u{1b}]11;?\u{1b}\u{5c}\u{1b}]10;?\u{1b}\u{5c}\u{1b}[?2026$p{}\u{1b}[c", - PIXEL_SIZE_QUERY, kitty_graphics_probe + "{}{}\u{1b}]11;?\u{1b}\u{5c}\u{1b}]10;?\u{1b}\u{5c}\u{1b}[?2026$p{}\u{1b}[c", + host_theme_query, PIXEL_SIZE_QUERY, kitty_graphics_probe ) } @@ -464,7 +478,7 @@ mod tests { #[test] fn startup_query_has_no_palette_register_loop() { - let query = build_startup_query_string(true); + let query = build_startup_query_string(true, false); assert_eq!( query, "\u{1b}[14t\u{1b}[16t\u{1b}]11;?\u{1b}\u{5c}\u{1b}]10;?\u{1b}\u{5c}\u{1b}[?2026$p\u{1b}_Ga=q,i=31,s=1,v=1,t=d,f=24;AAAA\u{1b}\u{5c}\u{1b}[c" @@ -490,9 +504,16 @@ mod tests { assert!(focus_changes.is_empty()); } + #[test] + fn startup_query_leads_with_the_host_theme_query_when_requested() { + let query = build_startup_query_string(false, true); + assert!(query.starts_with("\u{1b}[?996n\u{1b}[14t\u{1b}[16t")); + assert!(query.ends_with("\u{1b}[c")); + } + #[test] fn startup_query_contains_kitty_probe_before_barrier() { - let query = build_startup_query_string(true); + let query = build_startup_query_string(true, false); assert!(query.contains("\u{1b}_Ga=q,i=31,s=1,v=1,t=d,f=24;AAAA\u{1b}\u{5c}")); let probe_pos = query.find("\u{1b}_Ga=q,i=31,").unwrap(); let barrier_pos = query.find("\u{1b}[c").unwrap(); @@ -501,7 +522,7 @@ mod tests { #[test] fn startup_query_omits_kitty_probe_when_the_protocol_is_disabled() { - let query = build_startup_query_string(false); + let query = build_startup_query_string(false, false); assert!(!query.contains("\u{1b}_G")); assert!(query.ends_with("\u{1b}[c")); assert!(query.starts_with("\u{1b}[14t\u{1b}[16t")); diff --git a/zellij-client/src/unit/ipc_pipe_length_tests.rs b/zellij-client/src/unit/ipc_pipe_length_tests.rs new file mode 100644 index 000000000..fe6e9d4d3 --- /dev/null +++ b/zellij-client/src/unit/ipc_pipe_length_tests.rs @@ -0,0 +1,40 @@ +use crate::{ipc_pipe_length_error, session_ipc_pipe_length_error}; +use std::path::{Path, PathBuf}; +use zellij_utils::consts::ZELLIJ_SOCK_MAX_LENGTH; + +fn path_of_length(length: usize) -> PathBuf { + let mut path = String::from("/tmp/"); + path.push_str(&"a".repeat(length - path.len())); + PathBuf::from(path) +} + +#[test] +fn a_socket_path_below_the_limit_is_accepted() { + assert!(ipc_pipe_length_error(&path_of_length(ZELLIJ_SOCK_MAX_LENGTH - 1)).is_none()); +} + +#[test] +fn a_socket_path_at_the_limit_is_rejected_with_its_length() { + let message = ipc_pipe_length_error(&path_of_length(ZELLIJ_SOCK_MAX_LENGTH)) + .expect("a path at the limit must be rejected"); + assert!( + message.contains(&format!("({} bytes", ZELLIJ_SOCK_MAX_LENGTH)), + "the message must report the offending length: {}", + message + ); + assert!(message.contains("ZELLIJ_SOCKET_DIR=")); +} + +#[test] +fn the_startup_check_measures_the_socket_directory_joined_with_the_session_name() { + let sock_dir = Path::new("/run/user/1000/zellij/contract_version_1"); + let short_name = "multiverse"; + let long_name = format!( + "multiverse-{}", + "a".repeat(ZELLIJ_SOCK_MAX_LENGTH - sock_dir.as_os_str().len()) + ); + assert!(session_ipc_pipe_length_error(sock_dir, short_name).is_none()); + let message = session_ipc_pipe_length_error(sock_dir, &long_name) + .expect("a session name that pushes the socket path past the limit must be rejected"); + assert!(message.contains(&sock_dir.join(&long_name).display().to_string())); +} diff --git a/zellij-client/src/unit/mod.rs b/zellij-client/src/unit/mod.rs index 48c726f0a..500d09294 100644 --- a/zellij-client/src/unit/mod.rs +++ b/zellij-client/src/unit/mod.rs @@ -4,3 +4,6 @@ mod terminal_loop_tests; #[cfg(test)] mod teardown_tests; + +#[cfg(test)] +mod ipc_pipe_length_tests; diff --git a/zellij-integration-tests/src/client_screen.rs b/zellij-integration-tests/src/client_screen.rs index 06a329af0..b40a31307 100644 --- a/zellij-integration-tests/src/client_screen.rs +++ b/zellij-integration-tests/src/client_screen.rs @@ -21,6 +21,69 @@ struct ReceivedBytesWithChangeSignal { received_bytes: Mutex, change_signal: Condvar, stdout_tap: Mutex>>>, + host_responder: Mutex>, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum HostTerminal { + Basic, + Kitty, + Manual, +} + +const PRIMARY_DA_QUERY: &[u8] = b"\x1b[c"; +const PRIMARY_DA_REPLY: &[u8] = b"\x1b[?62;22c"; +const KITTY_GRAPHICS_PROBE_PREFIX: &[u8] = b"\x1b_Ga=q,i=31,"; +const KITTY_GRAPHICS_PROBE_REPLY: &[u8] = b"\x1b_Gi=31;OK\x1b\\"; +const CELL_SIZE_QUERY: &[u8] = b"\x1b[16t"; +const CELL_SIZE_REPLY: &[u8] = b"\x1b[6;21;8t"; + +struct HostResponder { + host_terminal: HostTerminal, + stdin_tx: crossbeam::channel::Sender>, + carry: Vec, +} + +impl HostResponder { + fn answers(&self) -> &'static [(&'static [u8], &'static [u8])] { + match self.host_terminal { + HostTerminal::Basic => &[(PRIMARY_DA_QUERY, PRIMARY_DA_REPLY)], + HostTerminal::Kitty => &[ + (PRIMARY_DA_QUERY, PRIMARY_DA_REPLY), + (KITTY_GRAPHICS_PROBE_PREFIX, KITTY_GRAPHICS_PROBE_REPLY), + (CELL_SIZE_QUERY, CELL_SIZE_REPLY), + ], + HostTerminal::Manual => &[], + } + } + + fn observe(&mut self, bytes: &[u8]) { + let answers = self.answers(); + let mut data = std::mem::take(&mut self.carry); + data.extend_from_slice(bytes); + let mut replies = Vec::new(); + let mut index = 0; + while index < data.len() { + if data[index] == 0x1b { + let rest = &data[index..]; + if let Some((query, reply)) = + answers.iter().find(|(query, _)| rest.starts_with(query)) + { + replies.extend_from_slice(reply); + index += query.len(); + continue; + } + if answers.iter().any(|(query, _)| query.starts_with(rest)) { + self.carry = rest.to_vec(); + break; + } + } + index += 1; + } + if !replies.is_empty() { + let _ = self.stdin_tx.send(replies); + } + } } #[derive(Clone)] @@ -43,6 +106,18 @@ impl ClientScreen { }) } + pub fn set_host_terminal( + &self, + host_terminal: HostTerminal, + stdin_tx: crossbeam::channel::Sender>, + ) { + *self.inner.host_responder.lock().unwrap() = Some(HostResponder { + host_terminal, + stdin_tx, + carry: Vec::new(), + }); + } + pub fn set_stdout_tap(&self, sender: crossbeam::channel::Sender>) { *self.inner.stdout_tap.lock().unwrap() = Some(sender); } @@ -144,6 +219,10 @@ impl std::io::Write for ClientScreenWriter { let _ = stdout_tap.send(buf.to_vec()); } self.inner.change_signal.notify_all(); + drop(received_bytes); + if let Some(host_responder) = self.inner.host_responder.lock().unwrap().as_mut() { + host_responder.observe(buf); + } Ok(buf.len()) } fn flush(&mut self) -> std::io::Result<()> { diff --git a/zellij-integration-tests/src/fake_client_os_api.rs b/zellij-integration-tests/src/fake_client_os_api.rs index b70db3aea..c06d7bfd4 100644 --- a/zellij-integration-tests/src/fake_client_os_api.rs +++ b/zellij-integration-tests/src/fake_client_os_api.rs @@ -11,7 +11,7 @@ use zellij_utils::ipc::{ use zellij_utils::pane_size::Size; use zellij_utils::shared::default_palette; -use crate::client_screen::ClientScreen; +use crate::client_screen::{ClientScreen, HostTerminal}; type ServerSpawner = Box; @@ -120,6 +120,7 @@ impl FakeClientOsApi { env: Arc::new(Mutex::new(env)), received_server_messages: received_server_messages.clone(), }; + client_screen.set_host_terminal(HostTerminal::Basic, stdin_tx.clone()); let fake_client_handle = FakeClientHandle { client_screen, size, diff --git a/zellij-integration-tests/src/lib.rs b/zellij-integration-tests/src/lib.rs index 2e58dcade..595da2e8b 100644 --- a/zellij-integration-tests/src/lib.rs +++ b/zellij-integration-tests/src/lib.rs @@ -12,6 +12,7 @@ pub mod test_env; pub use client_screen::{ col, AnsiCode, CellStyle, ClientScreen, Coord, CoordBuilder, CursorPosition, GridSnapshot, + HostTerminal, }; pub use fake_pty::FakePtyHandle; pub use harness::{ diff --git a/zellij-integration-tests/src/runner.rs b/zellij-integration-tests/src/runner.rs index 15a02ee82..9697069e8 100644 --- a/zellij-integration-tests/src/runner.rs +++ b/zellij-integration-tests/src/runner.rs @@ -13,7 +13,7 @@ use zellij_utils::input::options::Options; use zellij_utils::pane_size::Size; use zellij_utils::setup::Setup; -use crate::client_screen::GridSnapshot; +use crate::client_screen::{GridSnapshot, HostTerminal}; use crate::fake_client_os_api::{FakeClientHandle, FakeClientOsApi}; use crate::fake_pty::FakePtyHandle; use crate::fake_server_os_api::FakeServerOsApi; @@ -29,6 +29,7 @@ pub struct TestRunner { env: std::collections::HashMap, stdout_tap: Option>>, skip_concurrency_slot: bool, + host_terminal: Option, } impl TestRunner { @@ -41,6 +42,7 @@ impl TestRunner { env: std::collections::HashMap::new(), stdout_tap: None, skip_concurrency_slot: false, + host_terminal: None, } } @@ -72,6 +74,11 @@ impl TestRunner { self } + pub fn with_host_terminal(mut self, host_terminal: HostTerminal) -> Self { + self.host_terminal = Some(host_terminal); + self + } + pub fn skip_concurrency_slot(mut self) -> Self { self.skip_concurrency_slot = true; self @@ -151,6 +158,14 @@ impl TestRunner { let (fake_client_os_api, fake_client_handle) = FakeClientOsApi::new_with_env(self.size, Some(server_spawner), self.env); + let host_terminal = self.host_terminal.unwrap_or(if self.stdout_tap.is_some() { + HostTerminal::Manual + } else { + HostTerminal::Basic + }); + fake_client_handle + .client_screen + .set_host_terminal(host_terminal, fake_client_handle.stdin_tx.clone()); if let Some(stdout_tap) = self.stdout_tap { fake_client_handle.client_screen.set_stdout_tap(stdout_tap); } diff --git a/zellij-integration-tests/tests/kitty_graphics.rs b/zellij-integration-tests/tests/kitty_graphics.rs index 51df4f93d..f4ceeed06 100644 --- a/zellij-integration-tests/tests/kitty_graphics.rs +++ b/zellij-integration-tests/tests/kitty_graphics.rs @@ -2,7 +2,7 @@ use zellij_integration_tests::{ claim_first_terminal_and_wait_for_prompt, col, keys, split_down_and_wait_for_prompt, - split_right_and_wait_for_prompt, start_zellij, FakePtyHandle, TestRunner, TestSession, + split_right_and_wait_for_prompt, FakePtyHandle, HostTerminal, TestRunner, TestSession, TERMINAL_SIZE, }; @@ -59,10 +59,14 @@ fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option { .position(|window| window == needle) } +fn start_zellij() -> TestSession { + TestRunner::new(TERMINAL_SIZE) + .with_host_terminal(HostTerminal::Kitty) + .start() +} + fn setup_kitty_host(zellij: &TestSession, terminal: &FakePtyHandle) { terminal.disable_echo(); - zellij.send_stdin(b"\x1b[6;21;8t"); - zellij.send_stdin(b"\x1b_Gi=31;OK\x1b\\"); zellij.send_stdin(b"Z"); terminal.wait_for_stdin( "kitty handshake barrier keystroke reached the pane", diff --git a/zellij-integration-tests/tests/nested_sessions.rs b/zellij-integration-tests/tests/nested_sessions.rs index e17d665b0..44d39707d 100644 --- a/zellij-integration-tests/tests/nested_sessions.rs +++ b/zellij-integration-tests/tests/nested_sessions.rs @@ -490,7 +490,7 @@ fn clicking_the_host_new_tab_button_while_descended_ascends_and_keeps_working() "the host opened a second tab and undimmed after clicking the new tab button", |host_grid| { host_grid.lines().first().map_or(false, |tab_bar| { - tab_bar.contains("Tab #1") && tab_bar.contains("Tab #2") + tab_bar.contains(guest_session_name.as_str()) && tab_bar.contains("Tab #2") }) && normal_mode_bar_settled(host_grid) }, ); @@ -507,7 +507,6 @@ fn clicking_the_host_new_tab_button_while_descended_ascends_and_keeps_working() |host_grid| host_grid.contains("Pane #2") && normal_mode_bar_settled(host_grid), ); - let _ = guest_session_name; nested.guest.quit(); nested.host.quit(); } @@ -544,14 +543,14 @@ fn clicking_a_host_tab_while_descended_switches_tabs_and_ascends() { "the host now has two tabs with the second one focused on a host pane", |host_grid| { host_grid.lines().first().map_or(false, |tab_bar| { - tab_bar.contains("Tab #1") && tab_bar.contains("Tab #2") + tab_bar.contains(guest_session_name.as_str()) && tab_bar.contains("Tab #2") }) && normal_mode_bar_settled(host_grid) }, ); let first_tab_column = host_two_tabs .lines() .first() - .and_then(|tab_bar| display_column_of(tab_bar, "Tab #1")) + .and_then(|tab_bar| display_column_of(tab_bar, &guest_session_name)) .expect("the first host tab ribbon is on the host tab bar") + 1; let second_tab_column = host_two_tabs @@ -574,7 +573,7 @@ fn clicking_a_host_tab_while_descended_switches_tabs_and_ascends() { "clicking the second host tab switches away from the guest tab in normal mode", |host_grid| { host_grid.lines().first().map_or(false, |tab_bar| { - tab_bar.contains("Tab #1") && tab_bar.contains("Tab #2") + tab_bar.contains(guest_session_name.as_str()) && tab_bar.contains("Tab #2") }) && normal_mode_bar_settled(host_grid) }, ); @@ -587,7 +586,6 @@ fn clicking_a_host_tab_while_descended_switches_tabs_and_ascends() { |host_grid| host_grid.contains("Pane #2") && normal_mode_bar_settled(host_grid), ); - let _ = guest_session_name; nested.guest.quit(); nested.host.quit(); } diff --git a/zellij-integration-tests/tests/snapshots/nested_sessions__host_ascended_after_clicking_new_tab_button.snap b/zellij-integration-tests/tests/snapshots/nested_sessions__host_ascended_after_clicking_new_tab_button.snap index 782526bf9..45b86afeb 100644 --- a/zellij-integration-tests/tests/snapshots/nested_sessions__host_ascended_after_clicking_new_tab_button.snap +++ b/zellij-integration-tests/tests/snapshots/nested_sessions__host_ascended_after_clicking_new_tab_button.snap @@ -2,6 +2,6 @@ source: zellij-integration-tests/tests/nested_sessions.rs expression: normalized(&host_after_new_tab) --- - Zellij (test)  Tab #1  Tab #2  +  + Zellij (test)  test-1  Tab #2  +  $ █ Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  ... diff --git a/zellij-integration-tests/tests/startup_host_query.rs b/zellij-integration-tests/tests/startup_host_query.rs index 7c4b6648e..bedc7807a 100644 --- a/zellij-integration-tests/tests/startup_host_query.rs +++ b/zellij-integration-tests/tests/startup_host_query.rs @@ -4,11 +4,18 @@ use std::thread::sleep; use std::time::Duration; use zellij_integration_tests::{ - claim_first_terminal_and_wait_for_prompt, keys, start_zellij, TestSession, + claim_first_terminal_and_wait_for_prompt, keys, start_zellij, HostTerminal, TestRunner, + TestSession, TERMINAL_SIZE, }; const IDLE_BEYOND_SHORT_FLUSH: Duration = Duration::from_millis(150); +fn start_zellij_with_a_silent_host() -> TestSession { + TestRunner::new(TERMINAL_SIZE) + .with_host_terminal(HostTerminal::Manual) + .start() +} + fn lock_interface(zellij: &TestSession) { zellij.send_stdin(&keys::ctrl('g')); zellij.wait_until("interface locked", |grid_snapshot| { @@ -58,6 +65,69 @@ fn fragmented_attach_reply_burst_does_not_leak_into_focused_pane() { zellij.quit(); } +#[test] +fn pixel_reply_split_after_the_introducer_does_not_leak_into_focused_pane() { + let mut zellij = start_zellij_with_a_silent_host(); + let terminal = claim_first_terminal_and_wait_for_prompt(&zellij); + terminal.disable_echo(); + + zellij.send_stdin(b"\x1b[4;600;800t\x1b["); + sleep(IDLE_BEYOND_SHORT_FLUSH); + zellij.send_stdin(b"6;25;12t"); + + zellij.send_stdin(b"Z"); + let stdin = terminal.wait_for_stdin("sentinel keystroke reached the pane", |stdin_bytes| { + stdin_bytes.contains(&b'Z') + }); + + assert_eq!( + stdin, + b"Z", + "host reply bytes leaked into the pane: {:?}", + String::from_utf8_lossy(&stdin) + ); + + zellij.quit(); +} + +#[test] +fn startup_replies_arriving_while_a_pane_query_is_forwarded_are_not_handed_to_the_pane() { + let mut zellij = start_zellij_with_a_silent_host(); + let terminal = claim_first_terminal_and_wait_for_prompt(&zellij); + terminal.disable_echo(); + + terminal.output(b"\x1b]11;?\x1b\\"); + zellij.wait_until_raw_output("the pane query reached the host terminal", |bytes| { + bytes + .windows(11) + .any(|window| window == b"\x1b]11;?\x1b\\\x1b[c") + }); + + zellij.send_stdin( + b"\x1b[4;600;800t\x1b[6;25;12t\x1b]11;rgb:1111/1313/1313\x1b\\\x1b]10;rgb:ffff/ffff/ffff\x1b\\\x1b[?2026;2$y\x1b[?62;22c", + ); + zellij.send_stdin(b"\x1b]11;rgb:1111/1313/1313\x1b\\\x1b[?62;22c"); + + terminal.wait_for_stdin("the forwarded reply reached the pane", |stdin_bytes| { + stdin_bytes.windows(4).any(|window| window == b"11;r") + }); + sleep(IDLE_BEYOND_SHORT_FLUSH); + let stdin = terminal.stdin_bytes(); + + assert!( + !stdin.windows(3).any(|window| window == b"25;"), + "a startup pixel reply was handed to the pane: {:?}", + String::from_utf8_lossy(&stdin) + ); + assert!( + !stdin.windows(4).any(|window| window == b"2026"), + "a startup DECRPM reply was handed to the pane: {:?}", + String::from_utf8_lossy(&stdin) + ); + + zellij.quit(); +} + #[test] fn theme_query_from_pane_is_answered_when_host_mode_is_unknown() { let mut zellij = start_zellij(); diff --git a/zellij-integration-tests/tests/tabs.rs b/zellij-integration-tests/tests/tabs.rs index e506c8653..d0b19caa3 100644 --- a/zellij-integration-tests/tests/tabs.rs +++ b/zellij-integration-tests/tests/tabs.rs @@ -442,6 +442,7 @@ fn move_tab_to_right() { let grid_snapshot = zellij.wait_until("second tab moved one position right", |grid_snapshot| { tabs_in_order(grid_snapshot, &["Tab #1", "Tab #3", "Tab #2"]) + && grid_snapshot.status_bar_appears() }); assert_snapshot!(normalized(&grid_snapshot)); zellij.quit(); diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index 1eb11cadb..b0c146b4e 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -2297,6 +2297,26 @@ pub(crate) fn route_action( Ok((should_break, Some(result))) } +const SESSION_READY_WAIT: Duration = Duration::from_secs(10); +const SESSION_READY_POLL_INTERVAL: Duration = Duration::from_millis(5); + +fn wait_for_session_to_be_ready(session_data: &Arc>>) -> bool { + let deadline = std::time::Instant::now() + SESSION_READY_WAIT; + loop { + let ready = session_data + .read() + .map(|session_data| session_data.is_some()) + .unwrap_or(false); + if ready { + return true; + } + if std::time::Instant::now() >= deadline { + return false; + } + thread::sleep(SESSION_READY_POLL_INTERVAL); + } +} + // this should only be used for one-off startup instructions macro_rules! send_to_screen_or_retry_queue { ($senders:expr, $message:expr, $instruction: expr, $retry_queue:expr) => {{ @@ -2966,27 +2986,38 @@ pub(crate) fn route_thread_main( // stale nested AnnounceAck landing after the AncestryUpdate that // supersedes it, for instance). Once one instruction has to be // parked, every instruction behind it is parked too. - let retried_count = retry_queue.len(); + let mut retried_count = retry_queue.len(); let mut pending_instructions = std::mem::take(&mut retry_queue); pending_instructions.push_back(instruction); - let mut deferred_instructions = VecDeque::new(); - for (index, pending_instruction) in pending_instructions.into_iter().enumerate() { - if !deferred_instructions.is_empty() { - deferred_instructions.push_back(pending_instruction); - continue; + loop { + let mut deferred_instructions = VecDeque::new(); + for (index, pending_instruction) in pending_instructions.into_iter().enumerate() + { + if !deferred_instructions.is_empty() { + deferred_instructions.push_back(pending_instruction); + continue; + } + if index < retried_count { + log::warn!("Server ready, retrying sending instruction."); + thread::sleep(Duration::from_millis(5)); + } + let should_break = handle_instruction( + pending_instruction, + Some(&mut deferred_instructions), + )?; + if should_break { + break 'route_loop; + } } - if index < retried_count { - log::warn!("Server ready, retrying sending instruction."); - thread::sleep(Duration::from_millis(5)); - } - let should_break = - handle_instruction(pending_instruction, Some(&mut deferred_instructions))?; - if should_break { - break 'route_loop; + if deferred_instructions.is_empty() + || !wait_for_session_to_be_ready(&session_data) + { + retry_queue = deferred_instructions; + break; } + retried_count = deferred_instructions.len(); + pending_instructions = deferred_instructions; } - // retry on loop around - retry_queue = deferred_instructions; }, Err(IpcReceiveError::Disconnected) => { break 'route_loop;