Implement PTY-native session terminal control
This commit is contained in:
parent
058e58e816
commit
8cef037fa3
1 changed files with 1488 additions and 382 deletions
|
|
@ -12,14 +12,14 @@ use lazaret_core::identity::{
|
|||
SandboxId, SessionId,
|
||||
};
|
||||
use lazaret_core::records::{
|
||||
DeveloperDecision, DeveloperDecisionKind, GenerationIdentity, InteractiveSessionBatch,
|
||||
InteractiveSessionEvent, InteractiveSessionMessage, InteractiveSessionRequest, SessionBinding,
|
||||
SessionExit, SessionTerminalMessage, SignedProtocolRecord, TerminalOutput, TerminalPrompt,
|
||||
TerminalResize, TerminalSignalKind,
|
||||
DeveloperDecision, DeveloperDecisionKind, GenerationIdentity, InteractiveSessionBatchV2,
|
||||
InteractiveSessionEventV2, InteractiveSessionMessage, InteractiveSessionRequestV2,
|
||||
SessionBinding, SessionExit, SessionTerminalEventV2, SessionTerminalMessage,
|
||||
SignedProtocolRecord, TerminalModesV2, TerminalOutput, TerminalPrompt, TerminalResize,
|
||||
};
|
||||
use rustix::fs::{OFlags, fcntl_getfl, fcntl_setfl};
|
||||
use rustix::pty::{OpenptFlags, grantpt, ioctl_tiocgptpeer, openpt, unlockpt};
|
||||
use rustix::termios::{LocalModes, OptionalActions, Termios, tcgetattr, tcsetattr};
|
||||
use rustix::termios::{OptionalActions, Termios, tcgetattr, tcsetattr};
|
||||
|
||||
struct PtyTerminal {
|
||||
_master: File,
|
||||
|
|
@ -74,7 +74,6 @@ impl HostTerminal for PtyTerminal {
|
|||
fn enter_raw(&mut self) -> io::Result<()> {
|
||||
let mut raw = self.original.clone();
|
||||
raw.make_raw();
|
||||
raw.local_modes.insert(LocalModes::ISIG);
|
||||
tcsetattr(&self.slave, OptionalActions::Now, &raw)?;
|
||||
fcntl_setfl(&self.slave, self.original_flags | OFlags::NONBLOCK)?;
|
||||
self.raw = true;
|
||||
|
|
@ -98,6 +97,10 @@ impl HostTerminal for PtyTerminal {
|
|||
})
|
||||
}
|
||||
|
||||
fn modes(&self) -> TerminalModesV2 {
|
||||
test_modes()
|
||||
}
|
||||
|
||||
fn next_event(&mut self) -> io::Result<HostTerminalEvent> {
|
||||
Ok(self.events.pop_front().unwrap_or(HostTerminalEvent::Idle))
|
||||
}
|
||||
|
|
@ -139,11 +142,14 @@ enum TransportFailure {
|
|||
|
||||
struct FixtureTransport {
|
||||
session_id: SessionId,
|
||||
events: Vec<InteractiveSessionEvent>,
|
||||
events: Vec<InteractiveSessionEventV2>,
|
||||
prompt_sent: bool,
|
||||
failure: Option<TransportFailure>,
|
||||
signed_decision: Option<Vec<u8>>,
|
||||
prompt_after_events: usize,
|
||||
batches: VecDeque<InteractiveSessionBatchV2>,
|
||||
cancelled: bool,
|
||||
blocked: bool,
|
||||
}
|
||||
|
||||
impl FixtureTransport {
|
||||
|
|
@ -155,6 +161,9 @@ impl FixtureTransport {
|
|||
failure: None,
|
||||
signed_decision: None,
|
||||
prompt_after_events: 5,
|
||||
batches: VecDeque::new(),
|
||||
cancelled: false,
|
||||
blocked: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -164,13 +173,17 @@ impl InteractiveSessionTransport for FixtureTransport {
|
|||
|
||||
fn start(
|
||||
&mut self,
|
||||
request: InteractiveSessionRequest,
|
||||
) -> Result<InteractiveSessionBatch, ClientError<Self::Error>> {
|
||||
request: InteractiveSessionRequestV2,
|
||||
) -> Result<(), ClientError<Self::Error>> {
|
||||
if matches!(self.failure, Some(TransportFailure::Stale)) {
|
||||
return Err(ClientError::StaleCoordinator);
|
||||
}
|
||||
assert_eq!(request.session_id, self.session_id);
|
||||
Ok(batch(
|
||||
assert_eq!(
|
||||
request.protocol_version,
|
||||
lazaret_core::records::InteractiveProtocolVersion::V2
|
||||
);
|
||||
self.batches.push_back(batch(
|
||||
self.session_id,
|
||||
vec![InteractiveSessionMessage::Terminal(
|
||||
SessionTerminalMessage::Output(TerminalOutput {
|
||||
|
|
@ -178,32 +191,31 @@ impl InteractiveSessionTransport for FixtureTransport {
|
|||
bytes: b"guest-output".to_vec(),
|
||||
}),
|
||||
)],
|
||||
))
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn exchange(
|
||||
&mut self,
|
||||
event: InteractiveSessionEvent,
|
||||
) -> Result<InteractiveSessionBatch, ClientError<Self::Error>> {
|
||||
fn send(&mut self, event: InteractiveSessionEventV2) -> Result<(), ClientError<Self::Error>> {
|
||||
if matches!(self.failure, Some(TransportFailure::Lost)) {
|
||||
return Err(ClientError::LostConnection);
|
||||
}
|
||||
self.events.push(event.clone());
|
||||
if let InteractiveSessionEvent::PromptDecision {
|
||||
if let InteractiveSessionEventV2::PromptDecision {
|
||||
signed_decision, ..
|
||||
} = event
|
||||
{
|
||||
self.signed_decision = Some(signed_decision);
|
||||
return Ok(batch(
|
||||
self.batches.push_back(batch(
|
||||
self.session_id,
|
||||
vec![InteractiveSessionMessage::Terminal(
|
||||
SessionTerminalMessage::Exit(SessionExit::exited(self.session_id, 23)),
|
||||
)],
|
||||
));
|
||||
return Ok(());
|
||||
}
|
||||
if !self.prompt_sent && self.events.len() >= self.prompt_after_events {
|
||||
self.prompt_sent = true;
|
||||
return Ok(batch(
|
||||
self.batches.push_back(batch(
|
||||
self.session_id,
|
||||
vec![InteractiveSessionMessage::Prompt(TerminalPrompt {
|
||||
session_id: self.session_id,
|
||||
|
|
@ -214,8 +226,23 @@ impl InteractiveSessionTransport for FixtureTransport {
|
|||
justification: "update host metadata".into(),
|
||||
})],
|
||||
));
|
||||
return Ok(());
|
||||
}
|
||||
Ok(batch(self.session_id, Vec::new()))
|
||||
self.batches.push_back(batch(self.session_id, Vec::new()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn try_receive(
|
||||
&mut self,
|
||||
) -> Result<Option<InteractiveSessionBatchV2>, ClientError<Self::Error>> {
|
||||
if self.blocked {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(self.batches.pop_front())
|
||||
}
|
||||
|
||||
fn cancel(&mut self) {
|
||||
self.cancelled = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -243,23 +270,21 @@ fn prompt_suspend_sends_resumed_size_before_decision() {
|
|||
assert!(matches!(
|
||||
transport.events.as_slice(),
|
||||
[
|
||||
InteractiveSessionEvent::Poll { .. },
|
||||
InteractiveSessionEvent::Terminal(lazaret_core::records::SessionTerminalEvent::Resize(
|
||||
_
|
||||
)),
|
||||
InteractiveSessionEvent::PromptDecision { .. }
|
||||
InteractiveSessionEventV2::Poll { .. },
|
||||
InteractiveSessionEventV2::Terminal(SessionTerminalEventV2::Resize(_)),
|
||||
InteractiveSessionEventV2::PromptDecision { .. }
|
||||
]
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_fixture_routes_resize_signals_suspend_prompt_and_nonzero_exit() {
|
||||
fn terminal_fixture_routes_resize_input_break_suspend_prompt_and_nonzero_exit() {
|
||||
let identity = ClientSessionIdentity::generate::<std::convert::Infallible>().unwrap();
|
||||
let binding = binding(&identity);
|
||||
let mut terminal = PtyTerminal::new([
|
||||
HostTerminalEvent::Resize,
|
||||
HostTerminalEvent::Signal(TerminalSignalKind::Interrupt),
|
||||
HostTerminalEvent::Signal(TerminalSignalKind::Quit),
|
||||
HostTerminalEvent::Input(vec![3]),
|
||||
HostTerminalEvent::Break,
|
||||
HostTerminalEvent::LocalSuspend,
|
||||
HostTerminalEvent::Input(b"input".to_vec()),
|
||||
]);
|
||||
|
|
@ -289,21 +314,16 @@ fn terminal_fixture_routes_resize_signals_suspend_prompt_and_nonzero_exit() {
|
|||
assert_eq!(decision.approved_command_argv, [b"/usr/bin/tool".to_vec()]);
|
||||
assert!(transport.events.iter().any(|event| matches!(
|
||||
event,
|
||||
InteractiveSessionEvent::Terminal(lazaret_core::records::SessionTerminalEvent::Resize(_))
|
||||
InteractiveSessionEventV2::Terminal(SessionTerminalEventV2::Resize(_))
|
||||
)));
|
||||
for signal in [TerminalSignalKind::Interrupt, TerminalSignalKind::Quit] {
|
||||
assert!(transport.events.iter().any(|event| matches!(
|
||||
event,
|
||||
InteractiveSessionEvent::Terminal(
|
||||
lazaret_core::records::SessionTerminalEvent::Signal(sent)
|
||||
) if sent.signal == signal
|
||||
)));
|
||||
}
|
||||
assert!(!transport.events.iter().any(|event| matches!(
|
||||
assert!(transport.events.iter().any(|event| matches!(
|
||||
event,
|
||||
InteractiveSessionEvent::Terminal(
|
||||
lazaret_core::records::SessionTerminalEvent::Signal(sent)
|
||||
) if sent.signal == TerminalSignalKind::Suspend
|
||||
InteractiveSessionEventV2::Terminal(SessionTerminalEventV2::Input(input))
|
||||
if input.bytes == [3]
|
||||
)));
|
||||
assert!(transport.events.iter().any(|event| matches!(
|
||||
event,
|
||||
InteractiveSessionEventV2::Terminal(SessionTerminalEventV2::Break(_))
|
||||
)));
|
||||
}
|
||||
|
||||
|
|
@ -335,6 +355,34 @@ fn terminal_fixture_restores_raw_mode_for_stale_and_lost_coordinator() {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_disconnect_cancels_blocked_transport_and_restores_terminal() {
|
||||
let identity = ClientSessionIdentity::generate::<std::convert::Infallible>().unwrap();
|
||||
let binding = binding(&identity);
|
||||
let mut terminal = PtyTerminal::new([
|
||||
HostTerminalEvent::LocalSuspend,
|
||||
HostTerminalEvent::Terminate,
|
||||
]);
|
||||
let mut transport = FixtureTransport {
|
||||
blocked: true,
|
||||
..FixtureTransport::successful(identity.session_id())
|
||||
};
|
||||
|
||||
assert!(matches!(
|
||||
run_interactive_session(
|
||||
&mut transport,
|
||||
&mut terminal,
|
||||
&identity,
|
||||
&binding,
|
||||
&[b"agent".to_vec()],
|
||||
),
|
||||
Err(ClientError::LostConnection)
|
||||
));
|
||||
assert!(transport.cancelled);
|
||||
assert_eq!(terminal.suspend_count, 1);
|
||||
assert!(terminal.restored());
|
||||
}
|
||||
|
||||
fn binding(identity: &ClientSessionIdentity) -> SessionBinding {
|
||||
SessionBinding {
|
||||
generation_identity: GenerationIdentity {
|
||||
|
|
@ -354,9 +402,38 @@ fn binding(identity: &ClientSessionIdentity) -> SessionBinding {
|
|||
fn batch(
|
||||
session_id: SessionId,
|
||||
messages: Vec<InteractiveSessionMessage>,
|
||||
) -> InteractiveSessionBatch {
|
||||
InteractiveSessionBatch {
|
||||
) -> InteractiveSessionBatchV2 {
|
||||
InteractiveSessionBatchV2 {
|
||||
session_id,
|
||||
messages,
|
||||
}
|
||||
}
|
||||
|
||||
fn test_modes() -> TerminalModesV2 {
|
||||
TerminalModesV2 {
|
||||
input_modes: 0,
|
||||
output_modes: 0,
|
||||
control_modes: 0,
|
||||
local_modes: 0,
|
||||
line_discipline: 0,
|
||||
input_speed: 38_400,
|
||||
output_speed: 38_400,
|
||||
interrupt: 3,
|
||||
quit: 28,
|
||||
erase: 127,
|
||||
kill: 21,
|
||||
eof: 4,
|
||||
time: 0,
|
||||
minimum: 1,
|
||||
switch: 0,
|
||||
start: 17,
|
||||
stop: 19,
|
||||
suspend: 26,
|
||||
end_of_line: 0,
|
||||
reprint: 18,
|
||||
discard: 15,
|
||||
word_erase: 23,
|
||||
literal_next: 22,
|
||||
end_of_line_2: 0,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue