Skip to content

Commit bb39823

Browse files
committed
PHD: make callers consume serial buffer by hand
Change the semantics of waiting for characters in the serial buffer so that waits return the preceding buffer contents but don't actually erase anything from the buffer. Instead, make callers manually issue a "clear" command to discard from the buffer anything they know they're not going to want, and update the guest shell command sequences to clear the buffer between echoing the entire command and pressing Enter to execute it. This fixes issues with command echoing for Windows guests that don't redraw characters that the guest believes are already on the screen--now those characters don't get consumed from the buffer, so it's OK that the guest doesn't explicitly reprint them.
1 parent 73f52ee commit bb39823

File tree

8 files changed

+73
-56
lines changed

8 files changed

+73
-56
lines changed

phd-tests/framework/src/guest_os/mod.rs

+3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ pub(super) enum CommandSequenceEntry<'a> {
2727
/// Write the specified string as a command to the guest serial console.
2828
WriteStr(Cow<'a, str>),
2929

30+
/// Tell the serial console task to clear its buffer.
31+
ClearBuffer,
32+
3033
/// Change the serial console buffering discipline to the supplied
3134
/// discipline.
3235
ChangeSerialConsoleBuffer(crate::serial::BufferKind),

phd-tests/framework/src/guest_os/shell_commands.rs

+13-2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ pub(super) fn shell_command_sequence(
2525
crate::serial::BufferKind::Raw => CommandSequence(vec![
2626
CommandSequenceEntry::WriteStr(cmd),
2727
CommandSequenceEntry::WaitFor(echo.into()),
28+
CommandSequenceEntry::ClearBuffer,
2829
CommandSequenceEntry::WriteStr("\n".into()),
2930
]),
3031

@@ -37,12 +38,22 @@ pub(super) fn shell_command_sequence(
3738
let cmd_lines = cmd.trim_end().lines();
3839
let echo_lines = echo.lines();
3940
let mut seq = vec![];
40-
for (cmd, echo) in cmd_lines.zip(echo_lines) {
41+
42+
let mut iter = cmd_lines.zip(echo_lines).peekable();
43+
while let Some((cmd, echo)) = iter.next() {
4144
seq.push(CommandSequenceEntry::WriteStr(cmd.to_owned().into()));
4245
seq.push(CommandSequenceEntry::WaitFor(echo.to_owned().into()));
43-
seq.push(CommandSequenceEntry::WriteStr("\n".into()));
46+
47+
if iter.peek().is_some() {
48+
seq.push(CommandSequenceEntry::WriteStr("\n".into()));
49+
}
4450
}
4551

52+
// Before issuing the command, clear any stale echoed characters
53+
// from the serial console buffer. This ensures that the
54+
seq.push(CommandSequenceEntry::ClearBuffer);
55+
seq.push(CommandSequenceEntry::WriteStr("\n".into()));
56+
4657
CommandSequence(seq)
4758
}
4859
}

phd-tests/framework/src/guest_os/windows_server_2016.rs

+4
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ impl GuestOs for WindowsServer2016 {
3232
}
3333

3434
fn shell_command_sequence<'a>(&self, cmd: &'a str) -> CommandSequence<'a> {
35+
// `reset` the command prompt before issuing the command to try to force
36+
// Windows to redraw the subsequent command prompt. Without this,
37+
// Windows may not draw the prompt if the post-command state happens to
38+
// place a prompt at a location that already had one pre-command.
3539
let cmd = format!("reset && {cmd}");
3640
super::shell_commands::shell_command_sequence(
3741
Cow::Owned(cmd),

phd-tests/framework/src/guest_os/windows_server_2019.rs

+4
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ impl GuestOs for WindowsServer2019 {
2828
}
2929

3030
fn shell_command_sequence<'a>(&self, cmd: &'a str) -> CommandSequence<'a> {
31+
// `reset` the command prompt before issuing the command to try to force
32+
// Windows to redraw the subsequent command prompt. Without this,
33+
// Windows may not draw the prompt if the post-command state happens to
34+
// place a prompt at a location that already had one pre-command.
3135
let cmd = format!("reset && {cmd}");
3236
super::shell_commands::shell_command_sequence(
3337
Cow::Owned(cmd),

phd-tests/framework/src/serial/mod.rs

+21-4
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ trait Buffer: Send {
3535
/// Processes the supplied `bytes` as input to the buffer.
3636
fn process_bytes(&mut self, bytes: &[u8]);
3737

38+
/// Clears the unprocessed contents of the buffer.
39+
fn clear(&mut self);
40+
3841
/// Registers a new request to wait for a string to appear in the buffer.
3942
fn register_wait_for_output(&mut self, waiter: OutputWaiter);
4043

@@ -61,6 +64,10 @@ enum TaskCommand {
6164
/// Send the supplied bytes to the VM.
6265
SendBytes { bytes: Vec<u8>, done: oneshot::Sender<()> },
6366

67+
/// Clears the contents of the task's console buffer. This does not cancel
68+
/// the active wait, if there is one.
69+
Clear,
70+
6471
/// Register to be notified if and when a supplied string appears in the
6572
/// serial console's buffer.
6673
RegisterWait(OutputWaiter),
@@ -117,12 +124,21 @@ impl SerialConsole {
117124
Ok(())
118125
}
119126

127+
/// Directs the console worker thread to clear the serial console buffer.
128+
pub fn clear(&self) -> anyhow::Result<()> {
129+
self.cmd_tx.send(TaskCommand::Clear)?;
130+
Ok(())
131+
}
132+
120133
/// Registers with the current buffer a request to wait for `wanted` to
121134
/// appear in the console buffer. When a match is found, the buffer sends
122-
/// all buffered characters preceding the match to `preceding_tx`, consuming
123-
/// those characters and the matched string. If the buffer already contains
124-
/// one or more matches at the time the waiter is registered, the last match
125-
/// is used to satisfy the wait immediately.
135+
/// all buffered characters preceding the match to `preceding_tx`. If the
136+
/// buffer already contains one or more matches at the time the waiter is
137+
/// registered, the last match is used to satisfy the wait immediately.
138+
///
139+
/// Note that this function *does not* clear any characters from the buffer.
140+
/// Callers who want to retire previously-echoed characters in the buffer
141+
/// must explicitly call `clear`.
126142
pub fn register_wait_for_string(
127143
&self,
128144
wanted: String,
@@ -225,6 +241,7 @@ async fn serial_task(
225241

226242
let _ = done.send(());
227243
}
244+
TaskCommand::Clear => buffer.clear(),
228245
TaskCommand::RegisterWait(waiter) => {
229246
buffer.register_wait_for_output(waiter);
230247
}

phd-tests/framework/src/serial/raw_buffer.rs

+6-7
Original file line numberDiff line numberDiff line change
@@ -87,19 +87,14 @@ impl RawBuffer {
8787
fn satisfy_or_set_wait(&mut self, waiter: OutputWaiter) {
8888
assert!(self.waiter.is_none());
8989
if let Some(idx) = self.wait_buffer.rfind(&waiter.wanted) {
90-
// Send all of the data in the buffer prior to the target string
91-
// out the waiter's channel.
92-
//
90+
let out = self.wait_buffer[..idx].to_owned();
91+
9392
// Because incoming bytes from Propolis may be processed on a
9493
// separate task than the task that registered the wait, this
9594
// can race such that the wait is satisfied just as the waiter
9695
// times out and closes its half of the channel. There's nothing
9796
// to be done about this, so just ignore any errors here.
98-
let out = self.wait_buffer.drain(..idx).collect();
9997
let _ = waiter.preceding_tx.send(out);
100-
101-
// Clear the matched string out of the wait buffer.
102-
self.wait_buffer = self.wait_buffer.split_off(waiter.wanted.len());
10398
} else {
10499
self.waiter = Some(waiter);
105100
}
@@ -126,6 +121,10 @@ impl Buffer for RawBuffer {
126121
}
127122
}
128123

124+
fn clear(&mut self) {
125+
self.wait_buffer.clear();
126+
}
127+
129128
fn register_wait_for_output(&mut self, waiter: OutputWaiter) {
130129
self.satisfy_or_set_wait(waiter);
131130
}

phd-tests/framework/src/serial/vt80x24.rs

+7-41
Original file line numberDiff line numberDiff line change
@@ -110,47 +110,6 @@ impl Vt80x24 {
110110
let mut contents = self.surface.screen_chars_to_string();
111111
trace!(?contents, "termwiz contents");
112112
if let Some(idx) = contents.rfind(&waiter.wanted) {
113-
// Callers who set waits assume that matched strings are consumed
114-
// from the buffer and won't match again unless the string is
115-
// rendered to the buffer again. In the raw buffering case, this is
116-
// straightforward: the buffer is already a String, so it suffices
117-
// just to split the string just after the match. In this case,
118-
// however, life is harder, because the buffer is not a string but a
119-
// collection of cells containing `char`s.
120-
//
121-
// To "consume" the buffer, overwrite everything up through the
122-
// match string with spaces. Start by truncating the current buffer
123-
// contents down to the substring that ends in the match.
124-
let last_byte = idx + waiter.wanted.len();
125-
contents.truncate(last_byte);
126-
127-
// Then move the cursor to the top left and "type" as many blank
128-
// spaces as there were characters in the buffer. Note that termwiz
129-
// inserts extra '\n' characters at the end of every line that need
130-
// to be ignored here. (It's assumed that none of the actual
131-
// terminal cells contain a '\n' control character--if one of those
132-
// is printed the cursor moves instead.)
133-
let char_count = contents.chars().filter(|c| *c != '\n').count();
134-
135-
// Before typing anything, remember the old cursor position so that
136-
// it can be restored after typing the spaces.
137-
//
138-
// It's insufficient to assume that the last character of the match
139-
// was actually "typed" such that the cursor advanced past it. For
140-
// example, if a match string ends with "$ ", and the guest moves to
141-
// the start of an empty line and types a single "$", the cursor is
142-
// in column 1 (just past the "$"), but the match is satisfied by
143-
// the "pre-existing" space in that column.
144-
let (old_col, old_row) = self.surface.cursor_position();
145-
self.surface.add_change(make_absolute_cursor_position(0, 0));
146-
self.surface.add_change(Change::Text(" ".repeat(char_count)));
147-
let seq = self
148-
.surface
149-
.add_change(make_absolute_cursor_position(old_col, old_row));
150-
self.surface.flush_changes_older_than(seq);
151-
152-
// Remove the match string from the match contents and push
153-
// everything else back to the listener.
154113
contents.truncate(idx);
155114
let _ = waiter.preceding_tx.send(contents);
156115
} else {
@@ -165,6 +124,13 @@ impl Buffer for Vt80x24 {
165124
self.apply_actions(&actions);
166125
}
167126

127+
fn clear(&mut self) {
128+
let seq = self
129+
.surface
130+
.add_change(Change::ClearScreen(ColorAttribute::Default));
131+
self.surface.flush_changes_older_than(seq);
132+
}
133+
168134
fn register_wait_for_output(&mut self, waiter: OutputWaiter) {
169135
self.satisfy_or_set_wait(waiter);
170136
}

phd-tests/framework/src/test_vm/mod.rs

+15-2
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,9 @@ impl TestVm {
659659
self.send_serial_str(s.as_ref()).await?;
660660
self.send_serial_str("\n").await?;
661661
}
662+
CommandSequenceEntry::ClearBuffer => {
663+
self.clear_serial_buffer()?
664+
}
662665
CommandSequenceEntry::ChangeSerialConsoleBuffer(kind) => {
663666
self.change_serial_buffer_kind(kind)?;
664667
}
@@ -686,8 +689,8 @@ impl TestVm {
686689
}
687690

688691
/// Waits for up to `timeout_duration` for `line` to appear on the guest
689-
/// serial console, then returns the unconsumed portion of the serial
690-
/// console buffer that preceded the requested string.
692+
/// serial console, then returns the contents of the console buffer that
693+
/// preceded the requested string.
691694
#[instrument(skip_all, fields(vm = self.spec.vm_name, vm_id = %self.id))]
692695
pub async fn wait_for_serial_output(
693696
&self,
@@ -746,6 +749,9 @@ impl TestVm {
746749
CommandSequenceEntry::WriteStr(s) => {
747750
self.send_serial_str(s.as_ref()).await?;
748751
}
752+
CommandSequenceEntry::ClearBuffer => {
753+
self.clear_serial_buffer()?
754+
}
749755
_ => {
750756
anyhow::bail!(
751757
"Unexpected command sequence entry {step:?} while \
@@ -792,6 +798,13 @@ impl TestVm {
792798
}
793799
}
794800

801+
fn clear_serial_buffer(&self) -> Result<()> {
802+
match &self.state {
803+
VmState::Ensured { serial } => serial.clear(),
804+
VmState::New => Err(VmStateError::InstanceNotEnsured.into()),
805+
}
806+
}
807+
795808
fn change_serial_buffer_kind(&self, kind: BufferKind) -> Result<()> {
796809
match &self.state {
797810
VmState::Ensured { serial } => serial.change_buffer_kind(kind),

0 commit comments

Comments
 (0)