Skip to content
Open
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
17 changes: 15 additions & 2 deletions libshpool/src/session_restore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,15 @@ impl SessionSpool for Vt100Screen {
fn restore_buffer(&self) -> Vec<u8> {
let (rows, cols) = self.parser.screen().size();
info!("computing screen restore buf with (rows={}, cols={})", rows, cols);
self.parser.screen().contents_formatted()
// The screen contents alone do not tell a freshly attached terminal
// which input modes the application enabled (bracketed paste,
// application cursor/keypad, mouse protocol). Without them a paste
// into a reattached session arrives unbracketed: terminals flag it
// as unsafe and every newline submits early. Replay the tracked
// input modes after the contents.
let mut buf = self.parser.screen().contents_formatted();
buf.extend(self.parser.screen().input_mode_formatted());
buf
}

fn process(&mut self, bytes: &[u8]) {
Expand All @@ -111,7 +119,12 @@ impl SessionSpool for Vt100Lines {
fn restore_buffer(&self) -> Vec<u8> {
let (rows, cols) = self.parser.screen().size();
info!("computing lines({}) restore buf with (rows={}, cols={})", self.nlines, rows, cols);
self.parser.screen().last_n_rows_contents_formatted(self.nlines)
// See Vt100Screen::restore_buffer: input modes must ride along with
// the restored contents or the attached terminal loses bracketed
// paste (and friends) across a reattach.
let mut buf = self.parser.screen().last_n_rows_contents_formatted(self.nlines);
buf.extend(self.parser.screen().input_mode_formatted());
buf
}

fn process(&mut self, bytes: &[u8]) {
Expand Down
42 changes: 42 additions & 0 deletions shpool/tests/attach.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1040,6 +1040,48 @@ fn lines_restore() -> anyhow::Result<()> {
Ok(())
}

// The restore buffer has to carry the input modes the application turned on,
// not just the screen contents. A terminal that attaches without them silently
// loses bracketed paste, application cursor/keypad and mouse reporting.
//
// We drive mouse reporting here rather than bracketed paste because no shell
// sets it on our behalf, so a match can only come from the restore buffer.
#[test]
#[timeout(30000)]
fn screen_restore_input_modes() -> anyhow::Result<()> {
let mut daemon_proc = support::daemon::Proc::new("restore_screen.toml", DaemonArgs::default())
.context("starting daemon proc")?;
let bidi_done_w = daemon_proc.events.take().unwrap().waiter(["daemon-bidi-stream-done"]);

{
let mut attach_proc =
daemon_proc.attach("sh1", Default::default()).context("starting attach proc")?;
let mut line_matcher = attach_proc.line_matcher()?;

attach_proc.run_cmd("printf '\\033[?1000h'; echo modes-on")?;
line_matcher.scan_until_re("modes-on$")?;
}

// wait until the daemon has noticed that the connection
// has dropped before we attempt to open the connection again
daemon_proc.events = Some(bidi_done_w.wait_final_event("daemon-bidi-stream-done")?);

{
let mut attach_proc =
daemon_proc.attach("sh1", Default::default()).context("starting attach proc")?;
let mut line_matcher = attach_proc.line_matcher()?;

// The restore buffer does not end in a newline, so give the line
// matcher something that does before asserting on it. Nothing in the
// shell emits mouse reporting, so a match can only come from the
// restore buffer we are testing.
attach_proc.run_cmd("echo flushed")?;
line_matcher.scan_until_re("\\x1b\\[\\?1000h")?;
}

Ok(())
}

// Test to make sure that when we do a restore, we don't send back too many
// bytes in once chunk. The attach client has a fixed size buffer it reads into,
// and it will crash if it gets sent a chunk with too large a length.
Expand Down