mod control;
mod debug;
mod flow;
mod login_path;
mod process;
mod session;
mod tcp;
pub(super) use tcp::handle_tcp_connect;
use std::io::Read;
use std::os::fd::AsRawFd;
use std::process::Stdio;
use anyhow::Context;
use arcbox_connect::v1::MachineExecRequest;
use arcbox_pty::RunAs;
use buffa::Message;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::sync::mpsc;
use crate::rpc::{ErrorResponse, MessageType};
use flow::Flow;
use process::ProcessSpec;
use session::{Chunk, OUTPUT_CHANNEL_CAPACITY, OUTPUT_CHUNK, OutFrame, Streams};
pub(super) async fn handle_machine_exec<S>(
stream: &mut S,
trace_id: &str,
payload: &[u8],
) -> anyhow::Result<()>
where
S: AsyncRead + AsyncWrite + Unpin,
{
let req = MachineExecRequest::decode_from_slice(payload)
.context("failed to decode MachineExecRequest")?;
let flow = match Flow::new(req.output_window) {
Ok(flow) => flow,
Err(err) => return session::write_error(stream, trace_id, &err).await,
};
if let Some(window) = flow.initial_stdin_window() {
session::write_window(stream, trace_id, window).await?;
}
let spec = match ProcessSpec::resolve(&req).await {
Ok(spec) => spec,
Err(err) => return session::write_error(stream, trace_id, &err).await,
};
run_session(stream, trace_id, &req, spec, &flow, None).await
}
pub(super) async fn handle_debug_exec<S>(
stream: &mut S,
trace_id: &str,
payload: &[u8],
) -> anyhow::Result<()>
where
S: AsyncRead + AsyncWrite + Unpin,
{
let req = MachineExecRequest::decode_from_slice(payload)
.context("failed to decode DebugExecRequest")?;
let flow = match Flow::new(req.output_window) {
Ok(flow) => flow,
Err(err) => return session::write_error(stream, trace_id, &err).await,
};
if let Some(window) = flow.initial_stdin_window() {
session::write_window(stream, trace_id, window).await?;
}
let nsenter = match debug::NsEnter::resolve(&req.container).await {
Ok(ns) => ns,
Err(err) => return session::write_error(stream, trace_id, &err).await,
};
let spec = match ProcessSpec::resolve(&req).await {
Ok(spec) => spec,
Err(err) => return session::write_error(stream, trace_id, &err).await,
};
run_session(stream, trace_id, &req, spec, &flow, Some(nsenter)).await
}
async fn run_session<S>(
stream: &mut S,
trace_id: &str,
req: &MachineExecRequest,
spec: ProcessSpec,
flow: &Flow,
nsenter: Option<debug::NsEnter>,
) -> anyhow::Result<()>
where
S: AsyncRead + AsyncWrite + Unpin,
{
let out = OutFrame {
trace_id,
msg_type: if nsenter.is_some() {
MessageType::DebugExecResponse
} else {
MessageType::MachineExecOutput
},
};
if req.tty {
tty_session(stream, out, req, spec, flow, nsenter).await
} else {
piped_session(stream, out, req, spec, flow, nsenter).await
}
}
async fn piped_session<S>(
stream: &mut S,
out: OutFrame<'_>,
req: &MachineExecRequest,
spec: ProcessSpec,
flow: &Flow,
nsenter: Option<debug::NsEnter>,
) -> anyhow::Result<()>
where
S: AsyncRead + AsyncWrite + Unpin,
{
let mut cmd = spec.command();
cmd.stdin(if req.attach_stdin {
Stdio::piped()
} else {
Stdio::null()
});
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
let run_as = spec.run_as.clone();
unsafe {
cmd.pre_exec(move || {
if let Some(ns) = &nsenter {
ns.apply()?;
}
nix::unistd::setsid()?;
run_as.as_ref().map_or(Ok(()), RunAs::apply)
});
}
let mut child = match cmd.spawn() {
Ok(child) => child,
Err(e) => return session::write_error(stream, out.trace_id, &spec.spawn_error(e)).await,
};
let (output_tx, output) = mpsc::channel(OUTPUT_CHANNEL_CAPACITY);
let stdout = child.stdout.take().context("stdout not piped")?;
let stderr = child.stderr.take().context("stderr not piped")?;
session::read_output(stdout, "stdout", output_tx.clone());
session::read_output(stderr, "stderr", output_tx);
let (stdin, stdin_rx) = mpsc::unbounded_channel();
let (delivered_tx, delivered) = mpsc::unbounded_channel();
tokio::spawn(session::write_stdin(
child.stdin.take(),
stdin_rx,
delivered_tx,
));
let streams = Streams {
output,
stdin,
delivered,
};
session::run(stream, out, flow, streams, None, &mut child).await
}
async fn tty_session<S>(
stream: &mut S,
out: OutFrame<'_>,
req: &MachineExecRequest,
spec: ProcessSpec,
flow: &Flow,
nsenter: Option<debug::NsEnter>,
) -> anyhow::Result<()>
where
S: AsyncRead + AsyncWrite + Unpin,
{
let size = req.tty_size.as_option().map(|s| arcbox_pty::WinSize {
cols: u16::try_from(s.width).unwrap_or(80),
rows: u16::try_from(s.height).unwrap_or(24),
});
let pty = match arcbox_pty::openpty_sized(size) {
Ok(pty) => pty,
Err(e) => {
let err = ErrorResponse::new(500, format!("openpty: {e}"));
return session::write_error(stream, out.trace_id, &err).await;
}
};
let mut cmd = spec.command();
cmd.stdin(Stdio::null());
cmd.stdout(Stdio::null());
cmd.stderr(Stdio::null());
let mut terminal = arcbox_pty::child_terminal_setup(pty.slave.as_raw_fd(), spec.run_as.clone());
unsafe {
cmd.pre_exec(move || {
if let Some(ns) = &nsenter {
ns.apply()?;
}
terminal()
});
}
let mut child = match cmd.spawn() {
Ok(child) => child,
Err(e) => return session::write_error(stream, out.trace_id, &spec.spawn_error(e)).await,
};
drop(pty.slave);
let (output_tx, output) = mpsc::channel(OUTPUT_CHANNEL_CAPACITY);
drop(tokio::task::spawn_blocking({
let master = std::fs::File::from(pty.master.try_clone()?);
move || read_terminal(master, &output_tx)
}));
let (stdin, stdin_rx) = mpsc::unbounded_channel();
let (delivered_tx, delivered) = mpsc::unbounded_channel();
drop(tokio::task::spawn_blocking({
let master = std::fs::File::from(pty.master.try_clone()?);
move || session::write_terminal(master, stdin_rx, delivered_tx)
}));
let streams = Streams {
output,
stdin,
delivered,
};
session::run(stream, out, flow, streams, Some(&pty.master), &mut child).await
}
fn read_terminal(mut master: std::fs::File, output: &mpsc::Sender<Chunk>) {
let mut buf = [0u8; OUTPUT_CHUNK];
loop {
match master.read(&mut buf) {
Ok(0) | Err(_) => break,
Ok(n) => {
let chunk = Chunk {
stream: "stdout",
data: buf[..n].to_vec(),
};
if output.blocking_send(chunk).is_err() {
break;
}
}
}
}
}