use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use anyhow::{Context, Result, bail};
use bytes::Bytes;
use oxdock_core::{StepCtx, Value};
use oxdock_pipe::PipeInner;
use oxdock_process::{ProcessManager, SharedInput, SharedOutput};
use tokio::sync::mpsc;
use crate::state::{DownMsg, UpMsg};
pub(crate) const CHUNK: usize = 8192;
pub(crate) const TICK: Duration = Duration::from_millis(10);
pub(crate) const BACKSTOP: Duration = Duration::from_millis(10);
pub(crate) fn read_pipe(
reader: &SharedInput,
backend: Option<&Arc<PipeInner>>,
buffer: &mut [u8],
) -> std::io::Result<Option<usize>> {
match backend {
Some(inner) => inner.read_into_timeout(buffer, BACKSTOP),
None => {
let mut guard = reader
.lock()
.map_err(|_| std::io::Error::other("stdin lock poisoned"))?;
guard.read(buffer).map(Some)
}
}
}
struct PumpHandles {
reader: SharedInput,
backend: Option<Arc<PipeInner>>,
writer: SharedOutput,
up_rx: mpsc::Receiver<UpMsg>,
down_tx: mpsc::Sender<DownMsg>,
}
fn pump_out(
writer: &SharedOutput,
up_rx: &mut mpsc::Receiver<UpMsg>,
cancel: &AtomicBool,
peer_done: &AtomicBool,
) -> Result<()> {
use tokio::sync::mpsc::error::TryRecvError;
loop {
if cancel.load(Ordering::SeqCst) {
break;
}
match up_rx.try_recv() {
Ok(UpMsg::Data(bytes)) => {
let mut guard = writer
.lock()
.map_err(|_| anyhow::anyhow!("SSH pump output lock poisoned"))?;
guard
.write_all(&bytes)
.context("SSH pump output pipe write failed")?;
guard.flush().context("SSH pump output pipe flush failed")?;
}
Ok(UpMsg::Eof) | Err(TryRecvError::Disconnected) => break,
Err(TryRecvError::Empty) => {
if peer_done.load(Ordering::SeqCst) {
break;
}
std::thread::sleep(TICK);
}
}
}
Ok(())
}
fn pump_in(
reader: &SharedInput,
backend: Option<&Arc<PipeInner>>,
down_tx: &mpsc::Sender<DownMsg>,
cancel: &AtomicBool,
) -> Result<()> {
let mut buffer = [0u8; CHUNK];
loop {
if cancel.load(Ordering::SeqCst) {
break;
}
match read_pipe(reader, backend, &mut buffer) {
Err(err) => bail!("SSH pump input pipe read failed: {err}"),
Ok(None) => continue,
Ok(Some(0)) => {
let _ = down_tx.blocking_send(DownMsg::Eof);
break;
}
Ok(Some(count)) => {
if down_tx
.blocking_send(DownMsg::Data(Bytes::copy_from_slice(&buffer[..count])))
.is_err()
{
break;
}
}
}
}
Ok(())
}
fn reap(
slot: &mut Option<std::thread::ScopedJoinHandle<'_, Result<()>>>,
failed: &mut Option<anyhow::Error>,
) {
if !slot.as_ref().is_some_and(|handle| handle.is_finished()) {
return;
}
let Some(handle) = slot.take() else {
return;
};
match handle.join() {
Ok(Ok(())) => {}
Ok(Err(err)) => {
if failed.is_none() {
*failed = Some(err);
}
}
Err(_) => {
if failed.is_none() {
*failed = Some(anyhow::anyhow!("SSH pump worker panicked"));
}
}
}
}
pub fn pump_session<P: ProcessManager>(
cx: &StepCtx<P>,
in_pipe: &Value,
out_pipe: &Value,
up_rx: mpsc::Receiver<UpMsg>,
down_tx: mpsc::Sender<DownMsg>,
cancel: &AtomicBool,
) -> Result<()> {
let reader = cx
.pipe_reader(in_pipe)
.context("SSH pump cannot borrow the input pipe")?;
let writer = cx
.pipe_writer(out_pipe)
.context("SSH pump cannot borrow the output pipe")?;
let backend = cx.pipe_backend(in_pipe);
let handles = PumpHandles {
reader,
backend,
writer,
up_rx,
down_tx,
};
let PumpHandles {
reader,
backend,
writer,
mut up_rx,
down_tx,
} = handles;
let mut failed: Option<anyhow::Error> = None;
let mut out_closed = false;
let peer_done = AtomicBool::new(false);
std::thread::scope(|scope| {
let mut worker_in = Some(scope.spawn(|| {
let result = pump_in(&reader, backend.as_ref(), &down_tx, cancel);
if result.is_ok() {
peer_done.store(true, Ordering::SeqCst);
}
result
}));
let mut worker_out =
Some(scope.spawn(|| pump_out(&writer, &mut up_rx, cancel, &peer_done)));
loop {
reap(&mut worker_in, &mut failed);
let out_was_live = worker_out.is_some();
reap(&mut worker_out, &mut failed);
if out_was_live && worker_out.is_none() && !out_closed {
out_closed = true;
let _ = cx.close_pipe(out_pipe);
}
if worker_in.is_none() && worker_out.is_none() {
break;
}
if cx.is_cancelled() {
cancel.store(true, Ordering::SeqCst);
}
std::thread::sleep(TICK);
}
});
match failed {
Some(err) => Err(err),
None => Ok(()),
}
}
pub fn pump_pipe_to_pipe<P: ProcessManager>(
cx: &StepCtx<P>,
from_pipe: &Value,
to_pipe: &Value,
cancel: &AtomicBool,
) -> Result<i64> {
let reader = cx
.pipe_reader(from_pipe)
.context("SSH_PUMP cannot borrow the source pipe")?;
let writer = cx
.pipe_writer(to_pipe)
.context("SSH_PUMP cannot borrow the target pipe")?;
let backend = cx.pipe_backend(from_pipe);
let mut buffer = [0u8; CHUNK];
let mut total: i64 = 0;
loop {
if cancel.load(Ordering::SeqCst) || cx.is_cancelled() {
break;
}
match read_pipe(&reader, backend.as_ref(), &mut buffer) {
Err(err) => bail!("SSH_PUMP source pipe read failed: {err}"),
Ok(None) => continue,
Ok(Some(0)) => break,
Ok(Some(count)) => {
{
let mut guard = writer
.lock()
.map_err(|_| anyhow::anyhow!("SSH_PUMP target lock poisoned"))?;
guard
.write_all(&buffer[..count])
.context("SSH_PUMP target pipe write failed")?;
guard.flush().context("SSH_PUMP target pipe flush failed")?;
}
total += count as i64;
}
}
}
let _ = cx.close_pipe(to_pipe);
Ok(total)
}