Skip to content

Commit 379a02c

Browse files
committed
feaat: timeout support
closes #2
1 parent 6505d0d commit 379a02c

File tree

9 files changed

+125
-14
lines changed

9 files changed

+125
-14
lines changed

Cargo.lock

Lines changed: 8 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ exclude = ["/test", "/resources"]
1616
[dependencies]
1717
async-trait = "0.1.68"
1818
bytes = { version = "1.4.0", features = ["serde"] }
19+
paste = "1.0.15"
1920
rand = "0.8.5"
2021
serde = { version = "1.0.160", features = ["derive"] }
2122
serde_json = "1.0.96"

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ async fn main() -> Result<()> {
6767
res.num_players,
6868
res.max_players
6969
);
70-
70+
7171
Ok(())
7272
}
7373
```
@@ -82,7 +82,7 @@ use tokio;:io::Result;
8282
async fn main() -> Result<()> {
8383
let res = stat_full("localhost", 25565).await?;
8484
println!("Online players: {:#?}, res.players);
85-
85+
8686
Ok(())
8787
}
8888
```

src/errors.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,7 @@ impl From<QueryProtocolError> for io::Error {
113113
io::Error::new(ErrorKind::InvalidData, err)
114114
}
115115
}
116+
117+
pub(crate) fn timeout_err<T>() -> io::Result<T> {
118+
Err(io::Error::new(ErrorKind::TimedOut, "connection timed out"))
119+
}

src/lib.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,39 @@
99
#![allow(clippy::cast_sign_loss)]
1010
#![allow(clippy::cast_lossless)]
1111

12+
macro_rules! create_timeout {
13+
($name:ident, $ret:ty) => {
14+
::paste::paste! {
15+
#[doc = concat!("Similar to [`", stringify!($name), "`]")]
16+
/// but with an added argument for timeout.
17+
///
18+
/// Note that timeouts are not precise, and may vary on the order
19+
/// of milliseconds, because of the way the async event loop works.
20+
///
21+
/// # Arguments
22+
/// * `host` - A string slice that holds the hostname of the server to connect to.
23+
/// * `port` - The port to connect to on that server.
24+
///
25+
/// # Errors
26+
/// Returns `Err` on any condition that
27+
#[doc = concat!("[`", stringify!($name), "`]")]
28+
/// does, and also when the response is not fully recieved within `dur`.
29+
pub async fn [<$name _with_timeout>](
30+
host: &str,
31+
port: u16,
32+
dur: ::std::time::Duration,
33+
) -> ::std::io::Result<$ret> {
34+
use crate::errors::timeout_err;
35+
use ::tokio::time::timeout;
36+
37+
timeout(dur, $name(host, port))
38+
.await
39+
.unwrap_or(timeout_err::<$ret>())
40+
}
41+
}
42+
};
43+
}
44+
1245
pub mod errors;
1346
pub mod query;
1447
pub mod rcon;

src/query.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,9 @@ pub async fn stat_full(host: &str, port: u16) -> io::Result<FullStatResponse> {
283283
})
284284
}
285285

286+
create_timeout!(stat_basic, BasicStatResponse);
287+
create_timeout!(stat_full, FullStatResponse);
288+
286289
/// Perform a handshake request per <https://wiki.vg/Query#Handshake>
287290
///
288291
/// # Returns

src/rcon.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//! Enables remote command execution for minecraft servers.
2-
//! See the documentation for [`RconCLient`] for more information.
2+
//! See the documentation for [`RconClient`] for more information.
33
44
mod client;
55
mod packet;

src/rcon/client.rs

Lines changed: 71 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ use super::{
44
packet::{RconPacket, RconPacketType},
55
MAX_LEN_CLIENTBOUND,
66
};
7-
use crate::errors::RconProtocolError;
7+
use crate::errors::{timeout_err, RconProtocolError};
88
use bytes::{BufMut, BytesMut};
9+
use std::time::Duration;
910
use tokio::{
1011
io::{self, AsyncReadExt, AsyncWriteExt, Error},
1112
net::TcpStream,
13+
time::timeout,
1214
};
1315

1416
/// Struct that stores the connection and other state of the RCON protocol with the server.
@@ -34,12 +36,16 @@ use tokio::{
3436
#[derive(Debug)]
3537
pub struct RconClient {
3638
socket: TcpStream,
39+
timeout: Option<Duration>,
3740
}
3841

3942
impl RconClient {
4043
/// Construct an [`RconClient`] that connects to the given host and port.
4144
/// Note: to authenticate use the `authenticate` method, this method does not take a password.
4245
///
46+
/// Clients constructed this way will wait arbitrarily long (maybe forever!) to recieve
47+
/// a response from the server. To set a timeout, see [`with_timeout`] or [`set_timeout`].
48+
///
4349
/// # Arguments
4450
/// * `host` - A string slice that holds the hostname of the server to connect to.
4551
/// * `port` - The port to connect to.
@@ -49,7 +55,40 @@ impl RconClient {
4955
pub async fn new(host: &str, port: u16) -> io::Result<Self> {
5056
let connection = TcpStream::connect(format!("{host}:{port}")).await?;
5157

52-
Ok(Self { socket: connection })
58+
Ok(Self {
59+
socket: connection,
60+
timeout: None,
61+
})
62+
}
63+
64+
/// Construct an [`RconClient`] that connects to the given host and port, and a connection
65+
/// timeout.
66+
/// Note: to authenticate use the `authenticate` method, this method does not take a password.
67+
///
68+
/// Note that timeouts are not precise, and may vary on the order of milliseconds, because
69+
/// of the way the async event loop works.
70+
///
71+
/// # Arguments
72+
/// * `host` - A string slice that holds the hostname of the server to connect to.
73+
/// * `port` - The port to connect to.
74+
/// * `timeout` - A duration to wait for each response to arrive in.
75+
///
76+
/// # Errors
77+
/// Returns `Err` if there was a network error.
78+
pub async fn with_timeout(host: &str, port: u16, timeout: Duration) -> io::Result<Self> {
79+
let mut client = Self::new(host, port).await?;
80+
client.set_timeout(Some(timeout));
81+
82+
Ok(client)
83+
}
84+
85+
/// Change the timeout for future requests.
86+
///
87+
/// # Arguments
88+
/// * `timeout` - an option specifying the duration to wait for a response.
89+
/// if none, the client may wait forever.
90+
pub fn set_timeout(&mut self, timeout: Option<Duration>) {
91+
self.timeout = timeout;
5392
}
5493

5594
/// Disconnect from the server and close the RCON connection.
@@ -70,7 +109,36 @@ impl RconClient {
70109
/// # Errors
71110
/// Returns the raw `tokio::io::Error` if there was a network error.
72111
/// Returns an apprpriate [`RconProtocolError`] if the authentication failed for other reasons.
112+
/// Also returns an error if a timeout is set, and the response is not recieved in that timeframe.
73113
pub async fn authenticate(&mut self, password: &str) -> io::Result<()> {
114+
let to = self.timeout;
115+
let fut = self.authenticate_raw(password);
116+
117+
match to {
118+
None => fut.await,
119+
Some(d) => timeout(d, fut).await.unwrap_or(timeout_err()),
120+
}
121+
}
122+
123+
/// Run the given command on the server and return the result.
124+
///
125+
/// # Arguments
126+
/// * `command` - A string slice that holds the command to run. Must be ASCII and under 1446 bytes in length.
127+
///
128+
/// # Errors
129+
/// Returns an error if there was a network issue or an [`RconProtocolError`] for other failures.
130+
/// Also returns an error if a timeout was set and a response was not recieved in that timeframe.
131+
pub async fn run_command(&mut self, command: &str) -> io::Result<String> {
132+
let to = self.timeout;
133+
let fut = self.run_command_raw(command);
134+
135+
match to {
136+
None => fut.await,
137+
Some(d) => timeout(d, fut).await.unwrap_or(timeout_err()),
138+
}
139+
}
140+
141+
async fn authenticate_raw(&mut self, password: &str) -> io::Result<()> {
74142
let packet =
75143
RconPacket::new(1, RconPacketType::Login, password.to_string()).map_err(Error::from)?;
76144

@@ -91,14 +159,7 @@ impl RconClient {
91159
Ok(())
92160
}
93161

94-
/// Run the given command on the server and return the result.
95-
///
96-
/// # Arguments
97-
/// * `command` - A string slice that holds the command to run. Must be ASCII and under 1446 bytes in length.
98-
///
99-
/// # Errors
100-
/// Returns an error if there was a network issue or an [`RconProtocolError`] for other failures.
101-
pub async fn run_command(&mut self, command: &str) -> io::Result<String> {
162+
async fn run_command_raw(&mut self, command: &str) -> io::Result<String> {
102163
let packet = RconPacket::new(1, RconPacketType::RunCommand, command.to_string())
103164
.map_err(Error::from)?;
104165

src/status.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ pub async fn status(host: &str, port: u16) -> io::Result<StatusResponse> {
8080
.map_err(|_| MinecraftProtocolError::InvalidStatusResponse.into())
8181
}
8282

83+
create_timeout!(status, StatusResponse);
84+
8385
#[cfg(test)]
8486
mod tests {
8587
use super::status;

0 commit comments

Comments
 (0)