lazaret/crates/lazaret-host-coordinator/src/terminal/interactive.rs

249 lines
9.2 KiB
Rust
Raw Normal View History

struct ActiveTerminal {
generation_id: GenerationId,
request: TerminalAcquisition,
route: TerminalRoute,
record: SessionTerminal,
process: Box<dyn SessionTerminalProcess>,
releasing: bool,
pending_input: VecDeque<u8>,
}
pub struct SessionTerminalEngine {
recorder: HostCoordinatorTerminalRecorder,
factory: Arc<dyn SessionTerminalFactory>,
cancellation: Arc<dyn PromptRouteCancellation>,
active: HashMap<SessionId, ActiveTerminal>,
}
pub struct RunscInteractiveSessionExecutor {
engine: Arc<Mutex<SessionTerminalEngine>>,
prompt_transport: Arc<SessionPromptTransport>,
domain_id: DomainId,
generation_id: GenerationId,
coordinator_id: CoordinatorId,
container_id: ContainerId,
host_coordinator: PidfsProcessIdentityV1,
runsc_program: OsString,
runsc_global_arguments: Vec<OsString>,
runtime_root: PathBuf,
}
impl RunscInteractiveSessionExecutor {
/// Creates the generation-bound control adapter for the coordinator-owned terminal engine.
///
/// # Errors
/// Refuses a run command without one exact `run`/bundle/container suffix.
pub fn new(
store: Arc<HostCoordinatorDomainStore>,
generation: &lazaret_core::records::GenerationIdentity,
container_id: ContainerId,
host_coordinator: PidfsProcessIdentityV1,
command: &lazaret_runtime_supervisor::sandbox::RunscCommand,
runtime_root: PathBuf,
) -> Result<Self, TerminalError> {
let run = command
.arguments()
.iter()
.position(|argument| argument == "run")
.ok_or(TerminalError::AuthorityMismatch)?;
if run + 3 != command.arguments().len() {
return Err(TerminalError::AuthorityMismatch);
}
let prompt_transport = Arc::new(SessionPromptTransport::new(Arc::clone(&store)));
Ok(Self {
engine: Arc::new(Mutex::new(SessionTerminalEngine::new(
store,
Arc::new(RunscSessionTerminalFactory),
prompt_transport.clone(),
))),
prompt_transport,
domain_id: generation.domain_id,
generation_id: generation.generation_id,
coordinator_id: generation.coordinator_id,
container_id,
host_coordinator,
runsc_program: command.program().to_owned(),
runsc_global_arguments: command.arguments()[..run].to_vec(),
runtime_root,
})
}
#[must_use]
pub fn prompt_transport(&self) -> Arc<SessionPromptTransport> {
Arc::clone(&self.prompt_transport)
}
}
impl InteractiveSessionExecutor for RunscInteractiveSessionExecutor {
fn start(
&self,
request: &InteractiveSessionRequest,
) -> Result<Box<dyn InteractiveControlSession>, ControlError> {
if request.protocol_version != ProtocolVersion::V1
|| request.initial_size.session_id != request.session_id
{
return Err(ControlError::InvalidRequest);
}
let acquisition = TerminalAcquisition {
domain_id: self.domain_id,
generation_id: self.generation_id,
coordinator_id: self.coordinator_id,
session_id: request.session_id,
container_id: self.container_id,
host_coordinator: self.host_coordinator,
runsc_program: self.runsc_program.clone(),
runsc_global_arguments: self.runsc_global_arguments.clone(),
internal_pid_file: self.runtime_root.join(format!(
"session-{}.pid",
hex_session(request.session_id.as_bytes())
)),
argv: request.argv.clone(),
};
let mut engine = self.engine.lock().map_err(|_| ControlError::LockPoisoned)?;
engine
.acquire(&acquisition)
.map_err(|error| terminal_control_error(&error))?;
if let Err(error) = engine.handle(
self.generation_id,
SessionTerminalEvent::Resize(request.initial_size),
) {
let _ = engine.lose(
request.session_id,
self.generation_id,
TerminalLoss::ClientDisconnected,
);
return Err(terminal_control_error(&error));
}
drop(engine);
Ok(Box::new(RunscInteractiveControlSession {
engine: Arc::clone(&self.engine),
prompt_transport: Arc::clone(&self.prompt_transport),
generation_id: self.generation_id,
session_id: request.session_id,
}))
}
}
struct RunscInteractiveControlSession {
engine: Arc<Mutex<SessionTerminalEngine>>,
prompt_transport: Arc<SessionPromptTransport>,
generation_id: GenerationId,
session_id: SessionId,
}
impl InteractiveControlSession for RunscInteractiveControlSession {
fn exchange(
&mut self,
event: InteractiveSessionEvent,
) -> Result<InteractiveSessionBatch, ControlError> {
let mut engine = self.engine.lock().map_err(|_| ControlError::LockPoisoned)?;
let mut messages = match event {
InteractiveSessionEvent::Terminal(SessionTerminalEvent::Resize(resize)) => {
if self
.prompt_transport
.retain_prompt_resize(resize)
.map_err(|error| terminal_control_error(&error))?
{
Vec::new()
} else {
engine
.handle(self.generation_id, SessionTerminalEvent::Resize(resize))
.map_err(|error| terminal_control_error(&error))?
}
}
InteractiveSessionEvent::Terminal(event) => engine
.handle(self.generation_id, event)
.map_err(|error| terminal_control_error(&error))?,
InteractiveSessionEvent::Poll { session_id } if session_id == self.session_id => {
Vec::new()
}
InteractiveSessionEvent::PromptDecision {
session_id,
signed_decision,
} if session_id == self.session_id => {
let decision = self
.prompt_transport
.route_decision(session_id, signed_decision)
.map_err(|error| terminal_control_error(&error))?;
if decision.decision.decision == lazaret_core::records::DeveloperDecisionKind::Deny
{
self.prompt_transport
.restore_sandbox_route(session_id)
.map_err(|error| terminal_control_error(&error))?;
engine
.set_route(session_id, self.generation_id, TerminalRoute::Sandbox)
.map_err(|error| terminal_control_error(&error))?;
if let Some(resize) = decision.resumed_size {
engine
.handle(self.generation_id, SessionTerminalEvent::Resize(resize))
.map_err(|error| terminal_control_error(&error))?;
}
}
self.prompt_transport
.publish_decision(session_id, decision)
.map_err(|error| terminal_control_error(&error))?;
Vec::new()
}
InteractiveSessionEvent::PromptDecision { .. }
| InteractiveSessionEvent::Poll { .. } => return Err(ControlError::InvalidRequest),
};
if let Some(prompt) = self
.prompt_transport
.prompt_for(self.session_id)
.map_err(|error| terminal_control_error(&error))?
{
engine
.set_route(self.session_id, self.generation_id, TerminalRoute::Prompt)
.map_err(|error| terminal_control_error(&error))?;
return Ok(InteractiveSessionBatch {
session_id: self.session_id,
messages: vec![InteractiveSessionMessage::Prompt(prompt)],
});
}
if !messages
.iter()
.any(|message| matches!(message, SessionTerminalMessage::Exit(_)))
{
messages.extend(
engine
.poll(self.session_id, self.generation_id)
.map_err(|error| terminal_control_error(&error))?,
);
}
Ok(InteractiveSessionBatch {
session_id: self.session_id,
messages: messages
.into_iter()
.map(InteractiveSessionMessage::Terminal)
.collect(),
})
}
}
impl Drop for RunscInteractiveControlSession {
fn drop(&mut self) {
if let Ok(mut engine) = self.engine.lock()
&& engine.contains(self.session_id)
{
let _ = engine.lose(
self.session_id,
self.generation_id,
TerminalLoss::ClientDisconnected,
);
}
}
}
fn terminal_control_error(error: &TerminalError) -> ControlError {
ControlError::Lifecycle(error.to_string())
}
fn hex_session(bytes: &[u8]) -> String {
use std::fmt::Write as _;
bytes.iter().fold(String::new(), |mut output, byte| {
write!(output, "{byte:02x}").expect("writing to a String cannot fail");
output
})
}