#![cfg_attr(not(unix), allow(dead_code))]
use std::collections::VecDeque;
use std::io;
use std::time::Duration;
use futures_util::FutureExt as _;
use futures_util::future::LocalBoxFuture;
use shep_client::{Client, EventStream, Lagged, RequestError};
use shep_core::protocol::{BusEvent, MAX_FRAME_BYTES, Request, Response};
use tokio::sync::mpsc;
use crate::cli::{AttachArgs, Format};
use crate::commands::rpc::{client_error, unexpected_response};
use crate::exit::ExitCode;
use crate::lookout::term::RestoreGuard;
use crate::output::Streams;
mod keys;
#[cfg(unix)]
mod tty;
#[cfg(test)]
mod tests;
pub(crate) const DETACH_KEY: u8 = 0x1d;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Size {
pub rows: u16,
pub cols: u16,
}
pub(crate) struct Tty {
pub size: Size,
pub keys: mpsc::Receiver<Vec<u8>>,
pub resizes: mpsc::Receiver<Size>,
pub restore: RestoreGuard,
}
#[cfg(unix)]
pub(crate) async fn attach(
client: &Client,
streams: &mut Streams<'_>,
args: &AttachArgs,
) -> ExitCode {
use tokio::signal::unix::{SignalKind, signal};
let mut terminate = match signal(SignalKind::terminate()) {
Ok(terminate) => terminate,
Err(err) => {
return streams.fail(
ExitCode::Failure,
&format!("could not listen for SIGTERM: {err}"),
);
}
};
let tty = match tty::open() {
Ok(tty) => tty,
Err(err) => {
return streams.fail(
ExitCode::Failure,
&format!("could not put the terminal into raw mode: {err}"),
);
}
};
tokio::select! {
code = session(client, streams, &args.name, tty) => code,
_ = terminate.recv() => streams.fail(ExitCode::Failure, "terminated by SIGTERM"),
}
}
#[cfg(unix)]
pub(crate) fn unsupported(_streams: &mut Streams<'_>) -> Option<ExitCode> {
None
}
#[cfg(not(unix))]
pub(crate) fn unsupported(streams: &mut Streams<'_>) -> Option<ExitCode> {
Some(streams.fail(
ExitCode::Failure,
"`shep attach` is not available on Windows",
))
}
#[cfg(not(unix))]
pub(crate) async fn attach(
_client: &Client,
streams: &mut Streams<'_>,
_args: &AttachArgs,
) -> ExitCode {
unsupported(streams).unwrap_or(ExitCode::Failure)
}
enum End {
Detached,
SheepExited,
Request(RequestError),
Write(io::Error),
}
pub(crate) async fn session(
client: &Client,
streams: &mut Streams<'_>,
name: &str,
tty: Tty,
) -> ExitCode {
let Tty {
size,
keys,
resizes,
restore,
} = tty;
let mut events = match client.events().await {
Ok(events) => events,
Err(err) => {
drop(restore);
return client_error(streams, &err);
}
};
let attach = Request::Attach {
name: name.to_owned(),
rows: size.rows,
cols: size.cols,
};
let end = match client.request(attach).await {
Ok(Response::Attached { .. }) => {
pump(client, streams, name, &mut events, keys, resizes).await
}
Ok(_) => {
drop(restore);
return unexpected_response(streams);
}
Err(err) => End::Request(err),
};
drop(restore);
match end {
End::Detached => closing_line(streams, "detached"),
End::SheepExited => closing_line(streams, "the sheep exited"),
End::Request(err) => return client_error(streams, &err),
End::Write(err) => {
let message = format!("writing the sheep's output: {err}");
return streams.fail(ExitCode::Failure, &message);
}
}
ExitCode::Success
}
const RESET_MODES: &str = "\x1b[?9l\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1005l\x1b[?1006l\x1b[?1015l\
\x1b[?1004l\x1b[?1l\x1b>\x1b[<99u\x1b[>4;0m\x1b[0m";
pub(crate) fn reset_modes(out: &mut impl io::Write) -> io::Result<()> {
out.write_all(RESET_MODES.as_bytes())?;
out.flush()
}
const DETACH_FLUSH: Duration = Duration::from_millis(250);
const MAX_INPUT_BYTES: usize = MAX_FRAME_BYTES / 16;
async fn pump<'a>(
client: &'a Client,
streams: &mut Streams<'_>,
name: &str,
events: &mut EventStream,
mut keys: mpsc::Receiver<Vec<u8>>,
mut resizes: mpsc::Receiver<Size>,
) -> End {
let typed = keys::Keys;
let mut queued: VecDeque<u8> = VecDeque::new();
let mut resize: Option<Size> = None;
let mut in_flight: Option<LocalBoxFuture<'a, Result<Response, RequestError>>> = None;
loop {
if in_flight.is_none() {
let body = if queued.is_empty() {
resize.take().map(|size| Request::PtyResize {
name: name.to_owned(),
rows: size.rows,
cols: size.cols,
})
} else {
Some(input(name, &mut queued))
};
in_flight = body.map(|body| client.request(body).boxed_local());
}
tokio::select! {
event = events.next() => {
if let Err(end) = show(streams, name, event) {
return end;
}
}
chunk = keys.recv() => {
let Some(chunk) = chunk else {
return detach(client, name, in_flight.take(), queued).await;
};
let typed_now = typed.push(&chunk);
queued.extend(typed_now.bytes);
if typed_now.detach {
return detach(client, name, in_flight.take(), queued).await;
}
}
Some(size) = resizes.recv() => resize = Some(size),
outcome = async {
match in_flight.as_mut() {
Some(request) => request.await,
None => std::future::pending().await,
}
} => {
in_flight = None;
if let Err(err) = outcome {
while let Some(event) = events.next().now_or_never() {
if let Err(end) = show(streams, name, event) {
return end;
}
}
return End::Request(err);
}
}
}
}
}
fn input(name: &str, queued: &mut VecDeque<u8>) -> Request {
let len = queued.len().min(MAX_INPUT_BYTES);
Request::PtyInput {
name: name.to_owned(),
data: queued.drain(..len).collect(),
}
}
async fn detach(
client: &Client,
name: &str,
in_flight: Option<LocalBoxFuture<'_, Result<Response, RequestError>>>,
mut queued: VecDeque<u8>,
) -> End {
let flush = async {
if let Some(request) = in_flight {
let _ = request.await;
}
while !queued.is_empty() {
let _ = client.request(input(name, &mut queued)).await;
}
};
let _ = tokio::time::timeout(DETACH_FLUSH, flush).await;
End::Detached
}
fn show(
streams: &mut Streams<'_>,
name: &str,
event: Option<Result<BusEvent, Lagged>>,
) -> Result<(), End> {
match event {
Some(Ok(BusEvent::PtyOut { name: from, data })) if from == name => streams
.out
.write_all(&data)
.and_then(|()| streams.out.flush())
.map_err(End::Write),
Some(Ok(BusEvent::PtyEnd { name: from })) if from == name => Err(End::SheepExited),
Some(Ok(BusEvent::Dropped { count }) | Err(Lagged { count })) => {
notice(
streams,
&format!("{count} events were dropped; the screen may be out of sync, redraw it"),
);
Ok(())
}
Some(_) => Ok(()),
None => Err(End::Request(RequestError::Closed)),
}
}
fn notice(streams: &mut Streams<'_>, message: &str) {
match streams.fmt {
Format::Table => {
let _ = write!(streams.err, "\r\nshep: {message}\r\n");
}
Format::Json => streams.aside("attach", message),
}
}
fn closing_line(streams: &mut Streams<'_>, message: &str) {
match streams.fmt {
Format::Table => {
let _ = writeln!(streams.err, "{message}");
}
Format::Json => streams.aside("attach", message),
}
}