Apply terminal backpressure without dropping output
This commit is contained in:
parent
74481b043d
commit
968ff09eae
1 changed files with 89 additions and 54 deletions
|
|
@ -114,7 +114,8 @@ pub struct PromptOutcome {
|
|||
const PROMPT_ESCAPE_INTERVAL: Duration = Duration::from_millis(40);
|
||||
|
||||
pub struct SystemTerminal {
|
||||
terminal: File,
|
||||
input: File,
|
||||
output: File,
|
||||
original: Termios,
|
||||
original_flags: OFlags,
|
||||
raw: bool,
|
||||
|
|
@ -245,12 +246,14 @@ impl SystemTerminal {
|
|||
/// # Errors
|
||||
/// Returns an error when the process has no usable controlling terminal.
|
||||
pub fn open() -> io::Result<Self> {
|
||||
let terminal = OpenOptions::new().read(true).write(true).open("/dev/tty")?;
|
||||
let size = tcgetwinsize(&terminal)?;
|
||||
let input = OpenOptions::new().read(true).write(true).open("/dev/tty")?;
|
||||
let output = OpenOptions::new().read(true).write(true).open("/dev/tty")?;
|
||||
let size = tcgetwinsize(&input)?;
|
||||
Ok(Self {
|
||||
original: tcgetattr(&terminal)?,
|
||||
original_flags: fcntl_getfl(&terminal)?,
|
||||
terminal,
|
||||
original: tcgetattr(&input)?,
|
||||
original_flags: fcntl_getfl(&input)?,
|
||||
input,
|
||||
output,
|
||||
raw: false,
|
||||
signals: SignalQueue::register()?,
|
||||
events: VecDeque::new(),
|
||||
|
|
@ -323,9 +326,9 @@ impl HostTerminal for SystemTerminal {
|
|||
}
|
||||
let mut raw = self.original.clone();
|
||||
raw.make_raw();
|
||||
tcsetattr(&self.terminal, OptionalActions::Flush, &raw)?;
|
||||
if let Err(error) = fcntl_setfl(&self.terminal, self.original_flags | OFlags::NONBLOCK) {
|
||||
let _ = tcsetattr(&self.terminal, OptionalActions::Flush, &self.original);
|
||||
tcsetattr(&self.input, OptionalActions::Flush, &raw)?;
|
||||
if let Err(error) = fcntl_setfl(&self.input, self.original_flags | OFlags::NONBLOCK) {
|
||||
let _ = tcsetattr(&self.input, OptionalActions::Flush, &self.original);
|
||||
return Err(error.into());
|
||||
}
|
||||
self.raw = true;
|
||||
|
|
@ -339,14 +342,14 @@ impl HostTerminal for SystemTerminal {
|
|||
self.leave_for_host()?;
|
||||
self.trusted = None;
|
||||
self.deferred_resize = None;
|
||||
fcntl_setfl(&self.terminal, self.original_flags)?;
|
||||
tcsetattr(&self.terminal, OptionalActions::Flush, &self.original)?;
|
||||
fcntl_setfl(&self.input, self.original_flags)?;
|
||||
tcsetattr(&self.input, OptionalActions::Flush, &self.original)?;
|
||||
self.raw = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn size(&self, session_id: lazaret_core::identity::SessionId) -> io::Result<TerminalResize> {
|
||||
let size = tcgetwinsize(&self.terminal)?;
|
||||
let size = tcgetwinsize(&self.input)?;
|
||||
Ok(TerminalResize {
|
||||
session_id,
|
||||
rows: NonZeroU16::new(size.ws_row)
|
||||
|
|
@ -373,7 +376,7 @@ impl HostTerminal for SystemTerminal {
|
|||
return Ok(event);
|
||||
}
|
||||
let mut bytes = vec![0_u8; 16 * 1024];
|
||||
match self.terminal.read(&mut bytes) {
|
||||
match self.input.read(&mut bytes) {
|
||||
Ok(0) => Ok(HostTerminalEvent::Terminate),
|
||||
Ok(length) => {
|
||||
bytes.truncate(length);
|
||||
|
|
@ -393,8 +396,8 @@ impl HostTerminal for SystemTerminal {
|
|||
self.restore_guest_presentation()?;
|
||||
}
|
||||
self.presentation.process(bytes);
|
||||
self.terminal.write_all(bytes)?;
|
||||
self.terminal.flush()?;
|
||||
self.output.write_all(bytes)?;
|
||||
self.output.flush()?;
|
||||
self.physical_screen = if self.presentation.alternate_screen() {
|
||||
PhysicalScreen::Alternate
|
||||
} else {
|
||||
|
|
@ -434,7 +437,7 @@ impl HostTerminal for SystemTerminal {
|
|||
self.events.clear();
|
||||
self.escape_pending = false;
|
||||
self.key_translator.clear();
|
||||
tcflush(&self.terminal, QueueSelector::IFlush)?;
|
||||
tcflush(&self.input, QueueSelector::IFlush)?;
|
||||
let mut decoder = PromptInputDecoder::new();
|
||||
self.trusted = Some(TrustedPresentation {
|
||||
prompt: prompt.clone(),
|
||||
|
|
@ -496,8 +499,8 @@ impl HostTerminal for SystemTerminal {
|
|||
|
||||
fn suspend_and_resume(&mut self) -> io::Result<()> {
|
||||
self.leave_for_host()?;
|
||||
fcntl_setfl(&self.terminal, self.original_flags)?;
|
||||
tcsetattr(&self.terminal, OptionalActions::Flush, &self.original)?;
|
||||
fcntl_setfl(&self.input, self.original_flags)?;
|
||||
tcsetattr(&self.input, OptionalActions::Flush, &self.original)?;
|
||||
self.raw = false;
|
||||
lazaret_linux::process::suspend_current_process()?;
|
||||
self.enter_raw()?;
|
||||
|
|
@ -519,29 +522,29 @@ impl SystemTerminal {
|
|||
}
|
||||
|
||||
fn leave_for_host(&mut self) -> io::Result<()> {
|
||||
self.terminal.write_all(HOST_TERMINAL_MODES)?;
|
||||
self.output.write_all(HOST_TERMINAL_MODES)?;
|
||||
if self.physical_screen == PhysicalScreen::Alternate {
|
||||
self.terminal.write_all(b"\x1b[?1049l")?;
|
||||
self.output.write_all(b"\x1b[?1049l")?;
|
||||
self.physical_screen = PhysicalScreen::Primary;
|
||||
}
|
||||
self.terminal.flush()
|
||||
self.output.flush()
|
||||
}
|
||||
|
||||
fn reconstruct_after_resume(&mut self) -> io::Result<()> {
|
||||
if self.trusted.is_some() {
|
||||
return self.render_trusted_prompt(true);
|
||||
}
|
||||
self.terminal.write_all(b"\x1b[?2026l\x1b[?2026h")?;
|
||||
self.output.write_all(b"\x1b[?2026l\x1b[?2026h")?;
|
||||
if self.presentation.alternate_screen() {
|
||||
self.terminal.write_all(b"\x1b[?1049h")?;
|
||||
self.output.write_all(b"\x1b[?1049h")?;
|
||||
self.physical_screen = PhysicalScreen::Alternate;
|
||||
}
|
||||
self.terminal
|
||||
self.output
|
||||
.write_all(&self.presentation.restoration_snapshot())?;
|
||||
self.terminal.write_all(b"\x1b[?2026l")?;
|
||||
self.terminal
|
||||
self.output.write_all(b"\x1b[?2026l")?;
|
||||
self.output
|
||||
.write_all(&self.presentation.synchronized_output_mode())?;
|
||||
self.terminal.flush()
|
||||
self.output.flush()
|
||||
}
|
||||
|
||||
fn render_trusted_prompt(&mut self, reconstruct: bool) -> io::Result<()> {
|
||||
|
|
@ -552,26 +555,21 @@ impl SystemTerminal {
|
|||
let result = (|| {
|
||||
let size = self.size(trusted.prompt.session_id)?;
|
||||
let layout = PromptLayout::new(&trusted.prompt, size.columns.get(), size.rows.get());
|
||||
self.terminal.write_all(b"\x1b[?2026l\x1b[?2026h")?;
|
||||
self.terminal.write_all(PROMPT_INPUT_MODES_OFF)?;
|
||||
self.terminal.write_all(b"\x1b[?25l\x1b[?7l")?;
|
||||
self.output.write_all(b"\x1b[?2026l\x1b[?2026h")?;
|
||||
self.output.write_all(PROMPT_INPUT_MODES_OFF)?;
|
||||
self.output.write_all(b"\x1b[?25l\x1b[?7l")?;
|
||||
let entered_alternate = self.physical_screen == PhysicalScreen::Primary;
|
||||
if entered_alternate {
|
||||
self.terminal.write_all(b"\x1b[?1049h")?;
|
||||
self.output.write_all(b"\x1b[?1049h")?;
|
||||
self.physical_screen = PhysicalScreen::Alternate;
|
||||
}
|
||||
if reconstruct || entered_alternate {
|
||||
self.terminal
|
||||
self.output
|
||||
.write_all(&self.presentation.context_snapshot())?;
|
||||
}
|
||||
render_prompt_card(
|
||||
&mut self.terminal,
|
||||
&layout,
|
||||
trusted.choice,
|
||||
&mut trusted.view,
|
||||
)?;
|
||||
self.terminal.write_all(b"\x1b[?2026l")?;
|
||||
self.terminal.flush()
|
||||
render_prompt_card(&mut self.output, &layout, trusted.choice, &mut trusted.view)?;
|
||||
self.output.write_all(b"\x1b[?2026l")?;
|
||||
self.output.flush()
|
||||
})();
|
||||
self.trusted = Some(trusted);
|
||||
result
|
||||
|
|
@ -582,24 +580,24 @@ impl SystemTerminal {
|
|||
.trusted
|
||||
.take()
|
||||
.expect("guest restoration requires an active trusted presentation");
|
||||
self.terminal.write_all(b"\x1b[?2026l\x1b[?2026h")?;
|
||||
self.output.write_all(b"\x1b[?2026l\x1b[?2026h")?;
|
||||
if trusted.guest_alternate {
|
||||
if self.physical_screen == PhysicalScreen::Primary {
|
||||
self.terminal.write_all(b"\x1b[?1049h")?;
|
||||
self.output.write_all(b"\x1b[?1049h")?;
|
||||
self.physical_screen = PhysicalScreen::Alternate;
|
||||
}
|
||||
self.terminal
|
||||
self.output
|
||||
.write_all(&self.presentation.restoration_snapshot())?;
|
||||
} else if self.physical_screen == PhysicalScreen::Alternate {
|
||||
self.terminal.write_all(b"\x1b[?1049l")?;
|
||||
self.output.write_all(b"\x1b[?1049l")?;
|
||||
self.physical_screen = PhysicalScreen::Primary;
|
||||
}
|
||||
self.terminal
|
||||
self.output
|
||||
.write_all(&self.presentation.restoration_modes())?;
|
||||
self.terminal.write_all(b"\x1b[?2026l")?;
|
||||
self.terminal
|
||||
self.output.write_all(b"\x1b[?2026l")?;
|
||||
self.output
|
||||
.write_all(&self.presentation.synchronized_output_mode())?;
|
||||
self.terminal.flush()
|
||||
self.output.flush()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1098,17 +1096,24 @@ mod tests {
|
|||
openpt(OpenptFlags::RDWR | OpenptFlags::NOCTTY | OpenptFlags::CLOEXEC).unwrap();
|
||||
grantpt(&master).unwrap();
|
||||
unlockpt(&master).unwrap();
|
||||
let slave = File::from(
|
||||
let input = File::from(
|
||||
ioctl_tiocgptpeer(
|
||||
&master,
|
||||
OpenptFlags::RDWR | OpenptFlags::NOCTTY | OpenptFlags::CLOEXEC,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
let original = tcgetattr(&slave).unwrap();
|
||||
let original_flags = fcntl_getfl(&slave).unwrap();
|
||||
let output = File::from(
|
||||
ioctl_tiocgptpeer(
|
||||
&master,
|
||||
OpenptFlags::RDWR | OpenptFlags::NOCTTY | OpenptFlags::CLOEXEC,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
let original = tcgetattr(&input).unwrap();
|
||||
let original_flags = fcntl_getfl(&input).unwrap();
|
||||
tcsetwinsize(
|
||||
&slave,
|
||||
&input,
|
||||
Winsize {
|
||||
ws_row: 40,
|
||||
ws_col: 140,
|
||||
|
|
@ -1119,7 +1124,8 @@ mod tests {
|
|||
.unwrap();
|
||||
(
|
||||
SystemTerminal {
|
||||
terminal: slave,
|
||||
input,
|
||||
output,
|
||||
original,
|
||||
original_flags,
|
||||
raw: false,
|
||||
|
|
@ -1177,7 +1183,7 @@ mod tests {
|
|||
let (mut terminal, mut master) = terminal();
|
||||
terminal.enter_raw().unwrap();
|
||||
assert!(
|
||||
!tcgetattr(&terminal.terminal)
|
||||
!tcgetattr(&terminal.input)
|
||||
.unwrap()
|
||||
.local_modes
|
||||
.contains(LocalModes::ISIG)
|
||||
|
|
@ -1189,6 +1195,35 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_output_waits_for_a_slow_reader() {
|
||||
let (mut terminal, mut master) = terminal();
|
||||
terminal.enter_raw().unwrap();
|
||||
let output = vec![b'x'; 256 * 1024];
|
||||
let expected = output.clone();
|
||||
let reader = thread::spawn(move || {
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
let mut received = Vec::new();
|
||||
let mut buffer = [0_u8; 16 * 1024];
|
||||
while received.len() < expected.len() {
|
||||
match master.read(&mut buffer) {
|
||||
Ok(0) => break,
|
||||
Ok(length) => received.extend_from_slice(&buffer[..length]),
|
||||
Err(error) if error.raw_os_error() == Some(nix::libc::EIO) => break,
|
||||
Err(error) => panic!("slow terminal reader failed: {error}"),
|
||||
}
|
||||
}
|
||||
received
|
||||
});
|
||||
|
||||
let result = terminal.write_output(&output);
|
||||
drop(terminal);
|
||||
let received = reader.join().unwrap();
|
||||
|
||||
assert!(result.is_ok(), "{result:?}");
|
||||
assert!(received.starts_with(&output));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ssh_style_escapes_survive_read_boundaries_and_preserve_literals() {
|
||||
let (mut terminal, mut master) = terminal();
|
||||
|
|
|
|||
Loading…
Reference in a new issue