use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicU8, Ordering};
use std::task::{Context, Poll};
use std::time::Duration;
use futures_core::Stream;
use tokio::sync::{mpsc, oneshot, watch};
use tokio::time::Instant;
use crate::limits::ControlLimits;
use crate::version::since::CONTROL_PANE_OFF;
use crate::{Command, Error, IdParseError, PaneId, Server, SessionId, TmuxText, WindowId};
mod actor;
mod protocol;
#[cfg(test)]
use actor::{HELD_WHILE_AWAITING, ReplySlot, ReplySlots, admit_request};
use actor::{Request, deadline_elapsed};
#[cfg(any(test, feature = "unstable-fuzzing"))]
use protocol::Line;
#[cfg(test)]
use protocol::unescape_output;
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum Event {
Output {
pane: PaneId,
bytes: Vec<u8>,
},
ExtendedOutput {
pane: PaneId,
age: Duration,
bytes: Vec<u8>,
},
Paused {
pane: PaneId,
},
Continued {
pane: PaneId,
},
SessionChanged {
session: SessionId,
},
SessionRenamed {
session: SessionId,
name: TmuxText,
},
SessionWindowChanged {
session: SessionId,
window: WindowId,
},
SessionsChanged,
WindowAdded {
window: WindowId,
},
WindowClosed {
window: WindowId,
},
WindowRenamed {
window: WindowId,
name: TmuxText,
},
WindowPaneChanged {
window: WindowId,
pane: PaneId,
},
UnlinkedWindowAdded {
window: WindowId,
},
UnlinkedWindowClosed {
window: WindowId,
},
UnlinkedWindowRenamed {
window: WindowId,
name: TmuxText,
},
LayoutChanged {
window: WindowId,
layout: TmuxText,
visible_layout: TmuxText,
flags: TmuxText,
},
PaneModeChanged {
pane: PaneId,
},
ClientDetached {
client: TmuxText,
},
ClientSessionChanged {
client: TmuxText,
session: SessionId,
name: TmuxText,
},
PasteBufferChanged {
name: TmuxText,
},
PasteBufferDeleted {
name: TmuxText,
},
SubscriptionChanged {
name: TmuxText,
session: SessionId,
window: Option<WindowId>,
index: Option<u32>,
pane: Option<PaneId>,
value: TmuxText,
},
ConfigError {
message: TmuxText,
},
Message {
message: TmuxText,
},
Exit {
reason: Option<TmuxText>,
},
Other {
name: String,
rest: TmuxText,
},
}
impl Event {
#[must_use]
pub const fn invalidates_listings(&self) -> bool {
!matches!(
self,
Self::Output { .. }
| Self::ExtendedOutput { .. }
| Self::Paused { .. }
| Self::Continued { .. }
| Self::SubscriptionChanged { .. }
| Self::ConfigError { .. }
| Self::Message { .. }
)
}
#[must_use]
pub const fn may_have_added_a_pane(&self) -> bool {
matches!(
self,
Self::LayoutChanged { .. }
| Self::WindowAdded { .. }
| Self::UnlinkedWindowAdded { .. }
| Self::SessionsChanged
| Self::SessionChanged { .. }
| Self::Other { .. }
)
}
#[must_use]
pub const fn pane(&self) -> Option<&PaneId> {
match self {
Self::Output { pane, .. }
| Self::ExtendedOutput { pane, .. }
| Self::Paused { pane }
| Self::Continued { pane }
| Self::PaneModeChanged { pane }
| Self::WindowPaneChanged { pane, .. } => Some(pane),
Self::SubscriptionChanged { pane, .. } => pane.as_ref(),
_ => None,
}
}
#[must_use]
pub const fn window(&self) -> Option<&WindowId> {
match self {
Self::WindowAdded { window }
| Self::WindowClosed { window }
| Self::WindowRenamed { window, .. }
| Self::WindowPaneChanged { window, .. }
| Self::UnlinkedWindowAdded { window }
| Self::UnlinkedWindowClosed { window }
| Self::UnlinkedWindowRenamed { window, .. }
| Self::LayoutChanged { window, .. }
| Self::SessionWindowChanged { window, .. } => Some(window),
Self::SubscriptionChanged { window, .. } => window.as_ref(),
_ => None,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BlockResult {
number: u64,
succeeded: bool,
output: Vec<TmuxText>,
sensitive_input: bool,
}
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
}
#[must_use]
pub fn refusal_for(&self, operation: &'static str) -> Option<Error> {
if self.succeeded {
return None;
}
let mut bytes = Vec::new();
for line in &self.output {
bytes.extend_from_slice(line.as_bytes());
bytes.push(b'\n');
}
let classified = Error::refused(
operation,
None,
String::from_utf8_lossy(&bytes).into_owned(),
None,
);
Some(
if self.sensitive_input && !matches!(&classified, Error::ServerGone { .. }) {
Error::refused_withheld(operation, None)
} else {
classified
},
)
}
fn require_success(self, operation: &'static str) -> Result<Self, Error> {
match self.refusal_for(operation) {
Some(error) => Err(error),
None => Ok(self),
}
}
}
#[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 pane_off_is_safe = server
.capabilities()
.await
.is_ok_and(|capabilities| capabilities.tmux_version().meets(&CONTROL_PANE_OFF));
let timeout = server.default_timeout();
let actor::OpenedConnection {
commands,
events,
stop,
connection,
} = actor::open(server.spawn_control(session).await?, limits, timeout).await?;
Ok(Self {
sender: ControlSender {
commands,
timeout,
pane_off_is_safe,
},
events: ControlEvents {
events,
stop,
connection,
},
})
}
#[must_use]
pub fn split(self) -> (ControlSender, ControlEvents) {
(self.sender, self.events)
}
#[must_use]
pub fn reply_timeout(self, timeout: Duration) -> Self {
Self {
sender: self.sender.reply_timeout(timeout),
..self
}
}
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, Eq, PartialEq)]
#[non_exhaustive]
pub enum Subscription {
Session,
Window(WindowId),
AllWindows,
Pane(PaneId),
AllPanes,
}
impl std::fmt::Display for Subscription {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Session => Ok(()),
Self::Window(window) => write!(formatter, "{window}"),
Self::AllWindows => formatter.write_str("@*"),
Self::Pane(pane) => write!(formatter, "{pane}"),
Self::AllPanes => formatter.write_str("%*"),
}
}
}
fn check_subscription_name(name: &str) -> Result<(), Error> {
if name.is_empty() || name.contains(':') {
return Err(Error::control_mode_invalid_subscription());
}
Ok(())
}
#[derive(Clone, Debug)]
pub struct ControlSender {
commands: mpsc::Sender<Request>,
timeout: Duration,
pane_off_is_safe: bool,
}
impl ControlSender {
#[must_use]
pub fn reply_timeout(self, timeout: Duration) -> Self {
Self { timeout, ..self }
}
pub async fn send(&self, command: Command) -> Result<BlockResult, Error> {
self.send_ordered(command, None).await
}
async fn send_ordered(
&self,
command: Command,
boundary: Option<Boundary>,
) -> Result<BlockResult, Error> {
let deadline = Instant::now().checked_add(self.timeout);
let sensitive_input = command.summary().sensitive_argument_count() > 0;
let line = command
.control_mode_line()
.ok_or_else(Error::control_mode_unrepresentable)?;
let (result, mut answer) = oneshot::channel();
let (commit, mut commitment) = oneshot::channel();
let finish = |answer: Result<Result<BlockResult, Error>, oneshot::error::RecvError>| {
let mut block = answer.map_err(|_| Error::control_mode_closed())??;
block.sensitive_input = sensitive_input;
Ok(block)
};
if deadline.is_some_and(|deadline| deadline <= Instant::now()) {
return Err(Error::control_mode_dispatch_timeout());
}
let permit = tokio::select! {
biased;
permit = self.commands.reserve() => {
permit.map_err(|_| Error::control_mode_closed())?
}
() = deadline_elapsed(deadline) => {
return Err(Error::control_mode_dispatch_timeout());
}
};
permit.send(Request {
line,
deadline,
result,
commit,
boundary,
});
tokio::select! {
biased;
answer = &mut answer => return finish(answer),
() = deadline_elapsed(deadline) => {
commitment.close();
match commitment.try_recv() {
Ok(()) => {}
Err(oneshot::error::TryRecvError::Closed | oneshot::error::TryRecvError::Empty) => {
return Err(Error::control_mode_dispatch_timeout());
}
}
}
committed = &mut commitment => {
if committed.is_err() {
return finish(answer.await);
}
}
}
finish(answer.await)
}
pub async fn mute_pane(&self, pane: &PaneId) -> Result<(), Error> {
self.set_pane_stream(
pane,
if self.pane_off_is_safe {
"off"
} else {
"pause"
},
)
.await
}
pub async fn unmute_pane(&self, pane: &PaneId) -> Result<(), Error> {
self.set_pane_stream(
pane,
if self.pane_off_is_safe {
"on"
} else {
"continue"
},
)
.await
}
pub async fn resume_pane(&self, pane: &PaneId) -> Result<(), Error> {
self.set_pane_stream(pane, "continue").await
}
pub async fn subscribe(
&self,
name: &str,
watching: &Subscription,
format: &str,
) -> Result<(), Error> {
check_subscription_name(name)?;
self.send(
Command::new("refresh-client")
.arg("-B")
.arg(format!("{name}:{watching}:{format}")),
)
.await?
.require_success("refresh-client")
.map(|_| ())
}
pub async fn unsubscribe(&self, name: &str) -> Result<(), Error> {
check_subscription_name(name)?;
self.send(Command::new("refresh-client").arg("-B").arg(name))
.await?
.require_success("refresh-client")
.map(|_| ())
}
pub async fn pause_after(&self, behind: Duration) -> Result<(), Error> {
self.send(
Command::new("refresh-client")
.arg("-f")
.arg(format!("pause-after={}", behind.as_secs())),
)
.await?
.require_success("refresh-client")
.map(|_| ())
}
pub async fn watch_only(&self, panes: &[PaneId]) -> Result<(), Error> {
let listed = self
.send(
Command::new("list-panes")
.arg("-a")
.arg("-F")
.arg("#{pane_id}"),
)
.await?
.require_success("list-panes")?;
let mut effect_seen = false;
for line in listed.output() {
let found = decode_watched_pane_id(line).map_err(|error| {
if effect_seen {
error.after_effect("watch-only")
} else {
error
}
})?;
if !panes.contains(&found) {
self.mute_pane(&found).await.map_err(|error| {
if effect_seen {
error.after_effect("watch-only")
} else {
error
}
})?;
effect_seen = true;
}
}
Ok(())
}
async fn set_pane_stream(&self, pane: &PaneId, state: &str) -> Result<(), Error> {
self.send(
Command::new("refresh-client")
.arg("-A")
.arg(format!("{pane}:{state}")),
)
.await?
.require_success("refresh-client")
.map(|_| ())
}
#[must_use]
pub fn is_closed(&self) -> bool {
self.commands.is_closed()
}
}
fn decode_watched_pane_id(line: &TmuxText) -> Result<PaneId, Error> {
let invalid = |detail| Error::UnreadableFormatValue {
format: "#{pane_id}",
detail,
};
let id = line.as_str().map_err(|_| invalid(IdParseError::new('%')))?;
id.parse().map_err(invalid)
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct Boundary(u64);
#[derive(Debug)]
enum Delivery {
Event(Event),
Boundary(Boundary),
}
#[derive(Debug)]
pub struct ControlEvents {
events: mpsc::Receiver<Delivery>,
stop: watch::Sender<()>,
connection: tokio::task::JoinHandle<Result<(), Error>>,
}
impl ControlEvents {
async fn next_delivery(&mut self) -> Option<Delivery> {
self.events.recv().await
}
pub async fn next_event(&mut self) -> Option<Event> {
loop {
match self.next_delivery().await? {
Delivery::Event(event) => return Some(event),
Delivery::Boundary(_) => {}
}
}
}
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>> {
loop {
match std::task::ready!(self.events.poll_recv(context)) {
Some(Delivery::Event(event)) => return Poll::Ready(Some(event)),
Some(Delivery::Boundary(_)) => {}
None => return Poll::Ready(None),
}
}
}
}
const NARROW_IDLE: u8 = 0;
const NARROW_RUNNING: u8 = 1;
const NARROW_DIRTY: u8 = 2;
#[derive(Debug)]
pub struct PaneOutput {
pane: PaneId,
events: ControlEvents,
boundary: u64,
closed: bool,
sender: ControlSender,
narrowing: Arc<AtomicU8>,
}
impl PaneOutput {
pub(crate) fn new(pane: PaneId, events: ControlEvents, sender: ControlSender) -> Self {
Self {
pane,
events,
boundary: 0,
closed: false,
sender,
narrowing: Arc::new(AtomicU8::new(NARROW_IDLE)),
}
}
fn narrow(&self) {
let transition =
self.narrowing
.fetch_update(Ordering::AcqRel, Ordering::Acquire, |state| match state {
NARROW_IDLE => Some(NARROW_RUNNING),
NARROW_RUNNING => Some(NARROW_DIRTY),
_ => None,
});
if !matches!(transition, Ok(NARROW_IDLE)) {
return;
}
let sender = self.sender.clone();
let pane = self.pane.clone();
let narrowing = Arc::clone(&self.narrowing);
tokio::spawn(async move {
loop {
let _ = sender.watch_only(std::slice::from_ref(&pane)).await;
match narrowing.compare_exchange(
NARROW_RUNNING,
NARROW_IDLE,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => break,
Err(state) => {
debug_assert_eq!(state, NARROW_DIRTY);
narrowing.store(NARROW_RUNNING, Ordering::Release);
}
}
}
});
}
#[must_use]
pub const fn pane(&self) -> &PaneId {
&self.pane
}
pub async fn snapshot(
&mut self,
mut on_output: impl FnMut(&[u8]),
) -> Result<Vec<TmuxText>, Error> {
self.boundary = self.boundary.wrapping_add(1);
let boundary = Boundary(self.boundary);
let command = Command::new("capture-pane")
.arg("-p")
.arg("-t")
.arg(self.pane.to_string());
let sender = self.sender.clone();
let reply = sender.send_ordered(command, Some(boundary));
tokio::pin!(reply);
let mut answer = None;
loop {
let mut reached = false;
tokio::select! {
biased;
outcome = reply.as_mut(), if answer.is_none() => {
match outcome {
Ok(block) => answer = Some(block),
Err(error) => return Err(error),
}
}
delivery = self.events.next_delivery() => {
match delivery {
Some(Delivery::Event(
Event::Output { pane, bytes }
| Event::ExtendedOutput { pane, bytes, .. },
)) if pane == self.pane => on_output(&bytes),
Some(Delivery::Event(Event::Exit { .. })) => self.closed = true,
Some(Delivery::Event(event)) => {
if event.may_have_added_a_pane() {
self.narrow();
}
}
Some(Delivery::Boundary(found)) if found == boundary => reached = true,
Some(Delivery::Boundary(_)) => {}
None => {
self.closed = true;
if answer.is_none() {
reply.as_mut().await?;
}
return Err(Error::control_mode_closed());
}
}
}
}
if !reached {
continue;
}
let block = match answer.take() {
Some(block) => block,
None => reply.as_mut().await?,
}
.require_success("capture-pane")?;
return Ok(block.output);
}
}
pub async fn next_chunk(&mut self) -> Option<Vec<u8>> {
if self.closed {
return None;
}
loop {
let delivery = self.events.next_delivery().await;
match delivery {
Some(Delivery::Event(
Event::Output { pane, bytes } | Event::ExtendedOutput { pane, bytes, .. },
)) if pane == self.pane => {
return Some(bytes);
}
Some(Delivery::Event(Event::Exit { .. })) | None => {
self.closed = true;
return None;
}
Some(Delivery::Event(event)) if event.may_have_added_a_pane() => self.narrow(),
_ => {}
}
}
}
pub async fn shutdown(self) -> Result<(), Error> {
drop(self.sender);
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>>> {
if self.closed {
return Poll::Ready(None);
}
loop {
match std::task::ready!(self.events.events.poll_recv(context)) {
Some(Delivery::Event(
Event::Output { pane, bytes } | Event::ExtendedOutput { pane, bytes, .. },
)) if pane == self.pane => {
return Poll::Ready(Some(bytes));
}
Some(Delivery::Event(Event::Exit { .. })) | None => {
self.closed = true;
return Poll::Ready(None);
}
Some(Delivery::Event(event)) => {
if event.may_have_added_a_pane() {
self.narrow();
}
}
Some(Delivery::Boundary(_)) => {}
}
}
}
}
#[cfg(feature = "unstable-fuzzing")]
#[doc(hidden)]
pub fn __fuzz_parse_control_line(line: &[u8]) {
let _ = Line::parse(line);
}
#[cfg(test)]
mod tests;
#[cfg(test)]
#[path = "control/lifecycle_tests.rs"]
mod lifecycle_tests;