Add interactive client terminal transport

This commit is contained in:
ms2k 2026-07-17 06:59:39 +09:00
commit 6816f0632e

View file

@ -14,11 +14,13 @@ use lazaret_core::identity::{
PidfsProcessIdentityV1, SessionId,
};
use lazaret_core::records::{
DomainStore, DurableDecision, GuestProcessIdentity, ParticipantOwnership, ParticipantRole,
ParticipantState, PersistedHostProcessOwnership, ProcessOwnershipCommitAck, PtyIdentity,
SessionExit, SessionPhase, SessionTerminal, SessionTerminalEvent, SessionTerminalFields,
SessionTerminalMessage, SessionTerminalState, StagedProcessOwnership, TerminalOutput,
TerminalResize, TerminalSignalKind,
DomainStore, DurableDecision, GuestProcessIdentity, InteractiveSessionBatch,
InteractiveSessionEvent, InteractiveSessionMessage, InteractiveSessionRequest,
ParticipantOwnership, ParticipantRole, ParticipantState, PersistedHostProcessOwnership,
ProcessOwnershipCommitAck, ProtocolVersion, PtyIdentity, SessionExit, SessionPhase,
SessionTerminal, SessionTerminalEvent, SessionTerminalFields, SessionTerminalMessage,
SessionTerminalState, StagedProcessOwnership, TerminalOutput, TerminalResize,
TerminalSignalKind,
};
use lazaret_linux::process::{
ChildStandardIo, ProcessExit, ProcessOwnershipContext, RetainedProcess, TargetCommand,
@ -28,6 +30,7 @@ use rustix::fs::{OFlags, fcntl_getfl, fcntl_setfl, fstat};
use rustix::pty::{OpenptFlags, grantpt, ioctl_tiocgptpeer, openpt, unlockpt};
use rustix::termios::{Winsize, tcsetwinsize};
use crate::control::{ControlError, InteractiveControlSession, InteractiveSessionExecutor};
use crate::domain_store::{DomainStoreError, DomainStoreMutation, HostCoordinatorDomainStore};
use crate::sandbox_init::{PRIVATE_SESSION_EXEC_ROLE, PRIVATE_SESSION_IDENTITY_ROLE};
@ -406,6 +409,175 @@ pub struct SessionTerminalEngine {
active: HashMap<SessionId, ActiveTerminal>,
}
pub struct RunscInteractiveSessionExecutor {
engine: Arc<Mutex<SessionTerminalEngine>>,
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);
}
Ok(Self {
engine: Arc::new(Mutex::new(SessionTerminalEngine::new(
store,
Arc::new(RunscSessionTerminalFactory),
Arc::new(NoActivePrompt),
))),
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,
})
}
}
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),
generation_id: self.generation_id,
session_id: request.session_id,
}))
}
}
struct RunscInteractiveControlSession {
engine: Arc<Mutex<SessionTerminalEngine>>,
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(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 { .. }
| InteractiveSessionEvent::Poll { .. } => return Err(ControlError::InvalidRequest),
};
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
})
}
impl SessionTerminalEngine {
#[must_use]
pub fn new(