Apply terminal backpressure without dropping output

This commit is contained in:
ms2k 2026-08-21 02:35:08 +09:00
commit 968ff09eae

View file

@ -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();