Skip to content

Commit d4beb64

Browse files
authored
PHD: add Windows Server 2016 adapter & improve WS2016/2019 reliability (#646)
Add a Windows Server 2016 guest adapter to PHD. It's more or less identical to the Windows Server 2019 adapter (but is still worth keeping separate since the guests support different command-line tools and Powershell cmdlets out of the box). Tighten a few bolts to improve (but not perfect) test performance and reliability for these guests: - Rework the way shell commands are issued: - `GuestOs`es now specify a serial console `CommandSequence` that the framework should execute in order to run a shell command. - Waiting for a string to appear in the serial console buffer no longer consumes any characters from the buffer. Instead, callers must clear the buffer manually. With this change, it no longer matters that Windows guests don't re-echo characters that are already on-screen, because the framework no longer invalidates them until it knows they're no longer wanted. Additionally improve the keystroke debounce logic: - Only apply the debounce delay if the next character to be issued is the same as the previous character. This allows the debounce timeout to be lengthened considerably without tanking test performance (which is important to minimize flakiness). It might be worth investigating whether we could avoid this delay by doing byte-by-byte "call and response" for characters we expect the guest to echo. This may require a more substantial refactoring of the serial console code, so I've just resorted to having a longer debounce for now. - Change `TestVm::send_serial_bytes_async` to wait for the serial task to acknowledge that it has sent all the requested bytes to the guest before returning so that `TestVm` can time out waiting for characters to be echoed without including the time spent waiting for them to be sent. Tested with WS2016, WS2019, Alpine, and Debian 11 guests.
1 parent ffaf0fb commit d4beb64

12 files changed

+335
-178
lines changed

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

+10-3
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ pub(super) struct Alpine;
1111
impl GuestOs for Alpine {
1212
fn get_login_sequence(&self) -> CommandSequence {
1313
CommandSequence(vec![
14-
CommandSequenceEntry::WaitFor("localhost login: "),
15-
CommandSequenceEntry::WriteStr("root"),
16-
CommandSequenceEntry::WaitFor(self.get_shell_prompt()),
14+
CommandSequenceEntry::wait_for("localhost login: "),
15+
CommandSequenceEntry::write_str("root"),
16+
CommandSequenceEntry::wait_for(self.get_shell_prompt()),
1717
])
1818
}
1919

@@ -24,4 +24,11 @@ impl GuestOs for Alpine {
2424
fn read_only_fs(&self) -> bool {
2525
true
2626
}
27+
28+
fn shell_command_sequence<'a>(&self, cmd: &'a str) -> CommandSequence<'a> {
29+
super::shell_commands::shell_command_sequence(
30+
std::borrow::Cow::Borrowed(cmd),
31+
crate::serial::BufferKind::Raw,
32+
)
33+
}
2734
}

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ pub(super) struct Debian11NoCloud;
1111
impl GuestOs for Debian11NoCloud {
1212
fn get_login_sequence(&self) -> CommandSequence {
1313
CommandSequence(vec![
14-
CommandSequenceEntry::WaitFor("debian login: "),
15-
CommandSequenceEntry::WriteStr("root"),
16-
CommandSequenceEntry::WaitFor(self.get_shell_prompt()),
14+
CommandSequenceEntry::wait_for("debian login: "),
15+
CommandSequenceEntry::write_str("root"),
16+
CommandSequenceEntry::wait_for(self.get_shell_prompt()),
1717
])
1818
}
1919

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

+34-13
Original file line numberDiff line numberDiff line change
@@ -11,30 +11,45 @@ use serde::{Deserialize, Serialize};
1111

1212
mod alpine;
1313
mod debian11_nocloud;
14+
mod shell_commands;
1415
mod ubuntu22_04;
1516
mod windows;
17+
mod windows_server_2016;
1618
mod windows_server_2019;
1719
mod windows_server_2022;
1820

1921
/// An entry in a sequence of interactions with the guest's command prompt.
2022
#[derive(Debug)]
21-
pub(super) enum CommandSequenceEntry {
23+
pub(super) enum CommandSequenceEntry<'a> {
2224
/// Wait for the supplied string to appear on the guest serial console.
23-
WaitFor(&'static str),
25+
WaitFor(Cow<'a, str>),
2426

2527
/// Write the specified string as a command to the guest serial console.
26-
WriteStr(&'static str),
28+
WriteStr(Cow<'a, str>),
29+
30+
/// Tell the serial console task to clear its buffer.
31+
ClearBuffer,
2732

2833
/// Change the serial console buffering discipline to the supplied
2934
/// discipline.
3035
ChangeSerialConsoleBuffer(crate::serial::BufferKind),
3136

32-
/// Set a delay between writing individual bytes to the guest serial console
37+
/// Set a delay between writing identical bytes to the guest serial console
3338
/// to avoid keyboard debouncing logic in guests.
34-
SetSerialByteWriteDelay(std::time::Duration),
39+
SetRepeatedCharacterDebounce(std::time::Duration),
3540
}
3641

37-
pub(super) struct CommandSequence(pub Vec<CommandSequenceEntry>);
42+
impl<'a> CommandSequenceEntry<'a> {
43+
fn write_str(s: impl Into<Cow<'a, str>>) -> Self {
44+
Self::WriteStr(s.into())
45+
}
46+
47+
fn wait_for(s: impl Into<Cow<'a, str>>) -> Self {
48+
Self::WaitFor(s.into())
49+
}
50+
}
51+
52+
pub(super) struct CommandSequence<'a>(pub Vec<CommandSequenceEntry<'a>>);
3853

3954
pub(super) trait GuestOs: Send + Sync {
4055
/// Retrieves the command sequence used to wait for the OS to boot and log
@@ -47,13 +62,13 @@ pub(super) trait GuestOs: Send + Sync {
4762
/// Indicates whether the guest has a read-only filesystem.
4863
fn read_only_fs(&self) -> bool;
4964

50-
/// Some guests need to amend incoming shell commands from tests in order to
51-
/// get output to display on the serial console in a way those guests can
52-
/// accept (e.g. by clearing the screen immediately before running each
53-
/// command). This function amends an incoming command according to the
54-
/// guest adapter's instructions.
55-
fn amend_shell_command<'a>(&self, cmd: &'a str) -> Cow<'a, str> {
56-
Cow::Borrowed(cmd)
65+
/// Returns the sequence of serial console operations a test VM should issue
66+
/// in order to execute `cmd` in the guest's shell.
67+
fn shell_command_sequence<'a>(&self, cmd: &'a str) -> CommandSequence<'a> {
68+
shell_commands::shell_command_sequence(
69+
Cow::Borrowed(cmd),
70+
crate::serial::BufferKind::Raw,
71+
)
5772
}
5873
}
5974

@@ -64,6 +79,7 @@ pub enum GuestOsKind {
6479
Alpine,
6580
Debian11NoCloud,
6681
Ubuntu2204,
82+
WindowsServer2016,
6783
WindowsServer2019,
6884
WindowsServer2022,
6985
}
@@ -76,6 +92,8 @@ impl FromStr for GuestOsKind {
7692
"alpine" => Ok(Self::Alpine),
7793
"debian11nocloud" => Ok(Self::Debian11NoCloud),
7894
"ubuntu2204" => Ok(Self::Ubuntu2204),
95+
"windowsserver2016" => Ok(Self::WindowsServer2016),
96+
"windowsserver2019" => Ok(Self::WindowsServer2019),
7997
"windowsserver2022" => Ok(Self::WindowsServer2022),
8098
_ => Err(std::io::Error::new(
8199
std::io::ErrorKind::InvalidInput,
@@ -92,6 +110,9 @@ pub(super) fn get_guest_os_adapter(kind: GuestOsKind) -> Box<dyn GuestOs> {
92110
Box::new(debian11_nocloud::Debian11NoCloud)
93111
}
94112
GuestOsKind::Ubuntu2204 => Box::new(ubuntu22_04::Ubuntu2204),
113+
GuestOsKind::WindowsServer2016 => {
114+
Box::new(windows_server_2016::WindowsServer2016)
115+
}
95116
GuestOsKind::WindowsServer2019 => {
96117
Box::new(windows_server_2019::WindowsServer2019)
97118
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
//! Common helper functions for issuing shell commands to guests and handling
6+
//! their outputs.
7+
8+
use std::borrow::Cow;
9+
10+
use super::{CommandSequence, CommandSequenceEntry};
11+
12+
/// Produces the shell command sequence necessary to execute `cmd` in a guest's
13+
/// shell, given that the guest is using the supplied serial console buffering
14+
/// discipline.
15+
///
16+
/// This routine assumes that multi-line commands will be echoed with `> ` at
17+
/// the start of each line in the command. This is technically shell-dependent
18+
/// but is true for all the shell types in PHD's currently-supported guests.
19+
pub(super) fn shell_command_sequence(
20+
cmd: Cow<'_, str>,
21+
buffer_kind: crate::serial::BufferKind,
22+
) -> CommandSequence {
23+
let echo = cmd.trim_end().replace('\n', "\n> ");
24+
match buffer_kind {
25+
crate::serial::BufferKind::Raw => CommandSequence(vec![
26+
CommandSequenceEntry::write_str(cmd),
27+
CommandSequenceEntry::wait_for(echo),
28+
CommandSequenceEntry::ClearBuffer,
29+
CommandSequenceEntry::write_str("\n"),
30+
]),
31+
32+
crate::serial::BufferKind::Vt80x24 => {
33+
// In 80x24 mode, it's simplest to issue multi-line operations one
34+
// line at a time and wait for each line to be echoed before
35+
// starting the next. For very long commands (more than 24 lines),
36+
// this avoids having to deal with lines scrolling off the buffer
37+
// before they can be waited for.
38+
let cmd_lines = cmd.trim_end().lines();
39+
let echo_lines = echo.lines();
40+
let mut seq = vec![];
41+
42+
let mut iter = cmd_lines.zip(echo_lines).peekable();
43+
while let Some((cmd, echo)) = iter.next() {
44+
seq.push(CommandSequenceEntry::write_str(cmd.to_owned()));
45+
seq.push(CommandSequenceEntry::wait_for(echo.to_owned()));
46+
47+
if iter.peek().is_some() {
48+
seq.push(CommandSequenceEntry::write_str("\n"));
49+
}
50+
}
51+
52+
// Before issuing the command, clear any stale echoed characters
53+
// from the serial console buffer. This ensures that the next prompt
54+
// is preceded in the buffer only by the output of the issued
55+
// command.
56+
seq.push(CommandSequenceEntry::ClearBuffer);
57+
seq.push(CommandSequenceEntry::write_str("\n"));
58+
CommandSequence(seq)
59+
}
60+
}
61+
}

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

+5-5
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ pub(super) struct Ubuntu2204;
1212
impl GuestOs for Ubuntu2204 {
1313
fn get_login_sequence(&self) -> CommandSequence {
1414
CommandSequence(vec![
15-
CommandSequenceEntry::WaitFor("ubuntu login: "),
16-
CommandSequenceEntry::WriteStr("ubuntu"),
17-
CommandSequenceEntry::WaitFor("Password: "),
18-
CommandSequenceEntry::WriteStr("1!Passw0rd"),
19-
CommandSequenceEntry::WaitFor(self.get_shell_prompt()),
15+
CommandSequenceEntry::wait_for("ubuntu login: "),
16+
CommandSequenceEntry::write_str("ubuntu"),
17+
CommandSequenceEntry::wait_for("Password: "),
18+
CommandSequenceEntry::write_str("1!Passw0rd"),
19+
CommandSequenceEntry::wait_for(self.get_shell_prompt()),
2020
])
2121
}
2222

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

+43-35
Original file line numberDiff line numberDiff line change
@@ -14,52 +14,60 @@ use super::{CommandSequence, CommandSequenceEntry, GuestOsKind};
1414
/// - Cygwin is installed to C:\cygwin and can be launched by invoking
1515
/// C:\cygwin\cygwin.bat.
1616
/// - The local administrator account is enabled with password `0xide#1Fan`.
17-
pub(super) fn get_login_sequence_for(guest: GuestOsKind) -> CommandSequence {
17+
pub(super) fn get_login_sequence_for<'a>(
18+
guest: GuestOsKind,
19+
) -> CommandSequence<'a> {
1820
assert!(matches!(
1921
guest,
20-
GuestOsKind::WindowsServer2019 | GuestOsKind::WindowsServer2022
22+
GuestOsKind::WindowsServer2016
23+
| GuestOsKind::WindowsServer2019
24+
| GuestOsKind::WindowsServer2022
2125
));
2226

2327
let mut commands = vec![
24-
CommandSequenceEntry::WaitFor(
28+
CommandSequenceEntry::wait_for(
2529
"Computer is booting, SAC started and initialized.",
2630
),
27-
CommandSequenceEntry::WaitFor(
31+
CommandSequenceEntry::wait_for(
2832
"EVENT: The CMD command is now available.",
2933
),
30-
CommandSequenceEntry::WaitFor("SAC>"),
31-
CommandSequenceEntry::WriteStr("cmd"),
32-
CommandSequenceEntry::WaitFor("Channel: Cmd0001"),
33-
CommandSequenceEntry::WaitFor("SAC>"),
34-
CommandSequenceEntry::WriteStr("ch -sn Cmd0001"),
35-
CommandSequenceEntry::WaitFor(
34+
CommandSequenceEntry::wait_for("SAC>"),
35+
CommandSequenceEntry::write_str("cmd"),
36+
CommandSequenceEntry::wait_for("Channel: Cmd0001"),
37+
CommandSequenceEntry::wait_for("SAC>"),
38+
CommandSequenceEntry::write_str("ch -sn Cmd0001"),
39+
CommandSequenceEntry::wait_for(
3640
"Use any other key to view this channel.",
3741
),
38-
CommandSequenceEntry::WriteStr(""),
39-
CommandSequenceEntry::WaitFor("Username:"),
40-
CommandSequenceEntry::WriteStr("Administrator"),
41-
CommandSequenceEntry::WaitFor("Domain :"),
42-
CommandSequenceEntry::WriteStr(""),
43-
CommandSequenceEntry::WaitFor("Password:"),
44-
CommandSequenceEntry::WriteStr("0xide#1Fan"),
42+
CommandSequenceEntry::write_str(""),
43+
CommandSequenceEntry::wait_for("Username:"),
44+
CommandSequenceEntry::write_str("Administrator"),
45+
CommandSequenceEntry::wait_for("Domain :"),
46+
CommandSequenceEntry::write_str(""),
47+
CommandSequenceEntry::wait_for("Password:"),
48+
CommandSequenceEntry::write_str("0xide#1Fan"),
4549
];
4650

47-
// Windows Server 2019's serial console-based command prompts default to
48-
// trying to drive a VT100 terminal themselves instead of emitting
49-
// characters and letting the recipient display them in whatever style it
50-
// likes. This only happens once the command prompt has been activated, so
51-
// only switch buffering modes after entering credentials.
52-
if let GuestOsKind::WindowsServer2019 = guest {
51+
// Earlier Windows Server versions' serial console-based command prompts
52+
// default to trying to drive a VT100 terminal themselves instead of
53+
// emitting characters and letting the recipient display them in whatever
54+
// style it likes. This only happens once the command prompt has been
55+
// activated, so only switch buffering modes after entering credentials.
56+
if matches!(
57+
guest,
58+
GuestOsKind::WindowsServer2016 | GuestOsKind::WindowsServer2019
59+
) {
5360
commands.extend([
5461
CommandSequenceEntry::ChangeSerialConsoleBuffer(
5562
crate::serial::BufferKind::Vt80x24,
5663
),
57-
// Server 2019 also likes to debounce keystrokes, so set a small
58-
// delay between characters to try to avoid this. (This value was
59-
// chosen by experimentation; there doesn't seem to be a guest
60-
// setting that controls this interval.)
61-
CommandSequenceEntry::SetSerialByteWriteDelay(
62-
std::time::Duration::from_millis(125),
64+
// These versions also like to debounce keystrokes, so set a delay
65+
// between repeated characters to try to avoid this. This is a very
66+
// conservative delay to try to avoid test flakiness; fortunately,
67+
// it only applies when typing the same character multiple times in
68+
// a row.
69+
CommandSequenceEntry::SetRepeatedCharacterDebounce(
70+
std::time::Duration::from_secs(1),
6371
),
6472
]);
6573
}
@@ -70,14 +78,14 @@ pub(super) fn get_login_sequence_for(guest: GuestOsKind) -> CommandSequence {
7078
// eat the command and just process the newline). It also appears to
7179
// prefer carriage returns to linefeeds. Accommodate this behavior
7280
// until Cygwin is launched.
73-
CommandSequenceEntry::WaitFor("C:\\Windows\\system32>"),
74-
CommandSequenceEntry::WriteStr("cls\r"),
75-
CommandSequenceEntry::WaitFor("C:\\Windows\\system32>"),
76-
CommandSequenceEntry::WriteStr("C:\\cygwin\\cygwin.bat\r"),
77-
CommandSequenceEntry::WaitFor("$ "),
81+
CommandSequenceEntry::wait_for("C:\\Windows\\system32>"),
82+
CommandSequenceEntry::write_str("cls\r"),
83+
CommandSequenceEntry::wait_for("C:\\Windows\\system32>"),
84+
CommandSequenceEntry::write_str("C:\\cygwin\\cygwin.bat\r"),
85+
CommandSequenceEntry::wait_for("$ "),
7886
// Tweak the command prompt so that it appears on a single line with
7987
// no leading newlines.
80-
CommandSequenceEntry::WriteStr("PS1='\\u@\\h:$ '"),
88+
CommandSequenceEntry::write_str("PS1='\\u@\\h:$ '"),
8189
]);
8290

8391
CommandSequence(commands)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
// This Source Code Form is subject to the terms of the Mozilla Public
6+
// License, v. 2.0. If a copy of the MPL was not distributed with this
7+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
8+
9+
//! Guest OS adaptations for Windows Server 2016 images. See [the general
10+
//! Windows module](mod@super::windows) documentation for more information.
11+
12+
use std::borrow::Cow;
13+
14+
use super::{CommandSequence, GuestOs, GuestOsKind};
15+
16+
/// The guest adapter for Windows Server 2016 images. See [the general
17+
/// Windows module](mod@super::windows) documentation for more information about
18+
/// the configuration this adapter requires.
19+
pub(super) struct WindowsServer2016;
20+
21+
impl GuestOs for WindowsServer2016 {
22+
fn get_login_sequence(&self) -> CommandSequence {
23+
super::windows::get_login_sequence_for(GuestOsKind::WindowsServer2016)
24+
}
25+
26+
fn get_shell_prompt(&self) -> &'static str {
27+
"Administrator@PHD-WINDOWS:$ "
28+
}
29+
30+
fn read_only_fs(&self) -> bool {
31+
false
32+
}
33+
34+
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.
39+
let cmd = format!("reset && {cmd}");
40+
super::shell_commands::shell_command_sequence(
41+
Cow::Owned(cmd),
42+
crate::serial::BufferKind::Vt80x24,
43+
)
44+
}
45+
}

0 commit comments

Comments
 (0)