Implement PTY-native session terminal control
This commit is contained in:
parent
058e58e816
commit
8cef037fa3
3 changed files with 1488 additions and 382 deletions
|
|
@ -70,7 +70,7 @@ fn durable_detach_failure_and_sandbox_release_revoke_active_terminal() {
|
|||
assert!(matches!(
|
||||
engine.handle(
|
||||
GenerationId::new(7),
|
||||
SessionTerminalEvent::Input(TerminalInput {
|
||||
SessionTerminalEventV2::Input(TerminalInput {
|
||||
session_id: session,
|
||||
bytes: b"refused".to_vec(),
|
||||
}),
|
||||
|
|
@ -102,7 +102,7 @@ fn domain_container_hc_and_authority_read_mismatch_use_host_only_release() {
|
|||
assert!(matches!(
|
||||
engine.handle(
|
||||
GenerationId::new(7),
|
||||
SessionTerminalEvent::Input(TerminalInput {
|
||||
SessionTerminalEventV2::Input(TerminalInput {
|
||||
session_id: session,
|
||||
bytes: b"refused".to_vec(),
|
||||
}),
|
||||
|
|
@ -142,7 +142,7 @@ fn explicit_close_and_loss_revalidate_before_guest_cleanup() {
|
|||
engine
|
||||
.handle(
|
||||
GenerationId::new(7),
|
||||
SessionTerminalEvent::Close(lazaret_core::records::SessionClose {
|
||||
SessionTerminalEventV2::Close(lazaret_core::records::SessionClose {
|
||||
session_id: session,
|
||||
}),
|
||||
)
|
||||
|
|
@ -183,7 +183,7 @@ fn grace_wait_revocation_and_read_error_stop_all_later_guest_actions() {
|
|||
engine
|
||||
.handle(
|
||||
GenerationId::new(7),
|
||||
SessionTerminalEvent::Close(lazaret_core::records::SessionClose {
|
||||
SessionTerminalEventV2::Close(lazaret_core::records::SessionClose {
|
||||
session_id: session,
|
||||
}),
|
||||
)
|
||||
|
|
@ -213,7 +213,7 @@ fn grace_wait_revocation_and_read_error_stop_all_later_guest_actions() {
|
|||
engine
|
||||
.handle(
|
||||
GenerationId::new(7),
|
||||
SessionTerminalEvent::Close(lazaret_core::records::SessionClose {
|
||||
SessionTerminalEventV2::Close(lazaret_core::records::SessionClose {
|
||||
session_id: session,
|
||||
}),
|
||||
)
|
||||
|
|
@ -236,9 +236,8 @@ fn stale_caller_and_superseded_generation_refuse_without_cross_generation_action
|
|||
assert!(matches!(
|
||||
engine.handle(
|
||||
GenerationId::new(8),
|
||||
SessionTerminalEvent::Signal(TerminalSignal {
|
||||
session_id: session,
|
||||
signal: TerminalSignalKind::Interrupt,
|
||||
SessionTerminalEventV2::Break(TerminalBreak {
|
||||
session_id: session
|
||||
}),
|
||||
),
|
||||
Err(TerminalError::StaleGeneration)
|
||||
|
|
@ -266,14 +265,14 @@ fn stale_caller_and_superseded_generation_refuse_without_cross_generation_action
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn resize_and_terminal_signals_use_the_exact_group_map() {
|
||||
fn resize_break_and_input_use_only_the_exact_pty() {
|
||||
let session = id(6);
|
||||
let (_fixture, factory, mut engine) = setup(&[session]);
|
||||
engine.acquire(&acquisition(session)).unwrap();
|
||||
engine
|
||||
.handle(
|
||||
GenerationId::new(7),
|
||||
SessionTerminalEvent::Resize(TerminalResize {
|
||||
SessionTerminalEventV2::Resize(TerminalResize {
|
||||
session_id: session,
|
||||
rows: NonZeroU16::new(40).unwrap(),
|
||||
columns: NonZeroU16::new(120).unwrap(),
|
||||
|
|
@ -282,49 +281,49 @@ fn resize_and_terminal_signals_use_the_exact_group_map() {
|
|||
}),
|
||||
)
|
||||
.unwrap();
|
||||
for (kind, expected) in [
|
||||
(TerminalSignalKind::Interrupt, "signal:INT"),
|
||||
(TerminalSignalKind::Quit, "signal:QUIT"),
|
||||
(TerminalSignalKind::Suspend, "signal:TSTP"),
|
||||
] {
|
||||
engine
|
||||
.handle(
|
||||
GenerationId::new(7),
|
||||
SessionTerminalEvent::Signal(TerminalSignal {
|
||||
session_id: session,
|
||||
signal: kind,
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(factory.log.lock().unwrap().last().unwrap(), expected);
|
||||
}
|
||||
engine
|
||||
.handle(
|
||||
GenerationId::new(7),
|
||||
SessionTerminalEventV2::Break(TerminalBreak {
|
||||
session_id: session,
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
engine
|
||||
.handle(
|
||||
GenerationId::new(7),
|
||||
SessionTerminalEventV2::Input(TerminalInput {
|
||||
session_id: session,
|
||||
bytes: vec![3],
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
&factory.log.lock().unwrap()[..2],
|
||||
&["resize:40x120", "signal:WINCH"]
|
||||
&factory.log.lock().unwrap()[..3],
|
||||
&["resize:40x120", "break", "input:1"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changed_guest_identity_refuses_numeric_group_action() {
|
||||
fn keyboard_input_does_not_target_the_recorded_group() {
|
||||
let session = id(6);
|
||||
let (fixture, factory, mut engine) = setup(&[session]);
|
||||
engine.acquire(&acquisition(session)).unwrap();
|
||||
factory.state(0).lock().unwrap().guest.identity_mismatch = true;
|
||||
assert!(matches!(
|
||||
engine.handle(
|
||||
engine
|
||||
.handle(
|
||||
GenerationId::new(7),
|
||||
SessionTerminalEvent::Signal(TerminalSignal {
|
||||
SessionTerminalEventV2::Input(TerminalInput {
|
||||
session_id: session,
|
||||
signal: TerminalSignalKind::Interrupt,
|
||||
bytes: vec![3],
|
||||
}),
|
||||
),
|
||||
Err(TerminalError::GuestIdentityMismatch)
|
||||
));
|
||||
assert!(factory.log.lock().unwrap().is_empty());
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(factory.log.lock().unwrap().as_slice(), ["input:1"]);
|
||||
let messages = engine
|
||||
.handle(
|
||||
GenerationId::new(7),
|
||||
SessionTerminalEvent::Close(lazaret_core::records::SessionClose {
|
||||
SessionTerminalEventV2::Close(lazaret_core::records::SessionClose {
|
||||
session_id: session,
|
||||
}),
|
||||
)
|
||||
|
|
@ -341,6 +340,47 @@ fn changed_guest_identity_refuses_numeric_group_action() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mismatched_leader_with_absent_group_closes_clean() {
|
||||
let session = id(6);
|
||||
let (fixture, factory, mut engine) = setup(&[session]);
|
||||
engine.acquire(&acquisition(session)).unwrap();
|
||||
{
|
||||
let state = factory.state(0);
|
||||
let mut state = state.lock().unwrap();
|
||||
state.guest.identity_mismatch = true;
|
||||
state.guest.group_present = false;
|
||||
}
|
||||
|
||||
let messages = engine
|
||||
.handle(
|
||||
GenerationId::new(7),
|
||||
SessionTerminalEventV2::Close(lazaret_core::records::SessionClose {
|
||||
session_id: session,
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
messages
|
||||
.iter()
|
||||
.any(|message| matches!(message, SessionTerminalMessage::Exit(_)))
|
||||
);
|
||||
assert!(!engine.contains(session));
|
||||
assert_eq!(
|
||||
fixture.store.snapshot().unwrap().session_terminals[0].state(),
|
||||
SessionTerminalState::Clean
|
||||
);
|
||||
assert!(
|
||||
!factory
|
||||
.log
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|action| action.starts_with("signal:"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn absent_leader_with_live_group_closes_with_release_intent() {
|
||||
let session = id(6);
|
||||
|
|
@ -355,7 +395,7 @@ fn absent_leader_with_live_group_closes_with_release_intent() {
|
|||
let messages = engine
|
||||
.handle(
|
||||
GenerationId::new(7),
|
||||
SessionTerminalEvent::Close(lazaret_core::records::SessionClose {
|
||||
SessionTerminalEventV2::Close(lazaret_core::records::SessionClose {
|
||||
session_id: session,
|
||||
}),
|
||||
)
|
||||
|
|
@ -412,7 +452,7 @@ fn persistent_guest_group_closes_session_with_release_intent() {
|
|||
let messages = engine
|
||||
.handle(
|
||||
GenerationId::new(7),
|
||||
SessionTerminalEvent::Close(lazaret_core::records::SessionClose {
|
||||
SessionTerminalEventV2::Close(lazaret_core::records::SessionClose {
|
||||
session_id: session,
|
||||
}),
|
||||
)
|
||||
|
|
@ -430,6 +470,40 @@ fn persistent_guest_group_closes_session_with_release_intent() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sandbox_absence_cleans_retained_terminal_after_exact_exec_absence() {
|
||||
let session = id(6);
|
||||
let (fixture, factory, mut engine) = setup(&[session]);
|
||||
engine.acquire(&acquisition(session)).unwrap();
|
||||
factory.state(0).lock().unwrap().guest.clears_after_signal = "never";
|
||||
engine
|
||||
.handle(
|
||||
GenerationId::new(7),
|
||||
SessionTerminalEventV2::Close(lazaret_core::records::SessionClose {
|
||||
session_id: session,
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
engine
|
||||
.clean_after_sandbox_absence(
|
||||
GenerationId::new(7),
|
||||
ContainerId::new(bytes(9)),
|
||||
process(70),
|
||||
|_| {
|
||||
Err(io::Error::from_raw_os_error(
|
||||
rustix::io::Errno::SRCH.raw_os_error(),
|
||||
))
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
fixture.store.snapshot().unwrap().session_terminals[0].state(),
|
||||
SessionTerminalState::Clean
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn running_store_failures_release_every_live_partial_handle() {
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ fn close_cancels_route_then_bounds_drain_and_emits_one_final_exit() {
|
|||
let messages = engine
|
||||
.handle(
|
||||
GenerationId::new(7),
|
||||
SessionTerminalEvent::Close(lazaret_core::records::SessionClose {
|
||||
SessionTerminalEventV2::Close(lazaret_core::records::SessionClose {
|
||||
session_id: session,
|
||||
}),
|
||||
)
|
||||
|
|
@ -91,7 +91,7 @@ fn close_cancels_route_then_bounds_drain_and_emits_one_final_exit() {
|
|||
assert!(matches!(
|
||||
engine.handle(
|
||||
GenerationId::new(7),
|
||||
SessionTerminalEvent::Input(TerminalInput {
|
||||
SessionTerminalEventV2::Input(TerminalInput {
|
||||
session_id: session,
|
||||
bytes: b"reattach".to_vec(),
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ fn prompt_adapter_retains_resume_resize_before_authenticated_denial() {
|
|||
session_id: binding.session_id,
|
||||
};
|
||||
let batch = control
|
||||
.exchange(InteractiveSessionEvent::Poll {
|
||||
.exchange(InteractiveSessionEventV2::Poll {
|
||||
session_id: binding.session_id,
|
||||
})
|
||||
.unwrap();
|
||||
|
|
@ -111,8 +111,8 @@ fn prompt_adapter_retains_resume_resize_before_authenticated_denial() {
|
|||
pixel_height: 900,
|
||||
};
|
||||
control
|
||||
.exchange(InteractiveSessionEvent::Terminal(
|
||||
SessionTerminalEvent::Resize(resumed_size),
|
||||
.exchange(InteractiveSessionEventV2::Terminal(
|
||||
SessionTerminalEventV2::Resize(resumed_size),
|
||||
))
|
||||
.unwrap();
|
||||
let signed_decision = encode_signed(
|
||||
|
|
@ -128,7 +128,7 @@ fn prompt_adapter_retains_resume_resize_before_authenticated_denial() {
|
|||
)
|
||||
.unwrap();
|
||||
control
|
||||
.exchange(InteractiveSessionEvent::PromptDecision {
|
||||
.exchange(InteractiveSessionEventV2::PromptDecision {
|
||||
session_id: binding.session_id,
|
||||
signed_decision: signed_decision.clone(),
|
||||
})
|
||||
|
|
@ -140,10 +140,7 @@ fn prompt_adapter_retains_resume_resize_before_authenticated_denial() {
|
|||
assert_eq!(routed.signed_decision, signed_decision);
|
||||
assert_eq!(routed.decision.decision, DeveloperDecisionKind::Deny);
|
||||
assert_eq!(routed.resumed_size, Some(resumed_size));
|
||||
assert_eq!(
|
||||
factory.log.lock().unwrap().as_slice(),
|
||||
["resize:55x144", "signal:WINCH"]
|
||||
);
|
||||
assert_eq!(factory.log.lock().unwrap().as_slice(), ["resize:55x144"]);
|
||||
assert!(
|
||||
!factory
|
||||
.log
|
||||
|
|
@ -169,7 +166,7 @@ fn prompt_adapter_retains_resume_resize_before_authenticated_denial() {
|
|||
.unwrap()
|
||||
.handle(
|
||||
GenerationId::new(7),
|
||||
SessionTerminalEvent::Input(TerminalInput {
|
||||
SessionTerminalEventV2::Input(TerminalInput {
|
||||
session_id: binding.session_id,
|
||||
bytes: b"sandbox-input".to_vec(),
|
||||
}),
|
||||
|
|
|
|||
Loading…
Reference in a new issue