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
4 changes: 2 additions & 2 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion libshpool/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,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.2.2" # alt terminal emulation for the scrollback buffer
shpool-vterm = "0.2.7" # 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
4 changes: 4 additions & 0 deletions libshpool/src/daemon/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -870,6 +870,7 @@ impl Server {
status,
shell_pid: session.child_pid,
attachments: lifecycle_state.attachment.into_iter().collect(),
window_title: session.window_title.get(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It seems a bit strange to store the title persistantly in the session struct. I would think it would be better to always dynamically fetch it from the term impl.

})
})
.collect();
Expand Down Expand Up @@ -1220,6 +1221,7 @@ impl Server {
supports_sentinels,
};
let child_pid = session_inner.pty_master.child_pid().ok_or(anyhow!("no child pid"))?;
let window_title = Arc::new(shell::SessionWindowTitle::default());
session_inner.shell_to_client_join_h =
Some(session_inner.spawn_shell_to_client(shell::ShellToClientArgs {
conn_id,
Expand All @@ -1240,6 +1242,7 @@ impl Server {
heartbeat_ack: heartbeat_ack_tx,
maybe_switch: maybe_switch_rx,
child_exit_notifier: shell_to_client_child_exit_notifier,
window_title: Arc::clone(&window_title),
})?);

if let Some(ttl_secs) = header.ttl_secs {
Expand All @@ -1256,6 +1259,7 @@ impl Server {
child_exit_notifier,
started_at: time::SystemTime::now(),
lifecycle: shell::SessionLifecycle::default(),
window_title,
inner: Arc::new(Mutex::new(session_inner)),
})
}
Expand Down
31 changes: 31 additions & 0 deletions libshpool/src/daemon/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,33 @@ pub struct SessionLifecycleState {
pub attachment: Option<Attachment>,
}

/// A session's current window title. Written by the shell->client thread
/// whenever the window title changes, and read by `handle_list`. Kept out of
/// `inner` for the same reason as [`SessionLifecycleState::attachment`].
#[derive(Debug, Default)]
pub struct SessionWindowTitle {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

same thing, I don't think we should reify this in a data structure like this

window_title: Mutex<Option<String>>,
}

impl SessionWindowTitle {
/// Record a new window title, replacing any non-UTF-8 bytes with U+FFFD.
pub fn set(&self, window_title: Option<&[u8]>) {
*self.window_title.lock() =
window_title.map(|window_title| String::from_utf8_lossy(window_title).into_owned());
}

/// Return a copy of the current window title.
pub fn get(&self) -> Option<String> {
self.window_title.lock().clone()
}
}

/// Session represent a shell session
#[derive(Debug)]
pub struct Session {
pub started_at: time::SystemTime,
pub lifecycle: SessionLifecycle,
pub window_title: Arc<SessionWindowTitle>,
pub child_pid: libc::pid_t,
pub child_exit_notifier: Arc<ExitNotifier>,
pub shell_to_client_ctl: Arc<Mutex<ShellToClientCtl>>,
Expand Down Expand Up @@ -260,6 +282,7 @@ pub struct ShellToClientArgs {
// true if the client is still live, false if it has hung up on us.
pub heartbeat_ack: crossbeam_channel::Sender<(u64, bool)>,
pub child_exit_notifier: Arc<ExitNotifier>,
pub window_title: Arc<SessionWindowTitle>,
}

impl SessionInner {
Expand Down Expand Up @@ -291,6 +314,9 @@ impl SessionInner {

let mut output_spool =
session_restore::new(config, &args.tty_size, args.scrollback_lines, &name);
// The raw bytes last written to `args.window_title`, cached locally so checking
// for changes doesn't lock `args.window_title`.
let mut last_window_title: Option<Vec<u8>> = None;
let mut buf: Vec<u8> = vec![0; consts::BUF_SIZE];
let mut poll_fds = [poll::PollFd::new(
watchable_master.borrow_fd(),
Expand Down Expand Up @@ -601,6 +627,11 @@ impl SessionInner {

if has_seen_prompt_sentinel {
output_spool.process(buf);
let window_title = output_spool.window_title();
if window_title != last_window_title.as_deref() {
args.window_title.set(window_title);
last_window_title = window_title.map(<[u8]>::to_vec);
}
}

let mut reset_client_conn = false;
Expand Down
22 changes: 22 additions & 0 deletions libshpool/src/session_restore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ pub trait SessionSpool {

/// Process bytes from pty master.
fn process(&mut self, bytes: &[u8]);

/// The current window title, or None if it is unset or empty. The title is
/// not checked to be UTF-8.
fn window_title(&self) -> Option<&[u8]>;
}

/// A spool that doesn't do anything.
Expand All @@ -69,6 +73,10 @@ impl SessionSpool for NullSpool {
}

fn process(&mut self, _: &[u8]) {}

fn window_title(&self) -> Option<&[u8]> {
None
}
}

/// A spool that restores the last screenful of content using shpool_vt100.
Expand Down Expand Up @@ -100,6 +108,11 @@ impl SessionSpool for Vt100Screen {
fn process(&mut self, bytes: &[u8]) {
self.parser.process(bytes)
}

fn window_title(&self) -> Option<&[u8]> {
Some(self.parser.screen().title().as_bytes())
.filter(|window_title| !window_title.is_empty())
}
}

/// A spool that restores the last n lines of content using shpool_vt100.
Expand Down Expand Up @@ -130,6 +143,11 @@ impl SessionSpool for Vt100Lines {
fn process(&mut self, bytes: &[u8]) {
self.parser.process(bytes)
}

fn window_title(&self) -> Option<&[u8]> {
Some(self.parser.screen().title().as_bytes())
.filter(|window_title| !window_title.is_empty())
}
}

/// A spool that restores the last screenful of content using shpool-vterm.
Expand Down Expand Up @@ -157,6 +175,10 @@ impl SessionSpool for Vterm {
fn process(&mut self, bytes: &[u8]) {
self.term.process(bytes);
}

fn window_title(&self) -> Option<&[u8]> {
self.term.title()
}
}

/// Creates a spool given a `mode`.
Expand Down
5 changes: 5 additions & 0 deletions shpool-protocol/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,11 @@ pub struct Session {
/// independently of `status`, so they can briefly disagree.
#[serde(default)]
pub attachments: Vec<Attachment>,
/// This session's window title. None if it is unset or empty, or if
/// `session_restore_mode` is `simple`. Non-UTF-8 bytes are replaced with
/// U+FFFD.
#[serde(default)]
pub window_title: Option<String>,
}

/// A session's attachment.
Expand Down
72 changes: 72 additions & 0 deletions shpool/tests/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -541,3 +541,75 @@ fn json_attachment_kill_removes_session() -> anyhow::Result<()> {

Ok(())
}

/// Start a daemon with `config` and attach a session named sh1 to it.
fn attach_sh1(config: &str) -> anyhow::Result<(support::daemon::Proc, support::attach::Proc)> {
let mut daemon_proc = support::daemon::Proc::new(config, DaemonArgs::default())
.context("starting daemon proc")?;
let bidi_enter_w = daemon_proc.events.take().unwrap().waiter(["daemon-bidi-stream-enter"]);
let sess1 = daemon_proc.attach("sh1", Default::default())?;
daemon_proc.events = Some(bidi_enter_w.wait_final_event("daemon-bidi-stream-enter")?);
Ok((daemon_proc, sess1))
}

/// The window title `shpool list --json` reports for sh1.
fn sh1_window_title(daemon_proc: &mut support::daemon::Proc) -> anyhow::Result<Value> {
let sessions = list_sessions(daemon_proc)?;
let sh1 =
sessions.iter().find(|s| s["name"] == "sh1").ok_or_else(|| anyhow!("sh1 not found"))?;
Ok(sh1["window_title"].clone())
}

/// Have sh1's shell print `printf_arg`, and wait for `shpool list --json` to
/// report `want` as its window title.
fn assert_window_title_reported(config: &str, printf_arg: &str, want: &str) -> anyhow::Result<()> {
let (mut daemon_proc, mut sess1) = attach_sh1(config)?;
assert!(
sh1_window_title(&mut daemon_proc)?.is_null(),
"expected no window title before one is set"
);

sess1.run_cmd(&format!("printf '{printf_arg}'"))?;
support::wait_until(|| Ok(sh1_window_title(&mut daemon_proc)? == want))
.with_context(|| format!("window title should be reported as {want:?}"))?;

Ok(())
}

#[test]
#[timeout(30000)]
fn json_window_title_vt100() -> anyhow::Result<()> {
assert_window_title_reported("restore_screen.toml", r"\033]2;hello\007", "hello")
}

#[test]
#[timeout(30000)]
fn json_window_title_vterm() -> anyhow::Result<()> {
assert_window_title_reported("vterm_screen.toml", r"\033]2;hello\007", "hello")
}

#[test]
#[timeout(30000)]
fn json_window_title_invalid_utf8_replaced() -> anyhow::Result<()> {
// vterm keeps a window title that is not UTF-8, where vt100 ignores it, so this
// shows the replacement.
assert_window_title_reported("vterm_screen.toml", r"\033]2;caf\351\007", "caf\u{fffd}")
}

#[test]
#[timeout(30000)]
fn json_window_title_null_in_simple_restore_mode() -> anyhow::Result<()> {
let (mut daemon_proc, mut sess1) = attach_sh1("norc.toml")?;
let mut lm1 = sess1.line_matcher()?;

sess1.run_cmd(r"printf '\033]2;hello\007'; echo done")?;
// The daemon handles output in order, so once `done` arrives it has also seen
// the window title.
lm1.scan_until_re("done$")?;
assert!(
sh1_window_title(&mut daemon_proc)?.is_null(),
"simple restore mode keeps no window title"
);

Ok(())
}
Loading