1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
use std::ffi::OsString;
use std::time::Duration;
use super::{ChannelWait, Server};
use crate::internal::listing;
use crate::{Command, Error};
impl Server {
/// Signal a `wait-for` channel, releasing anything waiting on it.
///
/// [`Server::wait_for_channel`] is the other half, and either order works:
/// tmux keeps a signal nobody is waiting on, so a command that finishes
/// before its watcher starts does not lose the race. The latch releases
/// one wait, and one signal releases every waiter already there.
/// Signalling the same channel twice before a wait clears the latch, so
/// this operation is not idempotent.
///
/// Signalling is not scoped to a pane or a session. The channel is a name
/// on the server, so anything that can reach the socket can signal it,
/// which is what makes it useful for telling an orchestrator that a
/// command inside a pane is done.
///
/// # Errors
///
/// Returns an error when tmux refuses the channel name.
pub async fn signal_channel(&self, channel: &str) -> Result<(), Error> {
listing::mutate(
&self.core,
"wait-for",
Command::new("wait-for")
.arg("-S")
.arg(OsString::from(channel)),
)
.await
}
/// Lock a `wait-for` channel, blocking later lock attempts on it.
///
/// # Errors
///
/// Returns an error when tmux refuses the channel name.
pub async fn lock_channel(&self, channel: &str) -> Result<(), Error> {
listing::mutate(
&self.core,
"wait-for",
Command::new("wait-for")
.arg("-L")
.arg(OsString::from(channel)),
)
.await
}
/// Unlock a `wait-for` channel.
///
/// # Errors
///
/// Returns an error when tmux refuses the channel name.
pub async fn unlock_channel(&self, channel: &str) -> Result<(), Error> {
listing::mutate(
&self.core,
"wait-for",
Command::new("wait-for")
.arg("-U")
.arg(OsString::from(channel)),
)
.await
}
/// Wait for a `wait-for` channel to be signalled.
///
/// The blocking half of [`Server::signal_channel`]. Nothing polls: tmux
/// releases the wait when the channel is signalled, so a caller costs one
/// idle client rather than a loop.
///
/// This waits for something to *say* it happened. It does not watch a
/// pane, so what signals the channel is the caller's to arrange -- a
/// command ending with `tmux wait-for -S <channel>` is the usual shape.
///
/// The channel latches. Signalling one nothing is waiting on is kept, and
/// the next wait returns at once; the latch is one-shot, so a second wait
/// blocks again. One signal releases every waiter present at the time. So
/// signalling before the wait starts is safe, which is what makes this
/// usable for a command that may finish first.
///
/// That holds across the supported range. `cmd-wait-for.c` is identical
/// between 3.5a and 3.7c, and the only changes since 3.2a are an argument
/// table gaining a field, an accessor replacing a direct index, and a
/// local being renamed -- none of them near the flag the latch is kept in.
/// Measured directly on 3.5a and 3.7c.
///
/// `within` is capped at [`Server::default_timeout`], because a dispatch
/// is bounded and this is one: ask for longer by building the server with
/// a longer timeout.
///
/// # Errors
///
/// Returns an error when tmux refuses the channel name or cannot be
/// reached. Running out of time is [`ChannelWait::TimedOut`] rather than
/// an error, so "nothing signalled it" stays distinct from "the command
/// did not get through" -- the caller retries only one of those.
///
/// # Examples
///
/// ```
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// # let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
/// # runtime.block_on(async {
/// use libtmux::ChannelWait;
/// use std::time::Duration;
///
/// # let guard = libtmux::test::TestServer::builder().start().await?;
/// # let server = guard.server();
/// // Signalling first is safe: the channel keeps it.
/// server.signal_channel("ready").await?;
/// let outcome = server.wait_for_channel("ready", Duration::from_secs(5)).await?;
/// assert_eq!(outcome, ChannelWait::Signalled);
///
/// // The latch is spent, so a second wait runs out of time instead.
/// let again = server.wait_for_channel("ready", Duration::from_millis(200)).await?;
/// assert_eq!(again, ChannelWait::TimedOut);
/// # guard.shutdown().await?;
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// # })?;
/// # Ok(())
/// # }
/// ```
pub async fn wait_for_channel(
&self,
channel: &str,
within: Duration,
) -> Result<ChannelWait, Error> {
let budget = within.min(self.default_timeout());
let waited = tokio::time::timeout(
budget,
listing::mutate(
&self.core,
"wait-for",
Command::new("wait-for").arg(OsString::from(channel)),
),
)
.await;
match waited {
Ok(Ok(())) => Ok(ChannelWait::Signalled),
// The dispatch reaching its own bound first is the same event, so
// it is reported the same way rather than as two outcomes a
// caller would have to unify.
Ok(Err(error)) if error.kind() == crate::ErrorKind::Timeout => {
Ok(ChannelWait::TimedOut)
}
Ok(Err(error)) => Err(error),
// Dropping the dispatch kills the tmux client that was waiting.
// The server is unaffected and the channel stays usable, measured
// by killing a waiter outright and signalling it afterwards.
Err(_elapsed) => Ok(ChannelWait::TimedOut),
}
}
}