Skip to content

Commit 7bd4e61

Browse files
authored
revert: remove rendered milestone fences (#528)
## What changed Revert #527, removing the rendered zero-width milestone fence implementation and its dependency changes. ## Motivation The anonymous FIFO fence protocol does not handle ConPTY coalescing or multiple milestone-producing processes reliably. Reverting it restores the prior marker behavior while a cross-platform window-title-based protocol with process-unique nonces is designed.
1 parent f5ac364 commit 7bd4e61

6 files changed

Lines changed: 32 additions & 190 deletions

File tree

Cargo.lock

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,6 @@ vite_task_plan = { path = "crates/vite_task_plan" }
161161
vite_task_server = { path = "crates/vite_task_server" }
162162
vite_workspace = { path = "crates/vite_workspace" }
163163
vt100 = "0.16.2"
164-
vte = "0.15.0"
165164
wax = "0.7.0"
166165
which = "8.0.0"
167166
widestring = "1.2.0"

crates/pty_terminal_test/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ anyhow = { workspace = true }
1212
portable-pty = { workspace = true }
1313
pty_terminal = { workspace = true }
1414
pty_terminal_test_client = { workspace = true }
15-
vte = { workspace = true }
1615

1716
[dev-dependencies]
1817
crossterm = { workspace = true }
@@ -25,6 +24,7 @@ subprocess_test = { workspace = true, features = ["portable-pty"] }
2524
workspace = true
2625

2726
[lib]
27+
test = false
2828
doctest = false
2929

3030
[package.metadata.cargo-shear]

crates/pty_terminal_test/README.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ Milestones are encoded as an OSC 8 hyperlink:
5353

5454
`Reader::expect_milestone` works like this:
5555

56-
1. Decode OSC 8 URI payloads from the PTY stream back into milestone names.
57-
2. Pair each marker with its following zero-width rendered anchor.
58-
3. Wait until both the requested marker and its anchor have arrived.
59-
4. Return the current `screen_contents()`.
56+
1. Drain parsed unhandled OSC sequences from `PtyReader`.
57+
2. Decode OSC 8 URI payload back into milestone name.
58+
3. If no match yet, continue reading from PTY and repeat.
59+
4. On match, return current `screen_contents()`.
6060

6161
The helper strips the protocol's zero-width space from returned screen text.
6262

@@ -65,8 +65,6 @@ The helper strips the protocol's zero-width space from returned screen text.
6565
The OSC 8 + zero-width anchor approach is used because it works across Unix and
6666
Windows ConPTY in this project. In particular, zero-length hyperlink opens can
6767
be lost on some Windows output paths, so the zero-width anchor is intentional.
68-
Waiting for the anchor also prevents ConPTY's control-sequence path from
69-
delivering a milestone before earlier rendered text.
7068

7169
## Typical test pattern
7270

Lines changed: 22 additions & 165 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::{collections::VecDeque, io::Read};
1+
use std::io::{BufReader, Read};
22

33
pub use portable_pty::CommandBuilder;
44
use pty_terminal::terminal::{PtyReader, Terminal};
@@ -10,58 +10,6 @@ pub use pty_terminal::{
1010

1111
const MILESTONE_HYPERTEXT: char = '\u{200b}';
1212

13-
/// Tracks the two independently delivered parts of each milestone.
14-
///
15-
/// A milestone starts with an OSC 8 hyperlink carrying its name and contains a
16-
/// zero-width printable character. On `ConPTY`, the OSC control sequence can be
17-
/// forwarded before earlier screen updates, while the printable character
18-
/// follows those updates through the asynchronous rendering path. A milestone
19-
/// is therefore complete only after both parts have arrived.
20-
///
21-
/// Several OSC markers can overtake their anchors. The two queues preserve the
22-
/// protocol order so an earlier marker's delayed anchor cannot complete a later
23-
/// marker by mistake.
24-
#[derive(Default)]
25-
struct MilestoneTracker {
26-
/// Marker names whose rendered zero-width anchors have not arrived yet.
27-
awaiting_fence: VecDeque<String>,
28-
/// Marker names whose matching rendered anchors have arrived.
29-
completed: VecDeque<String>,
30-
}
31-
32-
impl MilestoneTracker {
33-
fn take_completed(&mut self, name: &str) -> bool {
34-
// Keep unrelated completed milestones available for later calls. A PTY
35-
// read can contain more than the milestone currently being requested.
36-
self.completed
37-
.iter()
38-
.position(|completed| completed == name)
39-
.and_then(|index| self.completed.remove(index))
40-
.is_some()
41-
}
42-
}
43-
44-
impl vte::Perform for MilestoneTracker {
45-
fn print(&mut self, character: char) {
46-
// `print` is called only for rendered characters, not for bytes inside
47-
// OSC metadata. ConPTY preserves the order of these rendered anchors,
48-
// so each anchor completes the oldest marker still awaiting one.
49-
if character == MILESTONE_HYPERTEXT
50-
&& let Some(name) = self.awaiting_fence.pop_front()
51-
{
52-
self.completed.push_back(name);
53-
}
54-
}
55-
56-
fn osc_dispatch(&mut self, params: &[&[u8]], _bell_terminated: bool) {
57-
// The decoder accepts only milestone hyperlink opens. Ordinary OSC
58-
// sequences and the empty OSC 8 close sequence are ignored.
59-
if let Some(name) = pty_terminal_test_client::decode_milestone_from_osc8_params(params) {
60-
self.awaiting_fence.push_back(name);
61-
}
62-
}
63-
}
64-
6513
/// A test-oriented terminal that provides milestone-based synchronization.
6614
///
6715
/// Wraps a PTY terminal, splitting it into a [`PtyWriter`] for sending input
@@ -75,17 +23,7 @@ pub struct TestTerminal {
7523

7624
/// The read half of a test terminal, wrapping [`PtyReader`] with milestone support.
7725
pub struct Reader {
78-
/// Reads bytes and updates the terminal's primary `vt100` screen parser.
79-
///
80-
/// This is deliberately not wrapped in `BufReader`: its read-ahead would
81-
/// let the primary parser consume bytes the milestone parser has not seen.
82-
pty: PtyReader,
83-
/// Observes the same byte stream to distinguish OSC markers from printable
84-
/// anchors. `vt100::Callbacks` exposes unhandled OSC sequences but has no
85-
/// callback for ordinary rendered characters, hence this small second parser.
86-
milestone_parser: vte::Parser,
87-
/// Persists protocol state across reads and `expect_milestone` calls.
88-
milestone_tracker: MilestoneTracker,
26+
pty: BufReader<PtyReader>,
8927
child_handle: ChildHandle,
9028
}
9129

@@ -99,38 +37,17 @@ impl TestTerminal {
9937
let Terminal { pty_reader, pty_writer, child_handle, .. } = Terminal::spawn(size, cmd)?;
10038
Ok(Self {
10139
writer: pty_writer,
102-
reader: Reader {
103-
pty: pty_reader,
104-
milestone_parser: vte::Parser::new(),
105-
milestone_tracker: MilestoneTracker::default(),
106-
child_handle: child_handle.clone(),
107-
},
40+
reader: Reader { pty: BufReader::new(pty_reader), child_handle: child_handle.clone() },
10841
child_handle,
10942
})
11043
}
11144
}
11245

11346
impl Reader {
114-
/// Reads once while keeping the screen parser and milestone parser in lockstep.
115-
///
116-
/// All PTY draining, including shutdown, must go through this method. Reading
117-
/// directly from `pty` would update the screen while silently skipping those
118-
/// bytes in the milestone protocol state.
119-
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
120-
let n = self.pty.read(buf)?;
121-
self.milestone_parser.advance(&mut self.milestone_tracker, &buf[..n]);
122-
123-
// `PtyReader`'s primary parser also records the OSC sequences. The
124-
// dedicated tracker above owns milestone handling, so discard this
125-
// duplicate copy rather than letting it grow for the lifetime of a test.
126-
drop(self.pty.take_unhandled_osc_sequences());
127-
Ok(n)
128-
}
129-
13047
/// Returns terminal screen contents with milestone hyperlink text removed.
13148
#[must_use]
13249
pub fn screen_contents(&self) -> String {
133-
let mut contents = self.pty.screen_contents();
50+
let mut contents = self.pty.get_ref().screen_contents();
13451
contents.retain(|ch| ch != MILESTONE_HYPERTEXT);
13552
contents
13653
}
@@ -139,20 +56,18 @@ impl Reader {
13956
/// Useful for snapshot tests that need to assert colour or style attributes.
14057
#[must_use]
14158
pub fn screen_contents_formatted(&self) -> Vec<u8> {
142-
self.pty.screen_contents_formatted()
59+
self.pty.get_ref().screen_contents_formatted()
14360
}
14461

14562
/// Reads from the PTY until a milestone with the given name is encountered.
14663
///
14764
/// Returns the terminal screen contents at the moment the milestone is detected.
14865
///
14966
/// Milestones use a uniform protocol across platforms: the milestone name
150-
/// is encoded in an OSC 8 hyperlink URI. A zero-width hyperlink anchor follows
151-
/// each marker through the rendered output path. The reader waits for both the
152-
/// marker and its corresponding anchor before returning, then strips the anchor
153-
/// from the returned screen contents. Marker and anchor parsing is incremental,
154-
/// so either sequence may be split across PTY reads or share a read with other
155-
/// milestones.
67+
/// is encoded in an OSC 8 hyperlink URI. We parse unhandled OSC sequences
68+
/// from the VT parser state (instead of raw byte matching), then decode the
69+
/// milestone URI payload. The zero-width milestone hyperlink anchor is
70+
/// stripped from returned screen contents.
15671
///
15772
/// # Panics
15873
///
@@ -163,11 +78,20 @@ impl Reader {
16378
let mut buf = [0u8; 4096];
16479

16580
loop {
166-
if self.milestone_tracker.take_completed(name) {
81+
let found = self
82+
.pty
83+
.get_ref()
84+
.take_unhandled_osc_sequences()
85+
.into_iter()
86+
.filter_map(|params| {
87+
pty_terminal_test_client::decode_milestone_from_osc8_params(&params)
88+
})
89+
.any(|decoded| decoded == name);
90+
if found {
16791
return self.screen_contents();
16892
}
16993

170-
let n = self.read(&mut buf).expect("PTY read failed");
94+
let n = self.pty.read(&mut buf).expect("PTY read failed");
17195
assert!(n > 0, "EOF reached before milestone '{name}'");
17296
}
17397
}
@@ -182,75 +106,8 @@ impl Reader {
182106
///
183107
/// Panics if reading from the PTY fails.
184108
pub fn wait_for_exit(&mut self) -> anyhow::Result<ExitStatus> {
185-
let mut buf = [0u8; 4096];
186-
while self.read(&mut buf).expect("PTY read failed") > 0 {}
109+
let mut discard = Vec::new();
110+
self.pty.read_to_end(&mut discard).expect("PTY read_to_end failed");
187111
self.child_handle.wait()
188112
}
189113
}
190-
191-
#[cfg(test)]
192-
mod tests {
193-
use super::*;
194-
195-
fn marker_without_fence(name: &str) -> Vec<u8> {
196-
// Model ConPTY's fast control path by delivering the complete OSC marker
197-
// before its printable anchor reaches the output pipe.
198-
let mut marker = pty_terminal_test_client::encoded_milestone(name);
199-
let index = marker
200-
.windows(pty_terminal_test_client::MILESTONE_RENDER_FENCE.len())
201-
.position(|window| window == pty_terminal_test_client::MILESTONE_RENDER_FENCE)
202-
.unwrap();
203-
marker.drain(index..index + pty_terminal_test_client::MILESTONE_RENDER_FENCE.len());
204-
marker
205-
}
206-
207-
fn advance(parser: &mut vte::Parser, tracker: &mut MilestoneTracker, bytes: &[u8]) {
208-
parser.advance(tracker, bytes);
209-
}
210-
211-
#[test]
212-
fn milestone_waits_for_rendered_fence() {
213-
let mut parser = vte::Parser::new();
214-
let mut tracker = MilestoneTracker::default();
215-
216-
// Receiving the marker and subsequent printable output is insufficient:
217-
// only the protocol's rendered anchor establishes the screen barrier.
218-
advance(&mut parser, &mut tracker, &marker_without_fence("target"));
219-
advance(&mut parser, &mut tracker, b"rendered output");
220-
assert!(!tracker.take_completed("target"));
221-
222-
advance(&mut parser, &mut tracker, pty_terminal_test_client::MILESTONE_RENDER_FENCE);
223-
assert!(tracker.take_completed("target"));
224-
}
225-
226-
#[test]
227-
fn milestone_parses_across_every_chunk_boundary() {
228-
let marker = pty_terminal_test_client::encoded_milestone("target");
229-
230-
for split in 0..=marker.len() {
231-
let mut parser = vte::Parser::new();
232-
let mut tracker = MilestoneTracker::default();
233-
advance(&mut parser, &mut tracker, &marker[..split]);
234-
advance(&mut parser, &mut tracker, &marker[split..]);
235-
assert!(tracker.take_completed("target"), "failed at split {split}");
236-
}
237-
}
238-
239-
#[test]
240-
fn rendered_fences_complete_overtaken_markers_in_order() {
241-
let mut parser = vte::Parser::new();
242-
let mut tracker = MilestoneTracker::default();
243-
let mut markers = marker_without_fence("first");
244-
markers.extend(marker_without_fence("second"));
245-
246-
// Both controls overtake rendering. The first anchor must still complete
247-
// `first`, never whichever marker the caller happens to be waiting for.
248-
advance(&mut parser, &mut tracker, &markers);
249-
advance(&mut parser, &mut tracker, pty_terminal_test_client::MILESTONE_RENDER_FENCE);
250-
assert!(tracker.take_completed("first"));
251-
assert!(!tracker.take_completed("second"));
252-
253-
advance(&mut parser, &mut tracker, pty_terminal_test_client::MILESTONE_RENDER_FENCE);
254-
assert!(tracker.take_completed("second"));
255-
}
256-
}

crates/pty_terminal_test_client/src/lib.rs

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,7 @@ const OSC_ST: &str = "\x1b\\";
55
/// Invisible hyperlink text anchor.
66
const MILESTONE_HYPERTEXT: &str = "\u{200b}";
77
/// OSC 8 close sequence.
8-
///
9-
/// This terminates hyperlink metadata only. It is a control sequence and does
10-
/// not guarantee that `ConPTY` has emitted preceding rendered text.
118
pub const MILESTONE_FENCE: &[u8] = b"\x1b]8;;\x1b\\";
12-
/// Zero-width printable fence that follows each milestone marker.
13-
///
14-
/// Unlike the OSC control sequences, `ConPTY` emits this character through its
15-
/// rendering path. Observing it therefore confirms that earlier rendered text
16-
/// has reached the reader.
17-
pub const MILESTONE_RENDER_FENCE: &[u8] = MILESTONE_HYPERTEXT.as_bytes();
189

1910
/// Builds an OSC 8 marker with milestone name encoded in the hyperlink URI.
2011
///
@@ -54,12 +45,12 @@ const fn decode_hex_nibble(byte: u8) -> Option<u8> {
5445
/// Returns `Some(name)` only when the URI uses the milestone prefix and the
5546
/// suffix is valid hex-encoded UTF-8.
5647
#[must_use]
57-
pub fn decode_milestone_from_osc8_params<T: AsRef<[u8]>>(params: &[T]) -> Option<String> {
58-
if params.first().is_none_or(|p| p.as_ref() != b"8") {
48+
pub fn decode_milestone_from_osc8_params(params: &[Vec<u8>]) -> Option<String> {
49+
if params.first().is_none_or(|p| p.as_slice() != b"8") {
5950
return None;
6051
}
6152

62-
let uri = params.get(2)?.as_ref();
53+
let uri = params.get(2)?.as_slice();
6354
let encoded = uri.strip_prefix(MILESTONE_URI_PREFIX.as_bytes())?;
6455
if encoded.is_empty() || encoded.len() % 2 != 0 {
6556
return None;
@@ -89,8 +80,7 @@ pub fn decode_milestone_from_osc8_params<T: AsRef<[u8]>>(params: &[T]) -> Option
8980
///
9081
/// Milestones include a zero-width hyperlink anchor (`U+200B`) before closing.
9182
/// This keeps the hyperlink metadata observable in `ConPTY` output paths that can
92-
/// drop zero-length hyperlinks. The test harness also waits for this rendered
93-
/// character so preceding screen output cannot be overtaken by the OSC marker.
83+
/// drop zero-length hyperlinks.
9484
///
9585
/// When the `testing` feature is disabled, this is a no-op.
9686
///
@@ -103,8 +93,7 @@ pub fn mark_milestone(name: &str) {
10393

10494
let milestone = encoded_milestone(name);
10595
let mut stdout = stdout();
106-
// Flush prior output before emitting the marker. On ConPTY this flush alone
107-
// is not a rendering barrier; the reader waits for MILESTONE_RENDER_FENCE.
96+
// Flush prior output, then emit milestone sequence.
10897
stdout.flush().unwrap();
10998
stdout.write_all(&milestone).unwrap();
11099

0 commit comments

Comments
 (0)