use std::collections::VecDeque;
use std::pin::Pin;
use std::task::{Context, Poll};
use futures_core::Stream;
use tokio::io::{AsyncBufReadExt as _, AsyncWriteExt as _, BufReader};
use tokio::process::{Child, ChildStdin, ChildStdout};
use tokio::sync::{mpsc, oneshot, watch};
use crate::limits::ControlLimits;
use crate::{Command, Error, PaneId, Server, SessionId, TmuxText};
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum Event {
Output {
pane: PaneId,
bytes: Vec<u8>,
},
SessionChanged {
session: SessionId,
},
Exit,
Other {
name: String,
rest: TmuxText,
},
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BlockResult {
number: u64,
succeeded: bool,
output: Vec<TmuxText>,
}
impl BlockResult {
#[must_use]
pub const fn number(&self) -> u64 {
self.number
}
#[must_use]
pub const fn succeeded(&self) -> bool {
self.succeeded
}
#[must_use]
pub fn output(&self) -> &[TmuxText] {
&self.output
}
}
#[derive(Debug)]
pub struct ControlMode {
sender: ControlSender,
events: ControlEvents,
}
impl ControlMode {
pub async fn attach(server: &Server, session: &SessionId) -> Result<Self, Error> {
Self::attach_with_limits(server, session, ControlLimits::default()).await
}
pub async fn attach_with_limits(
server: &Server,
session: &SessionId,
limits: ControlLimits,
) -> Result<Self, Error> {
let mut command = tokio::process::Command::new(server.tmux_executable());
command
.arg("-S")
.arg(server.socket_path())
.arg("-C")
.arg("attach")
.arg("-t")
.arg(session.to_string())
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.kill_on_drop(true);
let mut child = command.spawn().map_err(Error::control_mode)?;
let stdin = child.stdin.take().ok_or_else(Error::control_mode_pipes)?;
let stdout = child.stdout.take().ok_or_else(Error::control_mode_pipes)?;
let (commands, queue) = mpsc::channel(COMMAND_QUEUE);
let (events, received) = mpsc::channel(EVENT_QUEUE);
let (stop, stopped) = watch::channel(());
let mut connection = Connection {
child,
stdin,
stdout: BufReader::new(stdout),
limits,
line: Vec::new(),
commands: queue,
events,
stopped,
awaiting: VecDeque::new(),
};
if !connection.discard_opening_block().await? {
return Err(Error::control_mode_closed());
}
Ok(Self {
sender: ControlSender { commands },
events: ControlEvents {
events: received,
stop,
connection: tokio::spawn(connection.run()),
},
})
}
#[must_use]
pub fn split(self) -> (ControlSender, ControlEvents) {
(self.sender, self.events)
}
pub async fn send(&self, command: Command) -> Result<BlockResult, Error> {
self.sender.send(command).await
}
pub async fn next_event(&mut self) -> Option<Event> {
self.events.next_event().await
}
pub async fn shutdown(self) -> Result<(), Error> {
drop(self.sender);
self.events.shutdown().await
}
}
#[derive(Clone, Debug)]
pub struct ControlSender {
commands: mpsc::Sender<Request>,
}
impl ControlSender {
pub async fn send(&self, command: Command) -> Result<BlockResult, Error> {
let line = command
.control_mode_line()
.ok_or_else(Error::control_mode_unrepresentable)?;
let (result, answer) = oneshot::channel();
self.commands
.send(Request { line, result })
.await
.map_err(|_| Error::control_mode_closed())?;
answer.await.map_err(|_| Error::control_mode_closed())?
}
#[must_use]
pub fn is_closed(&self) -> bool {
self.commands.is_closed()
}
}
#[derive(Debug)]
pub struct ControlEvents {
events: mpsc::Receiver<Event>,
stop: watch::Sender<()>,
connection: tokio::task::JoinHandle<Result<(), Error>>,
}
impl ControlEvents {
pub async fn next_event(&mut self) -> Option<Event> {
self.events.recv().await
}
pub async fn shutdown(mut self) -> Result<(), Error> {
let _ = self.stop.send(());
self.events.close();
while self.events.recv().await.is_some() {}
self.connection
.await
.map_err(|_| Error::control_mode_closed())?
}
}
impl Stream for ControlEvents {
type Item = Event;
fn poll_next(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<Event>> {
self.events.poll_recv(context)
}
}
const COMMAND_QUEUE: usize = 16;
const EVENT_QUEUE: usize = 256;
#[derive(Debug)]
pub struct PaneOutput {
pane: PaneId,
events: ControlEvents,
}
impl PaneOutput {
pub(crate) const fn new(pane: PaneId, events: ControlEvents) -> Self {
Self { pane, events }
}
#[must_use]
pub const fn pane(&self) -> &PaneId {
&self.pane
}
pub async fn next_chunk(&mut self) -> Option<Vec<u8>> {
loop {
match self.events.next_event().await? {
Event::Output { pane, bytes } if pane == self.pane => return Some(bytes),
Event::Exit => return None,
_ => {}
}
}
}
pub async fn shutdown(self) -> Result<(), Error> {
self.events.shutdown().await
}
}
impl Stream for PaneOutput {
type Item = Vec<u8>;
fn poll_next(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<Vec<u8>>> {
loop {
match std::task::ready!(self.events.events.poll_recv(context)) {
Some(Event::Output { pane, bytes }) if pane == self.pane => {
return Poll::Ready(Some(bytes));
}
Some(Event::Exit) | None => return Poll::Ready(None),
Some(_) => {}
}
}
}
}
#[derive(Debug)]
struct Request {
line: String,
result: oneshot::Sender<Result<BlockResult, Error>>,
}
enum Step {
Read(Result<Option<Line>, Error>),
Send(Option<Request>),
Unwatched {
asked: bool,
},
}
struct Connection {
child: Child,
stdin: ChildStdin,
stdout: BufReader<ChildStdout>,
limits: ControlLimits,
line: Vec<u8>,
commands: mpsc::Receiver<Request>,
events: mpsc::Sender<Event>,
stopped: watch::Receiver<()>,
awaiting: VecDeque<oneshot::Sender<Result<BlockResult, Error>>>,
}
impl Connection {
async fn run(mut self) -> Result<(), Error> {
let outcome = self.serve().await;
let reason = match &outcome {
Err(Error::ControlModeFrameTooLarge { frame, limit }) => {
Some(Error::control_mode_frame_too_large(frame, *limit))
}
_ => None,
};
while let Some(result) = self.awaiting.pop_front() {
let _ = result.send(Err(reason.as_ref().map_or_else(
Error::control_mode_closed,
|error| match error {
Error::ControlModeFrameTooLarge { frame, limit } => {
Error::control_mode_frame_too_large(frame, *limit)
}
_ => Error::control_mode_closed(),
},
)));
}
drop(self.stdin);
let _ = self.child.wait().await;
outcome
}
async fn serve(&mut self) -> Result<(), Error> {
let mut sending = true;
let mut watching = true;
while sending || watching {
let step = tokio::select! {
line = read_line(&mut self.stdout, &mut self.line, self.limits.max_line_bytes) => Step::Read(line),
request = self.commands.recv(), if sending => Step::Send(request),
asked = self.stopped.changed(), if watching => Step::Unwatched {
asked: asked.is_ok(),
},
};
match step {
Step::Read(Err(error)) => return Err(error),
Step::Read(Ok(None)) | Step::Unwatched { asked: true } => return Ok(()),
Step::Read(Ok(Some(line))) => {
if !self.dispatch(line).await? {
return Ok(());
}
}
Step::Send(Some(request)) => {
if let Err(error) = write_line(&mut self.stdin, &request.line).await {
let _ = request.result.send(Err(Error::control_mode_closed()));
return Err(error);
}
self.awaiting.push_back(request.result);
}
Step::Send(None) => sending = false,
Step::Unwatched { asked: false } => watching = false,
}
}
Ok(())
}
async fn discard_opening_block(&mut self) -> Result<bool, Error> {
loop {
match read_line(&mut self.stdout, &mut self.line, self.limits.max_line_bytes).await? {
Some(Line::BlockStart(number)) => {
self.read_block(number).await?;
return Ok(true);
}
Some(Line::Event(Event::Exit)) => {
self.report(Event::Exit).await;
return Ok(false);
}
Some(Line::Event(event)) => self.report(event).await,
Some(Line::Text(_) | Line::BlockEnd { .. }) => {}
None => return Ok(false),
}
}
}
async fn dispatch(&mut self, line: Line) -> Result<bool, Error> {
match line {
Line::BlockStart(number) => {
let block = self.read_block(number).await?;
if let Some(result) = self.awaiting.pop_front() {
let _ = result.send(Ok(block));
}
Ok(true)
}
Line::Event(Event::Exit) => {
self.report(Event::Exit).await;
Ok(false)
}
Line::Event(event) => {
self.report(event).await;
Ok(true)
}
Line::Text(_) | Line::BlockEnd { .. } => Ok(true),
}
}
async fn report(&self, event: Event) {
let _ = self.events.send(event).await;
}
async fn read_block(&mut self, number: u64) -> Result<BlockResult, Error> {
let mut output = Vec::new();
let mut accumulated = 0usize;
loop {
match read_line(&mut self.stdout, &mut self.line, self.limits.max_line_bytes).await? {
Some(Line::BlockEnd {
number: end,
succeeded,
}) if end == number => {
return Ok(BlockResult {
number,
succeeded,
output,
});
}
Some(Line::Text(text)) => {
accumulated = accumulated.saturating_add(text.as_bytes().len());
if accumulated > self.limits.max_block_bytes {
return Err(Error::control_mode_frame_too_large(
"block",
self.limits.max_block_bytes,
));
}
output.push(text);
}
Some(Line::Event(event)) => self.report(event).await,
Some(Line::BlockStart(_) | Line::BlockEnd { .. }) => {}
None => return Err(Error::control_mode_closed()),
}
}
}
}
async fn read_line(
stdout: &mut BufReader<ChildStdout>,
pending: &mut Vec<u8>,
limit: usize,
) -> Result<Option<Line>, Error> {
let read = stdout
.read_until(b'\n', pending)
.await
.map_err(Error::control_mode)?;
if read == 0 && pending.is_empty() {
return Ok(None);
}
if pending.len() > limit {
pending.clear();
return Err(Error::control_mode_frame_too_large("line", limit));
}
let line = Line::parse(pending.strip_suffix(b"\n").unwrap_or(pending));
pending.clear();
Ok(Some(line))
}
async fn write_line(stdin: &mut ChildStdin, line: &str) -> Result<(), Error> {
stdin
.write_all(line.as_bytes())
.await
.map_err(Error::control_mode)?;
stdin.write_all(b"\n").await.map_err(Error::control_mode)?;
stdin.flush().await.map_err(Error::control_mode)?;
Ok(())
}
#[derive(Clone, Debug, Eq, PartialEq)]
enum Line {
BlockStart(u64),
BlockEnd { number: u64, succeeded: bool },
Event(Event),
Text(TmuxText),
}
impl Line {
fn parse(line: &[u8]) -> Self {
let text = || Self::Text(TmuxText::from_bytes(line));
let Some(rest) = line.strip_prefix(b"%") else {
return text();
};
let (name, arguments) = split_once(rest, b' ');
let Ok(name) = std::str::from_utf8(name) else {
return text();
};
match name {
"begin" | "end" | "error" => {
let number = std::str::from_utf8(arguments).ok().and_then(|arguments| {
arguments
.split_whitespace()
.nth(1)
.and_then(|value| value.parse().ok())
});
match (name, number) {
("begin", Some(number)) => Self::BlockStart(number),
(_, Some(number)) => Self::BlockEnd {
number,
succeeded: name == "end",
},
(_, None) => text(),
}
}
"output" => {
let (pane, bytes) = split_once(arguments, b' ');
std::str::from_utf8(pane)
.ok()
.and_then(|pane| pane.parse().ok())
.map_or_else(text, |pane| {
Self::Event(Event::Output {
pane,
bytes: unescape_output(bytes),
})
})
}
"session-changed" => {
let (session, _) = split_once(arguments, b' ');
std::str::from_utf8(session)
.ok()
.and_then(|session| session.parse().ok())
.map_or_else(text, |session| {
Self::Event(Event::SessionChanged { session })
})
}
"exit" => Self::Event(Event::Exit),
_ => Self::Event(Event::Other {
name: name.to_owned(),
rest: TmuxText::from_bytes(arguments),
}),
}
}
}
fn split_once(bytes: &[u8], byte: u8) -> (&[u8], &[u8]) {
bytes
.iter()
.position(|found| *found == byte)
.map_or((bytes, [].as_slice()), |index| {
(&bytes[..index], &bytes[index + 1..])
})
}
fn unescape_output(source: &[u8]) -> Vec<u8> {
let mut bytes = Vec::with_capacity(source.len());
let mut index = 0;
while index < source.len() {
if source[index] != b'\\' {
bytes.push(source[index]);
index += 1;
continue;
}
match source.get(index + 1..index + 4) {
Some(digits) if digits.iter().all(|digit| (b'0'..=b'7').contains(digit)) => {
let value = digits
.iter()
.fold(0_u32, |value, digit| value * 8 + u32::from(digit - b'0'));
if let Ok(byte) = u8::try_from(value) {
bytes.push(byte);
index += 4;
continue;
}
bytes.push(source[index]);
index += 1;
}
_ => {
if source.get(index + 1) == Some(&b'\\') {
bytes.push(b'\\');
index += 2;
} else {
bytes.push(source[index]);
index += 1;
}
}
}
}
bytes
}
#[cfg(feature = "unstable-fuzzing")]
#[doc(hidden)]
pub fn __fuzz_parse_control_line(line: &[u8]) {
let _ = Line::parse(line);
}
#[cfg(test)]
mod tests {
use super::{Event, Line, unescape_output};
use crate::TmuxText;
#[test]
fn block_headers_correlate_by_the_number_tmux_assigns() {
assert_eq!(
Line::parse(b"%begin 1786582374 347 0"),
Line::BlockStart(347)
);
assert_eq!(
Line::parse(b"%end 1786582374 347 0"),
Line::BlockEnd {
number: 347,
succeeded: true,
},
);
assert_eq!(
Line::parse(b"%error 1786582374 353 1"),
Line::BlockEnd {
number: 353,
succeeded: false,
},
);
assert!(matches!(Line::parse(b"%begin bad"), Line::Text(_)));
}
#[test]
fn notifications_are_parsed_and_unknown_ones_are_kept() {
assert_eq!(
Line::parse(b"%session-changed $0 work"),
Line::Event(Event::SessionChanged {
session: "$0".parse().expect("a session id parses"),
}),
);
assert_eq!(Line::parse(b"%exit"), Line::Event(Event::Exit));
assert_eq!(
Line::parse(b"%window-renamed @2 build"),
Line::Event(Event::Other {
name: "window-renamed".to_owned(),
rest: TmuxText::from_bytes(*b"@2 build"),
}),
);
}
#[test]
fn a_line_is_bytes_because_tmux_does_not_promise_text() {
let line = Line::parse(b"%output %0 \xff\xc3(");
assert_eq!(
line,
Line::Event(Event::Output {
pane: "%0".parse().expect("a pane id parses"),
bytes: vec![0xff, 0xc3, b'('],
}),
);
assert_eq!(
Line::parse(b"%window-renamed @2 \xff"),
Line::Event(Event::Other {
name: "window-renamed".to_owned(),
rest: TmuxText::from_bytes(*b"@2 \xff"),
}),
);
}
#[test]
fn output_escaping_round_trips_the_bytes_tmux_sends() {
assert_eq!(unescape_output(b"plain"), b"plain");
assert_eq!(unescape_output(br"a\015b"), b"a\rb");
assert_eq!(unescape_output(br"\377"), vec![0xff]);
assert_eq!(unescape_output(br"a\\b"), b"a\\b");
assert_eq!(unescape_output(br"a\zb"), b"a\\zb");
}
}