From ef265eb309acb262ba0232177475f2519492e43a Mon Sep 17 00:00:00 2001 From: wan9chi Date: Thu, 9 Jul 2026 16:53:35 +0800 Subject: [PATCH 1/2] revert: remove rendered milestone fences Reverts f5ac3640 while the replacement cross-platform window-title milestone protocol is designed. Co-authored-by: GPT-5.6 --- Cargo.lock | 13 +- Cargo.toml | 5 +- crates/pty_terminal_test/Cargo.toml | 2 +- crates/pty_terminal_test/README.md | 10 +- crates/pty_terminal_test/src/lib.rs | 187 +++------------------ crates/pty_terminal_test_client/src/lib.rs | 21 +-- 6 files changed, 40 insertions(+), 198 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f93270b3b..f31c68a5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -126,9 +126,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.103" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] name = "arrayvec" @@ -682,9 +682,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.20" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ "crossbeam-utils", ] @@ -1983,9 +1983,9 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memmap2" -version = "0.9.11" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" dependencies = [ "libc", ] @@ -2863,7 +2863,6 @@ dependencies = [ "pty_terminal", "pty_terminal_test_client", "subprocess_test", - "vte", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index c40f4aa7b..0b9187367 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,7 @@ future_not_send = "allow" [workspace.dependencies] anstream = "1.0.0" -anyhow = "1.0.103" +anyhow = "1.0.98" assert2 = "0.4.0" assertables = "10.0.0" async-trait = "0.1.89" @@ -84,7 +84,7 @@ globset = "0.4.18" jsonc-parser = { version = "0.32.0", features = ["serde"] } libc = "0.2.185" libtest-mimic = "0.8.2" -memmap2 = "0.9.11" +memmap2 = "0.9.7" monostate = "1.0.2" napi = "3" napi-build = "2" @@ -161,7 +161,6 @@ vite_task_plan = { path = "crates/vite_task_plan" } vite_task_server = { path = "crates/vite_task_server" } vite_workspace = { path = "crates/vite_workspace" } vt100 = "0.16.2" -vte = "0.15.0" wax = "0.7.0" which = "8.0.0" widestring = "1.2.0" diff --git a/crates/pty_terminal_test/Cargo.toml b/crates/pty_terminal_test/Cargo.toml index 0f624bef0..e47ca1ffa 100644 --- a/crates/pty_terminal_test/Cargo.toml +++ b/crates/pty_terminal_test/Cargo.toml @@ -12,7 +12,6 @@ anyhow = { workspace = true } portable-pty = { workspace = true } pty_terminal = { workspace = true } pty_terminal_test_client = { workspace = true } -vte = { workspace = true } [dev-dependencies] crossterm = { workspace = true } @@ -25,6 +24,7 @@ subprocess_test = { workspace = true, features = ["portable-pty"] } workspace = true [lib] +test = false doctest = false [package.metadata.cargo-shear] diff --git a/crates/pty_terminal_test/README.md b/crates/pty_terminal_test/README.md index 9e2b4e776..ab97238cd 100644 --- a/crates/pty_terminal_test/README.md +++ b/crates/pty_terminal_test/README.md @@ -53,10 +53,10 @@ Milestones are encoded as an OSC 8 hyperlink: `Reader::expect_milestone` works like this: -1. Decode OSC 8 URI payloads from the PTY stream back into milestone names. -2. Pair each marker with its following zero-width rendered anchor. -3. Wait until both the requested marker and its anchor have arrived. -4. Return the current `screen_contents()`. +1. Drain parsed unhandled OSC sequences from `PtyReader`. +2. Decode OSC 8 URI payload back into milestone name. +3. If no match yet, continue reading from PTY and repeat. +4. On match, return current `screen_contents()`. The helper strips the protocol's zero-width space from returned screen text. @@ -65,8 +65,6 @@ The helper strips the protocol's zero-width space from returned screen text. The OSC 8 + zero-width anchor approach is used because it works across Unix and Windows ConPTY in this project. In particular, zero-length hyperlink opens can be lost on some Windows output paths, so the zero-width anchor is intentional. -Waiting for the anchor also prevents ConPTY's control-sequence path from -delivering a milestone before earlier rendered text. ## Typical test pattern diff --git a/crates/pty_terminal_test/src/lib.rs b/crates/pty_terminal_test/src/lib.rs index 2cf5d0479..af724599e 100644 --- a/crates/pty_terminal_test/src/lib.rs +++ b/crates/pty_terminal_test/src/lib.rs @@ -1,4 +1,4 @@ -use std::{collections::VecDeque, io::Read}; +use std::io::{BufReader, Read}; pub use portable_pty::CommandBuilder; use pty_terminal::terminal::{PtyReader, Terminal}; @@ -10,58 +10,6 @@ pub use pty_terminal::{ const MILESTONE_HYPERTEXT: char = '\u{200b}'; -/// Tracks the two independently delivered parts of each milestone. -/// -/// A milestone starts with an OSC 8 hyperlink carrying its name and contains a -/// zero-width printable character. On `ConPTY`, the OSC control sequence can be -/// forwarded before earlier screen updates, while the printable character -/// follows those updates through the asynchronous rendering path. A milestone -/// is therefore complete only after both parts have arrived. -/// -/// Several OSC markers can overtake their anchors. The two queues preserve the -/// protocol order so an earlier marker's delayed anchor cannot complete a later -/// marker by mistake. -#[derive(Default)] -struct MilestoneTracker { - /// Marker names whose rendered zero-width anchors have not arrived yet. - awaiting_fence: VecDeque, - /// Marker names whose matching rendered anchors have arrived. - completed: VecDeque, -} - -impl MilestoneTracker { - fn take_completed(&mut self, name: &str) -> bool { - // Keep unrelated completed milestones available for later calls. A PTY - // read can contain more than the milestone currently being requested. - self.completed - .iter() - .position(|completed| completed == name) - .and_then(|index| self.completed.remove(index)) - .is_some() - } -} - -impl vte::Perform for MilestoneTracker { - fn print(&mut self, character: char) { - // `print` is called only for rendered characters, not for bytes inside - // OSC metadata. ConPTY preserves the order of these rendered anchors, - // so each anchor completes the oldest marker still awaiting one. - if character == MILESTONE_HYPERTEXT - && let Some(name) = self.awaiting_fence.pop_front() - { - self.completed.push_back(name); - } - } - - fn osc_dispatch(&mut self, params: &[&[u8]], _bell_terminated: bool) { - // The decoder accepts only milestone hyperlink opens. Ordinary OSC - // sequences and the empty OSC 8 close sequence are ignored. - if let Some(name) = pty_terminal_test_client::decode_milestone_from_osc8_params(params) { - self.awaiting_fence.push_back(name); - } - } -} - /// A test-oriented terminal that provides milestone-based synchronization. /// /// Wraps a PTY terminal, splitting it into a [`PtyWriter`] for sending input @@ -75,17 +23,7 @@ pub struct TestTerminal { /// The read half of a test terminal, wrapping [`PtyReader`] with milestone support. pub struct Reader { - /// Reads bytes and updates the terminal's primary `vt100` screen parser. - /// - /// This is deliberately not wrapped in `BufReader`: its read-ahead would - /// let the primary parser consume bytes the milestone parser has not seen. - pty: PtyReader, - /// Observes the same byte stream to distinguish OSC markers from printable - /// anchors. `vt100::Callbacks` exposes unhandled OSC sequences but has no - /// callback for ordinary rendered characters, hence this small second parser. - milestone_parser: vte::Parser, - /// Persists protocol state across reads and `expect_milestone` calls. - milestone_tracker: MilestoneTracker, + pty: BufReader, child_handle: ChildHandle, } @@ -99,38 +37,17 @@ impl TestTerminal { let Terminal { pty_reader, pty_writer, child_handle, .. } = Terminal::spawn(size, cmd)?; Ok(Self { writer: pty_writer, - reader: Reader { - pty: pty_reader, - milestone_parser: vte::Parser::new(), - milestone_tracker: MilestoneTracker::default(), - child_handle: child_handle.clone(), - }, + reader: Reader { pty: BufReader::new(pty_reader), child_handle: child_handle.clone() }, child_handle, }) } } impl Reader { - /// Reads once while keeping the screen parser and milestone parser in lockstep. - /// - /// All PTY draining, including shutdown, must go through this method. Reading - /// directly from `pty` would update the screen while silently skipping those - /// bytes in the milestone protocol state. - fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - let n = self.pty.read(buf)?; - self.milestone_parser.advance(&mut self.milestone_tracker, &buf[..n]); - - // `PtyReader`'s primary parser also records the OSC sequences. The - // dedicated tracker above owns milestone handling, so discard this - // duplicate copy rather than letting it grow for the lifetime of a test. - drop(self.pty.take_unhandled_osc_sequences()); - Ok(n) - } - /// Returns terminal screen contents with milestone hyperlink text removed. #[must_use] pub fn screen_contents(&self) -> String { - let mut contents = self.pty.screen_contents(); + let mut contents = self.pty.get_ref().screen_contents(); contents.retain(|ch| ch != MILESTONE_HYPERTEXT); contents } @@ -139,7 +56,7 @@ impl Reader { /// Useful for snapshot tests that need to assert colour or style attributes. #[must_use] pub fn screen_contents_formatted(&self) -> Vec { - self.pty.screen_contents_formatted() + self.pty.get_ref().screen_contents_formatted() } /// Reads from the PTY until a milestone with the given name is encountered. @@ -147,12 +64,10 @@ impl Reader { /// Returns the terminal screen contents at the moment the milestone is detected. /// /// Milestones use a uniform protocol across platforms: the milestone name - /// is encoded in an OSC 8 hyperlink URI. A zero-width hyperlink anchor follows - /// each marker through the rendered output path. The reader waits for both the - /// marker and its corresponding anchor before returning, then strips the anchor - /// from the returned screen contents. Marker and anchor parsing is incremental, - /// so either sequence may be split across PTY reads or share a read with other - /// milestones. + /// is encoded in an OSC 8 hyperlink URI. We parse unhandled OSC sequences + /// from the VT parser state (instead of raw byte matching), then decode the + /// milestone URI payload. The zero-width milestone hyperlink anchor is + /// stripped from returned screen contents. /// /// # Panics /// @@ -163,11 +78,20 @@ impl Reader { let mut buf = [0u8; 4096]; loop { - if self.milestone_tracker.take_completed(name) { + let found = self + .pty + .get_ref() + .take_unhandled_osc_sequences() + .into_iter() + .filter_map(|params| { + pty_terminal_test_client::decode_milestone_from_osc8_params(¶ms) + }) + .any(|decoded| decoded == name); + if found { return self.screen_contents(); } - let n = self.read(&mut buf).expect("PTY read failed"); + let n = self.pty.read(&mut buf).expect("PTY read failed"); assert!(n > 0, "EOF reached before milestone '{name}'"); } } @@ -182,75 +106,8 @@ impl Reader { /// /// Panics if reading from the PTY fails. pub fn wait_for_exit(&mut self) -> anyhow::Result { - let mut buf = [0u8; 4096]; - while self.read(&mut buf).expect("PTY read failed") > 0 {} + let mut discard = Vec::new(); + self.pty.read_to_end(&mut discard).expect("PTY read_to_end failed"); self.child_handle.wait() } } - -#[cfg(test)] -mod tests { - use super::*; - - fn marker_without_fence(name: &str) -> Vec { - // Model ConPTY's fast control path by delivering the complete OSC marker - // before its printable anchor reaches the output pipe. - let mut marker = pty_terminal_test_client::encoded_milestone(name); - let index = marker - .windows(pty_terminal_test_client::MILESTONE_RENDER_FENCE.len()) - .position(|window| window == pty_terminal_test_client::MILESTONE_RENDER_FENCE) - .unwrap(); - marker.drain(index..index + pty_terminal_test_client::MILESTONE_RENDER_FENCE.len()); - marker - } - - fn advance(parser: &mut vte::Parser, tracker: &mut MilestoneTracker, bytes: &[u8]) { - parser.advance(tracker, bytes); - } - - #[test] - fn milestone_waits_for_rendered_fence() { - let mut parser = vte::Parser::new(); - let mut tracker = MilestoneTracker::default(); - - // Receiving the marker and subsequent printable output is insufficient: - // only the protocol's rendered anchor establishes the screen barrier. - advance(&mut parser, &mut tracker, &marker_without_fence("target")); - advance(&mut parser, &mut tracker, b"rendered output"); - assert!(!tracker.take_completed("target")); - - advance(&mut parser, &mut tracker, pty_terminal_test_client::MILESTONE_RENDER_FENCE); - assert!(tracker.take_completed("target")); - } - - #[test] - fn milestone_parses_across_every_chunk_boundary() { - let marker = pty_terminal_test_client::encoded_milestone("target"); - - for split in 0..=marker.len() { - let mut parser = vte::Parser::new(); - let mut tracker = MilestoneTracker::default(); - advance(&mut parser, &mut tracker, &marker[..split]); - advance(&mut parser, &mut tracker, &marker[split..]); - assert!(tracker.take_completed("target"), "failed at split {split}"); - } - } - - #[test] - fn rendered_fences_complete_overtaken_markers_in_order() { - let mut parser = vte::Parser::new(); - let mut tracker = MilestoneTracker::default(); - let mut markers = marker_without_fence("first"); - markers.extend(marker_without_fence("second")); - - // Both controls overtake rendering. The first anchor must still complete - // `first`, never whichever marker the caller happens to be waiting for. - advance(&mut parser, &mut tracker, &markers); - advance(&mut parser, &mut tracker, pty_terminal_test_client::MILESTONE_RENDER_FENCE); - assert!(tracker.take_completed("first")); - assert!(!tracker.take_completed("second")); - - advance(&mut parser, &mut tracker, pty_terminal_test_client::MILESTONE_RENDER_FENCE); - assert!(tracker.take_completed("second")); - } -} diff --git a/crates/pty_terminal_test_client/src/lib.rs b/crates/pty_terminal_test_client/src/lib.rs index d662e5f25..921f37ab8 100644 --- a/crates/pty_terminal_test_client/src/lib.rs +++ b/crates/pty_terminal_test_client/src/lib.rs @@ -5,16 +5,7 @@ const OSC_ST: &str = "\x1b\\"; /// Invisible hyperlink text anchor. const MILESTONE_HYPERTEXT: &str = "\u{200b}"; /// OSC 8 close sequence. -/// -/// This terminates hyperlink metadata only. It is a control sequence and does -/// not guarantee that `ConPTY` has emitted preceding rendered text. pub const MILESTONE_FENCE: &[u8] = b"\x1b]8;;\x1b\\"; -/// Zero-width printable fence that follows each milestone marker. -/// -/// Unlike the OSC control sequences, `ConPTY` emits this character through its -/// rendering path. Observing it therefore confirms that earlier rendered text -/// has reached the reader. -pub const MILESTONE_RENDER_FENCE: &[u8] = MILESTONE_HYPERTEXT.as_bytes(); /// Builds an OSC 8 marker with milestone name encoded in the hyperlink URI. /// @@ -54,12 +45,12 @@ const fn decode_hex_nibble(byte: u8) -> Option { /// Returns `Some(name)` only when the URI uses the milestone prefix and the /// suffix is valid hex-encoded UTF-8. #[must_use] -pub fn decode_milestone_from_osc8_params>(params: &[T]) -> Option { - if params.first().is_none_or(|p| p.as_ref() != b"8") { +pub fn decode_milestone_from_osc8_params(params: &[Vec]) -> Option { + if params.first().is_none_or(|p| p.as_slice() != b"8") { return None; } - let uri = params.get(2)?.as_ref(); + let uri = params.get(2)?.as_slice(); let encoded = uri.strip_prefix(MILESTONE_URI_PREFIX.as_bytes())?; if encoded.is_empty() || encoded.len() % 2 != 0 { return None; @@ -89,8 +80,7 @@ pub fn decode_milestone_from_osc8_params>(params: &[T]) -> Option /// /// Milestones include a zero-width hyperlink anchor (`U+200B`) before closing. /// This keeps the hyperlink metadata observable in `ConPTY` output paths that can -/// drop zero-length hyperlinks. The test harness also waits for this rendered -/// character so preceding screen output cannot be overtaken by the OSC marker. +/// drop zero-length hyperlinks. /// /// When the `testing` feature is disabled, this is a no-op. /// @@ -103,8 +93,7 @@ pub fn mark_milestone(name: &str) { let milestone = encoded_milestone(name); let mut stdout = stdout(); - // Flush prior output before emitting the marker. On ConPTY this flush alone - // is not a rendering barrier; the reader waits for MILESTONE_RENDER_FENCE. + // Flush prior output, then emit milestone sequence. stdout.flush().unwrap(); stdout.write_all(&milestone).unwrap(); From dfae62be84555fa0efcfed6ed11afb83661ed630 Mon Sep 17 00:00:00 2001 From: wan9chi Date: Thu, 9 Jul 2026 16:57:28 +0800 Subject: [PATCH 2/2] fix(deps): retain security updates Co-authored-by: GPT-5.6 --- Cargo.lock | 12 ++++++------ Cargo.toml | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f31c68a5e..6a8fd0e9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -126,9 +126,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.101" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "arrayvec" @@ -682,9 +682,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.18" +version = "0.9.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" dependencies = [ "crossbeam-utils", ] @@ -1983,9 +1983,9 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memmap2" -version = "0.9.9" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" dependencies = [ "libc", ] diff --git a/Cargo.toml b/Cargo.toml index 0b9187367..93d6882ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,7 @@ future_not_send = "allow" [workspace.dependencies] anstream = "1.0.0" -anyhow = "1.0.98" +anyhow = "1.0.103" assert2 = "0.4.0" assertables = "10.0.0" async-trait = "0.1.89" @@ -84,7 +84,7 @@ globset = "0.4.18" jsonc-parser = { version = "0.32.0", features = ["serde"] } libc = "0.2.185" libtest-mimic = "0.8.2" -memmap2 = "0.9.7" +memmap2 = "0.9.11" monostate = "1.0.2" napi = "3" napi-build = "2"