Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions firecracker-pilot/guestvm-tools/sci/src/defaults.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ pub const PROBE_MODULE: &str = "/sbin/modprobe";
pub const SYSTEMD_NETWORK_RESOLV_CONF: &str = "/run/systemd/resolve/resolv.conf";
pub const VM_QUIT: &str = "sci_quit";
pub const VHOST_TRANSPORT: &str = "vmw_vsock_virtio_transport";
pub const TERM_TYPE: &str = "xterm";
pub const TERM_LINES: u16 = 24;
pub const TERM_COLUMNS: u16 = 80;
pub const VM_PORT: u32 = 52;
pub const GUEST_CID: u32 = 3;
pub const RETRIES: u32 =
Expand Down
124 changes: 120 additions & 4 deletions firecracker-pilot/guestvm-tools/sci/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ fn main() {

env::set_var("PS1", "\\[\\]\\u@\\h: >\n");

// provide a terminal type for the command call
setup_terminal_environment();

// print user space env
for (key, value) in env::vars() {
debug(&format!("{key}: {value}"));
Expand Down Expand Up @@ -430,6 +433,106 @@ fn redirect_command(command: &str, stream: vsock::VsockStream) {
}
}

fn setup_terminal_environment() {
/*!
Provide a terminal type in the environment

The environment of sci is created from the kernel commandline
and therefore normally does not provide a TERM setting. Shells
like bash switch off their line editor (readline) if the
terminal type is unset or set to 'dumb'. Without the line
editor there is no tab completion and no history handling for
the caller. The terminal type of the caller can be handed over
through the sci_term=... boot parameter and defaults to
defaults::TERM_TYPE
!*/
let term = env::var("TERM").unwrap_or_default();
if term.is_empty() {
let mut term_type = env::var("sci_term").unwrap_or_default();
if term_type.is_empty() {
term_type = defaults::TERM_TYPE.to_string()
}
debug(&format!("Setting terminal type to: {term_type}"));
env::set_var("TERM", term_type);
}
}

fn set_interactive_terminal_flags(fd: i32) {
/*!
Setup the given terminal for interactive use

Keep the standard line discipline of the terminal switched on
such that the line editor of an interactive command, e.g the
tab completion of a shell, stays in control of the input
handling and echoes back what it has read. The terminal of the
caller is switched to raw mode by the pilot, thus every single
key stroke, including TAB, arrives here unmodified
!*/
match Termios::from_fd(fd) {
Ok(mut termios) => {
termios.c_lflag |= ECHO | ECHOE | ECHOK | ICANON | ISIG | IEXTEN;
termios.c_iflag |= ICRNL;
termios.c_oflag |= OPOST | ONLCR;
match tcsetattr(fd, TCSANOW, &termios) {
Ok(_) => {}
Err(error) => {
debug(&format!("tcsetattr failed with: {error}"));
}
}
},
Err(error) => {
debug(&format!(
"Term I/O failed with: {error}"
));
}
}
set_terminal_size(fd)
}

fn set_terminal_size(fd: i32) {
/*!
Set the window size of the given terminal

A newly allocated pseudo terminal comes with no window size
assigned. The line editor needs the size of the caller's
terminal to be able to redraw the input line and to arrange
the list of tab completion matches in columns. The size of the
caller's terminal can be handed over through the sci_lines=...
and sci_columns=... boot parameters and defaults to
defaults::TERM_LINES x defaults::TERM_COLUMNS
!*/
let window_size = libc::winsize {
ws_row: get_terminal_size_value(
"sci_lines", defaults::TERM_LINES
),
ws_col: get_terminal_size_value(
"sci_columns", defaults::TERM_COLUMNS
),
ws_xpixel: 0,
ws_ypixel: 0
};
let result = unsafe {
libc::ioctl(
fd, libc::TIOCSWINSZ as _, &window_size as *const libc::winsize
)
};
if result == -1 {
debug(&format!(
"Failed to set terminal size: {}",
std::io::Error::last_os_error()
));
}
}

fn get_terminal_size_value(name: &str, default_value: u16) -> u16 {
// Read a terminal geometry value from the given environment
// variable and fall back to the given default value
match env::var(name).unwrap_or_default().parse::<u16>() {
Ok(value) if value > 0 => value,
_ => default_value
}
}

fn set_output_terminal_flags(fd: i32) {
// Disable echo and canonical mode on stdout
match Termios::from_fd(fd) {
Expand Down Expand Up @@ -480,7 +583,7 @@ fn redirect_command_to_raw_channels(
set_output_terminal_flags(stdout_fd);

// main send/recv loop
let mut buffer = [0_u8; 100];
let mut buffer = [0_u8; 1];
loop {
// prepare file descriptors to be watched for by select()
let raw_fdset = std::mem::MaybeUninit::<libc::fd_set>::uninit();
Expand Down Expand Up @@ -549,6 +652,15 @@ fn redirect_command_to_raw_channels(
debug("EOF detected on stream");
break;
}
// On raw channels there is no terminal which
// could echo back the input. As the caller's
// terminal is in raw mode and no longer echoes
// locally, send the input back to make typing
// visible
if stream.write_all(&buffer[0..sz_r]).is_err() {
debug("write failure on stream");
break;
}
if stdin.write_all(&buffer[0..sz_r]).is_err() {
debug("write failure on stdin");
break;
Expand Down Expand Up @@ -576,10 +688,14 @@ fn redirect_command_to_pty(
let stdout_fd = master.as_raw_fd();
let stream_fd = stream.as_raw_fd();

set_output_terminal_flags(stdout_fd);
// Keep the line discipline of the pseudo terminal active.
// The command in the terminal, e.g a shell, takes care for
// reading and echoing the input which is the precondition
// for features like tab completion to work
set_interactive_terminal_flags(stdout_fd);

// main send/recv loop
let mut buffer = [0_u8; 100];
let mut buffer = [0_u8; 1];
loop {
// prepare file descriptors to be watched for by select()
let raw_fdset = std::mem::MaybeUninit::<libc::fd_set>::uninit();
Expand All @@ -606,7 +722,7 @@ fn redirect_command_to_pty(
// try to handle what happened on the file descriptors
if unsafe { libc::FD_ISSET(stdout_fd, &fdset) } {
// something new happened on master,
// try to receive some bytes an send them through the stream
// try to receive some bytes and send them through the stream
if let Ok(sz_r) = master.read(&mut buffer) {
if sz_r == 0 {
debug("EOF detected on stdout");
Expand Down
1 change: 1 addition & 0 deletions firecracker-pilot/src/defaults.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ pub const FIRECRACKER_VSOCK_PREFIX: &str =
"sci_cmd_";
pub const FIRECRACKER_VSOCK_PORT_START: u32 = 49200;
pub const GC_THRESHOLD: usize = 20;
pub const TERM_NAME_MAX_LEN: usize = 32;
pub const VM_CID: u32 = 3;
pub const VM_PORT: u32 =
52;
Expand Down
138 changes: 136 additions & 2 deletions firecracker-pilot/src/firecracker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,60 @@ pub fn execute_command_at_instance(
Ok(())
}

pub fn get_terminal_boot_args() -> Vec<String> {
/*!
Provide the terminal setup of the caller as kernel boot params

The terminal type is required in the guest to switch on the
line editor of interactive commands, e.g the tab completion of
a shell. The window size is required to let that line editor
redraw the input line and to arrange the tab completion matches
in columns
!*/
let mut terminal_boot_args: Vec<String> = Vec::new();
if let Ok(term) = env::var("TERM") {
// Only pass along a terminal name which cannot be used to
// smuggle further parameters into the kernel commandline
let is_terminal_name = ! term.is_empty()
&& term.len() <= defaults::TERM_NAME_MAX_LEN
&& term.chars().all(
|char| char.is_ascii_alphanumeric() || "-_.+".contains(char)
);
if is_terminal_name {
terminal_boot_args.push(format!("sci_term={term}"))
} else if Lookup::is_debug() {
debug!("Unsupported TERM name, not passed to the instance");
}
}
if let Some((lines, columns)) = get_terminal_size() {
terminal_boot_args.push(format!("sci_lines={lines}"));
terminal_boot_args.push(format!("sci_columns={columns}"));
}
terminal_boot_args
}

pub fn get_terminal_size() -> Option<(u16, u16)> {
/*!
Provide the window size of the caller's terminal
!*/
let mut window_size = std::mem::MaybeUninit::<libc::winsize>::uninit();
let result = unsafe {
libc::ioctl(
io::stdin().as_raw_fd(),
libc::TIOCGWINSZ as _,
window_size.as_mut_ptr()
)
};
if result == -1 {
return None
}
let window_size = unsafe { window_size.assume_init() };
if window_size.ws_row == 0 || window_size.ws_col == 0 {
return None
}
Some((window_size.ws_row, window_size.ws_col))
}

pub fn create_firecracker_config(
program_name: &String,
config_file: &NamedTempFile
Expand Down Expand Up @@ -642,6 +696,11 @@ pub fn create_firecracker_config(
if engine_section.overlay_size.is_some() {
boot_args.push("overlay_root=/dev/vdb".to_string());
}
// hand over the setup of the caller's terminal. sci uses this
// information to setup the pseudo terminal of the command such
// that interactive commands, e.g a shell, can provide features
// like tab completion
boot_args.append(&mut get_terminal_boot_args());
for boot_option in engine_section.boot_args
{
if (resume || force_vsock)
Expand Down Expand Up @@ -1069,6 +1128,69 @@ pub fn umount_vm(sub_dir: &str, user: User) -> Result<(), CommandError> {
x.into_iter().collect()
}

pub struct TerminalMode {
fd: i32,
saved: Option<libc::termios>
}

impl TerminalMode {
pub fn raw(fd: i32) -> Self {
/*!
Switch the given terminal into raw mode

In raw mode the terminal driver no longer buffers the input
until the line is complete and no longer echoes what was
typed. Every single key stroke is passed on unmodified and
the echo is left to the command on the other end of the
connection. Output post processing stays switched on such
that output which only provides a line feed is still
displayed correctly. The original terminal setup is
restored when the returned object is dropped.

If the given file descriptor is not a terminal, e.g if the
input is piped, nothing is changed
!*/
if unsafe { libc::isatty(fd) } != 1 {
return TerminalMode { fd, saved: None }
}
let mut terminal = std::mem::MaybeUninit::<libc::termios>::uninit();
if unsafe { libc::tcgetattr(fd, terminal.as_mut_ptr()) } != 0 {
if Lookup::is_debug() {
debug!(
"tcgetattr failed with: {}", io::Error::last_os_error()
);
}
return TerminalMode { fd, saved: None }
}
let saved = unsafe { terminal.assume_init() };
let mut raw = saved;
raw.c_lflag &= !(
libc::ECHO | libc::ECHONL | libc::ICANON |
libc::ISIG | libc::IEXTEN
);
raw.c_iflag &= !(libc::ICRNL | libc::IXON);
raw.c_cc[libc::VMIN] = 1;
raw.c_cc[libc::VTIME] = 0;
if unsafe { libc::tcsetattr(fd, libc::TCSANOW, &raw) } != 0 {
if Lookup::is_debug() {
debug!(
"tcsetattr failed with: {}", io::Error::last_os_error()
);
}
return TerminalMode { fd, saved: None }
}
TerminalMode { fd, saved: Some(saved) }
}
}

impl Drop for TerminalMode {
fn drop(&mut self) {
if let Some(saved) = self.saved {
unsafe { libc::tcsetattr(self.fd, libc::TCSANOW, &saved) };
}
}
}

pub fn stream_listener(socket_path: &str) -> thread::JoinHandle<()> {
let mut socket = String::new();
socket.push_str(socket_path);
Expand Down Expand Up @@ -1101,8 +1223,18 @@ pub fn stream_io(mut stream: UnixStream) {
let stream_fd = stream.as_raw_fd();
let stdin_fd = stdin.as_raw_fd();
let stdout_fd = stdout.as_raw_fd();

// Switch the caller's terminal into raw mode for the time of
// the session. Only in raw mode a single key stroke, e.g the
// TAB key, is passed on to the guest instead of being buffered
// until the line is complete. This is the precondition for the
// line editor of an interactive command in the guest, e.g a
// shell, to provide tab completion. The original terminal
// setup is restored when this function returns
let _terminal_mode = TerminalMode::raw(stdin_fd);

// main send/recv loop
let mut buffer = [0_u8; 100];
let mut buffer = [0_u8; 1];
loop {
// prepare file descriptors to be watched for by select()
let raw_fdset = std::mem::MaybeUninit::<libc::fd_set>::uninit();
Expand Down Expand Up @@ -1148,6 +1280,7 @@ pub fn stream_io(mut stream: UnixStream) {
}
break;
}
let _ = stdout.flush();
} else {
if Lookup::is_debug() {
debug!("read failure on stdin");
Expand All @@ -1157,7 +1290,7 @@ pub fn stream_io(mut stream: UnixStream) {
}
if unsafe { libc::FD_ISSET(stream_fd, &fdset) } {
// something new happened on the stream
// try to receive some bytes an send them to stdout
// try to receive some bytes and send them to stdout
if let Ok(sz_r) = stream.read(&mut buffer) {
if sz_r == 0 {
if Lookup::is_debug() {
Expand All @@ -1171,6 +1304,7 @@ pub fn stream_io(mut stream: UnixStream) {
}
break;
}
let _ = stdout.flush();
} else {
if Lookup::is_debug() {
debug!("read failure on stream");
Expand Down
Loading