Preserve restored screen through update reattach
ci / test (push) Waiting to run
ci / fuzz-smoke (push) Waiting to run
ci / windows-client (push) Waiting to run
ci / package-release (linux-x86_64, ubuntu-latest) (push) Waiting to run
ci / package-release (macos-aarch64, macos-14) (push) Waiting to run
ci / package-release (macos-x86_64, macos-13) (push) Waiting to run
ci / package-release (windows-x86_64, windows-latest) (push) Waiting to run
ci / publish-gitea-release (push) Blocked by required conditions
ci / remote-bench (push) Waiting to run
ci / test (push) Waiting to run
ci / fuzz-smoke (push) Waiting to run
ci / windows-client (push) Waiting to run
ci / package-release (linux-x86_64, ubuntu-latest) (push) Waiting to run
ci / package-release (macos-aarch64, macos-14) (push) Waiting to run
ci / package-release (macos-x86_64, macos-13) (push) Waiting to run
ci / package-release (windows-x86_64, windows-latest) (push) Waiting to run
ci / publish-gitea-release (push) Blocked by required conditions
ci / remote-bench (push) Waiting to run
This commit is contained in:
+109
-3
@@ -804,7 +804,7 @@ fn attach_snapshot(session: &Session, cols: u16, rows: u16) -> Vec<u8> {
|
|||||||
let cols = cols.max(1);
|
let cols = cols.max(1);
|
||||||
let rows = rows.max(1);
|
let rows = rows.max(1);
|
||||||
if let Some(restored) = &session.restored_screen
|
if let Some(restored) = &session.restored_screen
|
||||||
&& restored.output_seq == session.output_seq
|
&& (restored.output_seq == session.output_seq || session.clients.is_empty())
|
||||||
&& restored.cols == cols
|
&& restored.cols == cols
|
||||||
&& restored.rows == rows
|
&& restored.rows == rows
|
||||||
{
|
{
|
||||||
@@ -3691,8 +3691,11 @@ async fn broadcast_output(
|
|||||||
.sessions
|
.sessions
|
||||||
.get_mut(&output.session)
|
.get_mut(&output.session)
|
||||||
.ok_or_else(|| anyhow!("unknown session"))?;
|
.ok_or_else(|| anyhow!("unknown session"))?;
|
||||||
|
let has_clients = !session.clients.is_empty();
|
||||||
session.parser.process(&output.bytes);
|
session.parser.process(&output.bytes);
|
||||||
session.restored_screen = None;
|
if has_clients {
|
||||||
|
session.restored_screen = None;
|
||||||
|
}
|
||||||
session.output_seq += 1;
|
session.output_seq += 1;
|
||||||
let output_seq = session.output_seq;
|
let output_seq = session.output_seq;
|
||||||
session.recent.push_back(output.bytes.clone());
|
session.recent.push_back(output.bytes.clone());
|
||||||
@@ -3709,7 +3712,8 @@ async fn broadcast_output(
|
|||||||
.bytes_since_persist
|
.bytes_since_persist
|
||||||
.saturating_add(output.bytes.len());
|
.saturating_add(output.bytes.len());
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
if session.bytes_since_persist >= SCREEN_PERSIST_BYTES
|
if should_persist_screen_immediately(&output.bytes)
|
||||||
|
|| session.bytes_since_persist >= SCREEN_PERSIST_BYTES
|
||||||
|| now.duration_since(session.last_screen_persist_at) >= SCREEN_PERSIST_MAX_AGE
|
|| now.duration_since(session.last_screen_persist_at) >= SCREEN_PERSIST_MAX_AGE
|
||||||
{
|
{
|
||||||
session.bytes_since_persist = 0;
|
session.bytes_since_persist = 0;
|
||||||
@@ -3847,6 +3851,10 @@ fn screen_snapshot(screen: &vt100::Screen) -> Vec<u8> {
|
|||||||
bytes
|
bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn should_persist_screen_immediately(bytes: &[u8]) -> bool {
|
||||||
|
bytes.len() <= 1024 && bytes.contains(&b'\n')
|
||||||
|
}
|
||||||
|
|
||||||
async fn retransmit_pending(
|
async fn retransmit_pending(
|
||||||
state: &Arc<Mutex<ServerState>>,
|
state: &Arc<Mutex<ServerState>>,
|
||||||
socket: &Arc<UdpSocket>,
|
socket: &Arc<UdpSocket>,
|
||||||
@@ -4316,6 +4324,104 @@ mod tests {
|
|||||||
assert_ne!(attach_snapshot(&session, 100, 30), restored);
|
assert_ne!(attach_snapshot(&session, 100, 30), restored);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_reattach_prefers_restored_screen_even_after_pre_attach_pty_noise() {
|
||||||
|
let restored = b"\x1b[?1049lRESTORED_AFTER_RESTART".to_vec();
|
||||||
|
let mut session = Session {
|
||||||
|
pty: None,
|
||||||
|
parser: vt100::Parser::new(24, 80, 100),
|
||||||
|
restored_screen: Some(RestoredScreenSnapshot {
|
||||||
|
cols: 80,
|
||||||
|
rows: 24,
|
||||||
|
output_seq: 7,
|
||||||
|
snapshot: restored.clone(),
|
||||||
|
}),
|
||||||
|
clients: HashMap::new(),
|
||||||
|
output_seq: 8,
|
||||||
|
recent: VecDeque::new(),
|
||||||
|
empty_since: Some(Instant::now()),
|
||||||
|
holder_control: None,
|
||||||
|
persistent: true,
|
||||||
|
bytes_since_persist: 0,
|
||||||
|
last_persisted_seq: 7,
|
||||||
|
last_screen_persist_at: Instant::now(),
|
||||||
|
};
|
||||||
|
session.parser.process(b"$ ");
|
||||||
|
|
||||||
|
assert_eq!(attach_snapshot(&session, 80, 24), restored);
|
||||||
|
|
||||||
|
session
|
||||||
|
.clients
|
||||||
|
.insert([1u8; 16], test_client_state([2u8; 32]));
|
||||||
|
assert_ne!(attach_snapshot(&session, 80, 24), restored);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn no_client_pty_output_before_first_reattach_keeps_restored_screen() {
|
||||||
|
let restored = b"\x1b[?1049lRESTORED_AFTER_RESTART".to_vec();
|
||||||
|
let (pty_tx, _rx) = mpsc::unbounded_channel();
|
||||||
|
let state = Arc::new(Mutex::new(ServerState::new(
|
||||||
|
ServerConfig {
|
||||||
|
persist_sessions: true,
|
||||||
|
..ServerConfig::default()
|
||||||
|
},
|
||||||
|
[0u8; 32],
|
||||||
|
pty_tx,
|
||||||
|
)));
|
||||||
|
{
|
||||||
|
let mut locked = state.lock().unwrap();
|
||||||
|
locked.sessions.insert(
|
||||||
|
"work".to_string(),
|
||||||
|
Session {
|
||||||
|
pty: None,
|
||||||
|
parser: vt100::Parser::new(24, 80, 100),
|
||||||
|
restored_screen: Some(RestoredScreenSnapshot {
|
||||||
|
cols: 80,
|
||||||
|
rows: 24,
|
||||||
|
output_seq: 7,
|
||||||
|
snapshot: restored.clone(),
|
||||||
|
}),
|
||||||
|
clients: HashMap::new(),
|
||||||
|
output_seq: 7,
|
||||||
|
recent: VecDeque::new(),
|
||||||
|
empty_since: Some(Instant::now()),
|
||||||
|
holder_control: None,
|
||||||
|
persistent: true,
|
||||||
|
bytes_since_persist: 0,
|
||||||
|
last_persisted_seq: 7,
|
||||||
|
last_screen_persist_at: Instant::now(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let socket = Arc::new(UdpSocket::bind("127.0.0.1:0").await.unwrap());
|
||||||
|
broadcast_output(
|
||||||
|
&state,
|
||||||
|
&socket,
|
||||||
|
PtyOutput {
|
||||||
|
session: "work".to_string(),
|
||||||
|
bytes: b"$ ".to_vec(),
|
||||||
|
exited: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let locked = state.lock().unwrap();
|
||||||
|
let session = locked.sessions.get("work").unwrap();
|
||||||
|
assert!(session.restored_screen.is_some());
|
||||||
|
assert_eq!(attach_snapshot(session, 80, 24), restored);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn line_oriented_shell_output_persists_screen_immediately() {
|
||||||
|
assert!(should_persist_screen_immediately(
|
||||||
|
b"IMPLICIT_SCREEN_MARKER\r\n$ "
|
||||||
|
));
|
||||||
|
assert!(!should_persist_screen_immediately(b"\x1b[2K\x1b[H"));
|
||||||
|
assert!(!should_persist_screen_immediately(&vec![b'x'; 2048]));
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn unknown_resume_reject_keeps_client_id() {
|
async fn unknown_resume_reject_keeps_client_id() {
|
||||||
let (pty_tx, _rx) = mpsc::unbounded_channel();
|
let (pty_tx, _rx) = mpsc::unbounded_channel();
|
||||||
|
|||||||
Reference in New Issue
Block a user