use std::collections::{BTreeMap, HashMap};
use std::sync::Arc;
use a3s_box_core::pty::PtyRequest;
use a3s_box_core::{
BoxError, ExecEvent, ExecOutput, ExecRequest, ExecutionGeneration, ExecutionId,
ExecutionManagerError, ExecutionManagerResult, ExecutionProcess, ExecutionProcessInput,
ExecutionProcessSignal, ExecutionProcessStream, ExecutionSessionManager, FileRequest,
FileResponse, FilesystemRequest, FilesystemResponse,
};
use async_trait::async_trait;
use super::LocalExecutionManager;
use crate::{
BoxRecord, ExecClient, PtyClient, StreamingExec, StreamingExecInput, StreamingPty,
StreamingPtyInput,
};
#[async_trait]
impl ExecutionSessionManager for LocalExecutionManager {
async fn execute(
&self,
execution_id: &ExecutionId,
generation: ExecutionGeneration,
mut request: ExecRequest,
) -> ExecutionManagerResult<ExecOutput> {
request.streaming = false;
let (record, client, stream) = self.bind_exec(execution_id, generation).await?;
inherit_container_environment(&record.env, &mut request.env);
debug_session_environment(
execution_id,
generation,
"execute",
&record.env,
&request.env,
);
client
.exec_command_on_stream(stream, &request)
.await
.map_err(|error| session_error(execution_id, "execute command", error))
}
async fn start_process(
&self,
execution_id: &ExecutionId,
generation: ExecutionGeneration,
mut request: ExecRequest,
) -> ExecutionManagerResult<ExecutionProcess> {
let (record, client, stream) = self.bind_exec(execution_id, generation).await?;
inherit_container_environment(&record.env, &mut request.env);
debug_session_environment(
execution_id,
generation,
"start_process",
&record.env,
&request.env,
);
let stream = client
.exec_stream_on_stream(stream, &request)
.await
.map_err(|error| session_error(execution_id, "start command", error))?;
let input: Arc<dyn ExecutionProcessInput> = Arc::new(ExecInput {
execution_id: execution_id.clone(),
input: stream.input(),
});
Ok(Box::new(ExecStream { stream, input }))
}
async fn start_pty(
&self,
execution_id: &ExecutionId,
generation: ExecutionGeneration,
mut request: PtyRequest,
) -> ExecutionManagerResult<ExecutionProcess> {
let record = self
.require_running_record(execution_id, generation)
.await?;
inherit_container_environment(&record.env, &mut request.env);
debug_session_environment(
execution_id,
generation,
"start_pty",
&record.env,
&request.env,
);
let socket_path = record.exec_socket_path.with_file_name("pty.sock");
let client = PtyClient::connect(&socket_path)
.await
.map_err(|error| session_error(execution_id, "connect PTY", error))?;
self.require_same_runtime(&record, execution_id, generation)
.await?;
let stream = client
.start_stream(&request)
.await
.map_err(|error| session_error(execution_id, "start PTY", error))?;
let input: Arc<dyn ExecutionProcessInput> = Arc::new(PtyInput {
execution_id: execution_id.clone(),
input: stream.input(),
});
Ok(Box::new(PtyStream { stream, input }))
}
async fn transfer_file(
&self,
execution_id: &ExecutionId,
generation: ExecutionGeneration,
request: FileRequest,
) -> ExecutionManagerResult<FileResponse> {
let (_record, client, stream) = self.bind_exec(execution_id, generation).await?;
client
.file_transfer_on_stream(stream, &request)
.await
.map_err(|error| session_error(execution_id, "transfer file", error))
}
async fn filesystem(
&self,
execution_id: &ExecutionId,
generation: ExecutionGeneration,
request: FilesystemRequest,
) -> ExecutionManagerResult<FilesystemResponse> {
let (_record, client, stream) = self.bind_exec(execution_id, generation).await?;
client
.filesystem_on_stream(stream, &request)
.await
.map_err(|error| session_error(execution_id, "access filesystem", error))
}
}
impl LocalExecutionManager {
async fn bind_exec(
&self,
execution_id: &ExecutionId,
generation: ExecutionGeneration,
) -> ExecutionManagerResult<(BoxRecord, ExecClient, tokio::net::UnixStream)> {
let record = self
.require_running_record(execution_id, generation)
.await?;
let client = ExecClient::for_socket(&record.exec_socket_path);
let stream = client
.open_stream()
.await
.map_err(|error| session_error(execution_id, "connect exec", error))?;
self.require_same_runtime(&record, execution_id, generation)
.await?;
Ok((record, client, stream))
}
async fn require_same_runtime(
&self,
bound: &BoxRecord,
execution_id: &ExecutionId,
generation: ExecutionGeneration,
) -> ExecutionManagerResult<()> {
let current = self
.require_running_record(execution_id, generation)
.await?;
if current.pid != bound.pid
|| current.pid_start_time != bound.pid_start_time
|| current.exec_socket_path != bound.exec_socket_path
{
return Err(ExecutionManagerError::Conflict {
execution_id: execution_id.clone(),
message: "runtime generation changed while binding its execution session"
.to_string(),
});
}
Ok(())
}
}
fn inherit_container_environment(container: &HashMap<String, String>, request: &mut Vec<String>) {
let mut merged: BTreeMap<String, String> = container
.iter()
.map(|(key, value)| (key.clone(), value.clone()))
.collect();
let mut malformed = Vec::new();
for entry in std::mem::take(request) {
if let Some((key, value)) = entry.split_once('=') {
merged.insert(key.to_string(), value.to_string());
} else {
malformed.push(entry);
}
}
request.extend(
merged
.into_iter()
.map(|(key, value)| format!("{key}={value}")),
);
request.extend(malformed);
}
fn debug_session_environment(
execution_id: &ExecutionId,
generation: ExecutionGeneration,
operation: &str,
container: &HashMap<String, String>,
request: &[String],
) {
let mut container_keys: Vec<&str> = container.keys().map(String::as_str).collect();
container_keys.sort_unstable();
let mut request_keys: Vec<&str> = request
.iter()
.filter_map(|entry| entry.split_once('=').map(|(key, _)| key))
.collect();
request_keys.sort_unstable();
request_keys.dedup();
let malformed_request_entries = request.iter().filter(|entry| !entry.contains('=')).count();
tracing::debug!(
%execution_id,
generation = generation.get(),
operation,
container_env_count = container.len(),
container_env_keys = ?container_keys,
merged_request_env_count = request.len(),
merged_request_env_keys = ?request_keys,
malformed_request_entries,
"Prepared managed execution session environment"
);
}
struct ExecInput {
execution_id: ExecutionId,
input: StreamingExecInput,
}
#[async_trait]
impl ExecutionProcessInput for ExecInput {
async fn write_stdin(&self, data: &[u8]) -> ExecutionManagerResult<()> {
self.input
.write_stdin(data)
.await
.map_err(|error| session_error(&self.execution_id, "write command stdin", error))
}
async fn close_stdin(&self) -> ExecutionManagerResult<()> {
self.input
.close_stdin()
.await
.map_err(|error| session_error(&self.execution_id, "close command stdin", error))
}
async fn cancel(&self) -> ExecutionManagerResult<()> {
self.input
.cancel()
.await
.map_err(|error| session_error(&self.execution_id, "cancel command", error))
}
async fn send_signal(&self, signal: ExecutionProcessSignal) -> ExecutionManagerResult<()> {
self.input
.send_signal(signal)
.await
.map_err(|error| session_error(&self.execution_id, "signal command", error))
}
}
struct ExecStream {
stream: StreamingExec,
input: Arc<dyn ExecutionProcessInput>,
}
#[async_trait]
impl ExecutionProcessStream for ExecStream {
fn input(&self) -> Arc<dyn ExecutionProcessInput> {
self.input.clone()
}
async fn next_event(&mut self) -> ExecutionManagerResult<Option<ExecEvent>> {
self.stream
.next_event()
.await
.map_err(|error| ExecutionManagerError::Unavailable(error.to_string()))
}
}
struct PtyInput {
execution_id: ExecutionId,
input: StreamingPtyInput,
}
#[async_trait]
impl ExecutionProcessInput for PtyInput {
async fn write_stdin(&self, data: &[u8]) -> ExecutionManagerResult<()> {
self.input
.write_stdin(data)
.await
.map_err(|error| session_error(&self.execution_id, "write PTY stdin", error))
}
async fn close_stdin(&self) -> ExecutionManagerResult<()> {
self.cancel().await
}
async fn cancel(&self) -> ExecutionManagerResult<()> {
self.input
.close()
.await
.map_err(|error| session_error(&self.execution_id, "close PTY", error))
}
async fn send_signal(&self, signal: ExecutionProcessSignal) -> ExecutionManagerResult<()> {
self.input
.send_signal(signal)
.await
.map_err(|error| session_error(&self.execution_id, "signal PTY", error))
}
async fn resize_pty(&self, cols: u16, rows: u16) -> ExecutionManagerResult<()> {
self.input
.resize(cols, rows)
.await
.map_err(|error| session_error(&self.execution_id, "resize PTY", error))
}
}
struct PtyStream {
stream: StreamingPty,
input: Arc<dyn ExecutionProcessInput>,
}
#[async_trait]
impl ExecutionProcessStream for PtyStream {
fn input(&self) -> Arc<dyn ExecutionProcessInput> {
self.input.clone()
}
async fn next_event(&mut self) -> ExecutionManagerResult<Option<ExecEvent>> {
self.stream
.next_event()
.await
.map_err(|error| ExecutionManagerError::Unavailable(error.to_string()))
}
}
fn session_error(
execution_id: &ExecutionId,
operation: &str,
error: BoxError,
) -> ExecutionManagerError {
ExecutionManagerError::Unavailable(format!(
"failed to {operation} for execution {execution_id}: {error}"
))
}
#[cfg(test)]
mod tests {
use super::inherit_container_environment;
use std::collections::HashMap;
#[test]
fn request_environment_overrides_inherited_container_values() {
let container = HashMap::from([
("ALPHA".to_string(), "container".to_string()),
("BETA".to_string(), "container".to_string()),
]);
let mut request = vec!["BETA=request".to_string(), "GAMMA=request".to_string()];
inherit_container_environment(&container, &mut request);
assert_eq!(
request,
["ALPHA=container", "BETA=request", "GAMMA=request"]
);
}
#[test]
fn malformed_request_entries_are_preserved_after_inherited_values() {
let container = HashMap::from([("ALPHA".to_string(), "container".to_string())]);
let mut request = vec!["MALFORMED".to_string()];
inherit_container_environment(&container, &mut request);
assert_eq!(request, ["ALPHA=container", "MALFORMED"]);
}
}