Skip to content

Commit 0064994

Browse files
authored
feat(cmd): Add execute timeout (#16)
Added execute timeout for command. This can prevent command hanging cause csync hanging.
1 parent c0a18cb commit 0064994

File tree

4 files changed

+46
-28
lines changed

4 files changed

+46
-28
lines changed

src/net/client/mod.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,17 @@ pub use watch::WatchClient;
77
use std::borrow::Cow;
88
use std::net::SocketAddr;
99
use std::sync::Arc;
10-
use std::time::Duration;
1110

1211
use anyhow::{bail, Context, Result};
1312
use tokio::io::{AsyncRead, AsyncWrite};
1413
use tokio::net::{lookup_host, TcpSocket, TcpStream};
1514
use tokio::sync::oneshot;
1615
use tokio::sync::Mutex;
17-
use tokio::time::{self, Instant};
1816

1917
use crate::net::auth::Auth;
2018
use crate::net::conn::Connection;
2119
use crate::net::frame::{self, DataFrame, Frame};
20+
use crate::utils;
2221

2322
struct Client<S: AsyncWrite + AsyncRead + Unpin + Send> {
2423
conn: Arc<Mutex<Connection<S>>>,
@@ -97,7 +96,7 @@ impl<S: AsyncWrite + AsyncRead + Unpin + Send + 'static> Client<S> {
9796
let _ = done_tx.send(result);
9897
});
9998

100-
match time::timeout_at(Instant::now() + Duration::from_secs(1), done_rx).await {
99+
match utils::with_timeout(done_rx).await {
101100
Ok(result) => result.unwrap(),
102101
Err(_) => bail!("send data timeout after 1s"),
103102
}

src/sync/read.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ impl Reader {
128128
}
129129
let result = Cmd::new(&self.cfg.cmd, None, true)
130130
.execute()
131+
.await
131132
.context("execute read command");
132133
if result.is_err() && self.cfg.allow_cmd_failure {
133134
Ok(None)

src/sync/write.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,13 @@ impl Writer {
7373

7474
let is_image = from_utf8(&frame.body).is_err();
7575
if is_image {
76-
if let Err(err) = self.handle_image(frame) {
76+
if let Err(err) = self.handle_image(frame).await {
7777
self.err_tx.send(err).await.unwrap();
7878
}
7979
continue;
8080
}
8181

82-
if let Err(err) = self.handle_text(frame) {
82+
if let Err(err) = self.handle_text(frame).await {
8383
self.err_tx.send(err).await.unwrap();
8484
}
8585
}
@@ -104,7 +104,7 @@ impl Writer {
104104
Ok(())
105105
}
106106

107-
fn handle_image(&mut self, frame: DataFrame) -> Result<()> {
107+
async fn handle_image(&mut self, frame: DataFrame) -> Result<()> {
108108
if self.cfg.download_image {
109109
let path = self.image_path.as_ref().unwrap();
110110
println!("<Download image to {}>", path.display());
@@ -116,19 +116,19 @@ impl Writer {
116116
if !self.cfg.image_cmd.is_empty() {
117117
println!("<Execute image command>");
118118
let mut cmd = Cmd::new(&self.cfg.image_cmd, Some(frame.body), false);
119-
cmd.execute().context("execute image command")?;
119+
cmd.execute().await.context("execute image command")?;
120120
return Ok(());
121121
}
122122

123123
println!("<Image data>");
124124
Ok(())
125125
}
126126

127-
fn handle_text(&mut self, frame: DataFrame) -> Result<()> {
127+
async fn handle_text(&mut self, frame: DataFrame) -> Result<()> {
128128
if !self.cfg.text_cmd.is_empty() {
129129
println!("<Execute text command>");
130130
let mut cmd = Cmd::new(&self.cfg.text_cmd, Some(frame.body), false);
131-
cmd.execute().context("execute text command")?;
131+
cmd.execute().await.context("execute text command")?;
132132
return Ok(());
133133
}
134134

src/utils.rs

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
use std::fs;
2-
use std::io::{self, Read, Write};
2+
use std::future::Future;
3+
use std::io;
34
use std::path::Path;
4-
use std::process::Command;
55
use std::process::Stdio;
6+
use std::time::Duration;
67

78
use anyhow::bail;
89
use anyhow::{Context, Result};
910
use log::info;
1011
use sha2::{Digest, Sha256};
12+
use tokio::io::{AsyncReadExt, AsyncWriteExt};
13+
use tokio::process::Command;
14+
use tokio::time::{self, Instant, Timeout};
1115

1216
pub fn ensure_dir<P: AsRef<Path>>(dir: P) -> Result<()> {
1317
match fs::read_dir(dir.as_ref()) {
@@ -81,38 +85,48 @@ impl Cmd {
8185
Cmd { cmd, input }
8286
}
8387

84-
pub fn execute(&mut self) -> Result<Option<Vec<u8>>> {
88+
pub async fn execute(&mut self) -> Result<Option<Vec<u8>>> {
8589
let mut child = match self.cmd.spawn() {
8690
Ok(child) => child,
8791
Err(e) if e.kind() == io::ErrorKind::NotFound => {
88-
bail!(
89-
"cannot find command `{}`, please make sure it is installed",
90-
self.get_name()
91-
);
92-
}
93-
Err(e) => {
94-
return Err(e)
95-
.with_context(|| format!("cannot launch command `{}`", self.get_name()))
92+
bail!("cannot find command, please make sure it is installed");
9693
}
94+
Err(e) => return Err(e).context("cannot launch command"),
9795
};
9896

9997
if let Some(input) = &self.input {
10098
let handle = child.stdin.as_mut().unwrap();
10199
handle
102100
.write_all(input)
103-
.with_context(|| format!("write input to command `{}`", self.get_name()))?;
101+
.await
102+
.context("write input to command")?;
104103
drop(child.stdin.take());
105104
}
106105

107106
let mut stdout = child.stdout.take();
108107

109-
let status = child.wait().context("wait command done")?;
108+
let status = match with_timeout(child.wait()).await {
109+
Ok(result) => result.context("wait command exit")?,
110+
Err(_) => {
111+
// The command hang, try to kill it to avoid leakage. The kill also has a
112+
// timeout.
113+
if with_timeout(child.kill()).await.is_err() {
114+
// Kill failed, the child process is completely blocked now and cannot
115+
// handle kill signal. We donot known how to handle this, report the
116+
// warning message. Let user to handle this.
117+
let id = child.id().unwrap_or(0);
118+
println!("WARN: Failed to kill child process {id} after timeout, process leakage may appear, please be attention");
119+
}
120+
bail!("execute command timeout after 1s");
121+
}
122+
};
110123
let output = match stdout.as_mut() {
111124
Some(stdout) => {
112125
let mut out = Vec::new();
113126
stdout
114127
.read_to_end(&mut out)
115-
.with_context(|| format!("read stdout from command `{}`", self.get_name()))?;
128+
.await
129+
.context("read stdout from command")?;
116130
Some(out)
117131
}
118132
None => None,
@@ -128,11 +142,6 @@ impl Cmd {
128142
None => bail!("command exited with unknown code"),
129143
}
130144
}
131-
132-
#[inline]
133-
fn get_name(&self) -> &str {
134-
self.cmd.get_program().to_str().unwrap_or("<unknown>")
135-
}
136145
}
137146

138147
pub fn get_digest(data: &[u8]) -> String {
@@ -147,3 +156,12 @@ pub fn shellexpand(s: impl AsRef<str>) -> Result<String> {
147156
.with_context(|| format!("expand env for '{}'", s.as_ref()))
148157
.map(|s| s.into_owned())
149158
}
159+
160+
/// Every long operations should have an 1s timeout.
161+
#[inline]
162+
pub fn with_timeout<F>(future: F) -> Timeout<F>
163+
where
164+
F: Future,
165+
{
166+
time::timeout_at(Instant::now() + Duration::from_secs(1), future)
167+
}

0 commit comments

Comments
 (0)