Add interactive client terminal transport
This commit is contained in:
parent
d597b05484
commit
6816f0632e
1 changed files with 1454 additions and 59 deletions
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in a new issue