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
31 changes: 30 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions libshpool/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ log = "0.4" # logging facade (not used directly, but required if we have tracing
tracing = "0.1" # logging and performance monitoring facade
rmp-serde = "1" # serialization for the control protocol
shpool_vt100 = "0.1.3" # terminal emulation for the scrollback buffer
shpool-vterm = "0.1.0" # alt terminal emulation for the scrollback buffer
shell-words = "1" # parsing the -c/--cmd argument
motd = { version = "0.2.2", default-features = false, features = [] } # getting the message-of-the-day
termini = "1.0.0" # terminfo database
Expand Down
23 changes: 23 additions & 0 deletions libshpool/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,12 @@ pub struct Config {
/// existing session.
pub session_restore_mode: Option<SessionRestoreMode>,

/// Selects the virtual terminal to use for lines and screen
/// mode session restoration. By default, this is Vt100,
/// but you can opt into the experimental Vterm engine
/// if you want to try it out.
pub session_restore_engine: Option<SessionRestoreEngine>,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we update the readme or something as well to let people know that this is an option? I'm not sure how you typically advertise new config values

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was planning to test it out myself a bit first, then when it seems stable enough to be somewhat usable add an entry to the README for other testers. I don't want to tell people to try it out yet since I haven't even tried it myself.


/// The number of lines worth of output to keep in the output
/// spool which is maintained along side a shell session.
/// By default, 10000 lines.
Expand Down Expand Up @@ -312,6 +318,7 @@ impl Config {
forward_env: self.forward_env.or(another.forward_env),
initial_path: self.initial_path.or(another.initial_path),
session_restore_mode: self.session_restore_mode.or(another.session_restore_mode),
session_restore_engine: self.session_restore_engine.or(another.session_restore_engine),
output_spool_lines: self.output_spool_lines.or(another.output_spool_lines),
vt100_output_spool_width: self
.vt100_output_spool_width
Expand Down Expand Up @@ -352,6 +359,16 @@ pub enum SessionRestoreMode {
Lines(u16),
}

#[derive(Deserialize, Debug, Clone, Default)]
#[serde(rename_all = "lowercase")]
pub enum SessionRestoreEngine {
/// Use the shpool_vt100 crate for session restore.
#[default]
Vt100,
/// Use the shpool-vterm crate for session restore.
Vterm,
}

#[derive(Deserialize, Debug, Clone, Default)]
#[serde(rename_all = "lowercase")]
pub enum MotdDisplayMode {
Expand Down Expand Up @@ -413,6 +430,12 @@ mod test {
binding = "Ctrl-q a"
action = "detach"
"#,
r#"
session_restore_engine = "vt100"
"#,
r#"
session_restore_engine = "vterm"
"#,
];

for case in cases.into_iter() {
Expand Down
45 changes: 40 additions & 5 deletions libshpool/src/session_restore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
use shpool_protocol::TtySize;
use tracing::info;

use crate::config::{self, SessionRestoreMode};
use crate::config::{self, SessionRestoreEngine, SessionRestoreMode};

// To prevent data getting dropped, we set this to be large, but we don't want
// to use u16::MAX, since the vt100 crate eagerly fills in its rows, and doing
Expand Down Expand Up @@ -119,24 +119,59 @@ impl SessionSpool for Vt100Lines {
}
}

/// A spool that restores the last screenful of content using shpool-vterm.
pub struct Vterm {
term: shpool_vterm::Term,
mode: SessionRestoreMode,
}

impl SessionSpool for Vterm {
fn resize(&mut self, size: TtySize) {
self.term
.resize(shpool_vterm::Size { height: size.rows as usize, width: size.cols as usize });
}

fn restore_buffer(&self) -> Vec<u8> {
match self.mode {
SessionRestoreMode::Simple => vec![],
SessionRestoreMode::Screen => self.term.contents(shpool_vterm::ContentRegion::Screen),
SessionRestoreMode::Lines(nlines) => {
self.term.contents(shpool_vterm::ContentRegion::BottomLines(nlines as usize))
}
}
}

fn process(&mut self, bytes: &[u8]) {
self.term.process(bytes);
}
}

/// Creates a spool given a `mode`.
pub fn new(
config: config::Manager,
size: &TtySize,
scrollback_lines: usize,
) -> Box<dyn SessionSpool + 'static> {
let restore_engine = config.get().session_restore_engine.clone().unwrap_or_default();
let mode = config.get().session_restore_mode.clone().unwrap_or_default();
let vterm_width = config.vterm_width();
match mode {
SessionRestoreMode::Simple => Box::new(NullSpool),
SessionRestoreMode::Screen => Box::new(Vt100Screen {
match (mode, restore_engine) {
(SessionRestoreMode::Simple, _) => Box::new(NullSpool),
(SessionRestoreMode::Screen, SessionRestoreEngine::Vt100) => Box::new(Vt100Screen {
parser: shpool_vt100::Parser::new(size.rows, vterm_width, scrollback_lines),
config,
}),
SessionRestoreMode::Lines(nlines) => Box::new(Vt100Lines {
(SessionRestoreMode::Lines(nlines), SessionRestoreEngine::Vt100) => Box::new(Vt100Lines {
parser: shpool_vt100::Parser::new(size.rows, vterm_width, scrollback_lines),
nlines,
config,
}),
(mode, SessionRestoreEngine::Vterm) => Box::new(Vterm {
term: shpool_vterm::Term::new(
scrollback_lines,
shpool_vterm::Size { width: size.cols as usize, height: size.rows as usize },
),
mode,
}),
}
}
10 changes: 10 additions & 0 deletions shpool/tests/data/vterm_lines.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
norc = true
noecho = true
shell = "/bin/bash"
session_restore_mode = { lines = 5 }
session_restore_engine = "vterm"
prompt_prefix = ""

[env]
PS1 = "prompt> "
TERM = ""
10 changes: 10 additions & 0 deletions shpool/tests/data/vterm_screen.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
norc = true
noecho = true
shell = "/bin/bash"
session_restore_mode = "screen"
session_restore_engine = "vterm"
prompt_prefix = ""

[env]
PS1 = "prompt> "
TERM = ""
76 changes: 76 additions & 0 deletions shpool/tests/vterm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#![allow(clippy::literal_string_with_formatting_args)]

use anyhow::Context;
use ntest::timeout;

mod support;

use crate::support::daemon::DaemonArgs;

#[test]
#[timeout(30000)]
fn screen_restore() -> anyhow::Result<()> {
let mut daemon_proc = support::daemon::Proc::new("vterm_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("echo foo")?;
line_matcher.scan_until_re("foo$")?;
}

// 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 re-attach should redraw the screen for us, so we should
// get a line with "foo" as part of the re-drawn screen.
line_matcher.scan_until_re("foo$")?;

attach_proc.proc.kill()?;
}

Ok(())
}

#[test]
#[timeout(30000)]
fn lines_restore() -> anyhow::Result<()> {
let mut daemon_proc = support::daemon::Proc::new("vterm_lines.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("echo foo")?;
line_matcher.scan_until_re("foo$")?;
}

// 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 re-attach should redraw the last 2 lines for us, so we should
// get a line with "foo" as part of the re-drawn screen.
line_matcher.scan_until_re("foo$")?;
}

Ok(())
}
Loading