1
1
use std:: fs;
2
- use std:: io:: { self , Read , Write } ;
2
+ use std:: future:: Future ;
3
+ use std:: io;
3
4
use std:: path:: Path ;
4
- use std:: process:: Command ;
5
5
use std:: process:: Stdio ;
6
+ use std:: time:: Duration ;
6
7
7
8
use anyhow:: bail;
8
9
use anyhow:: { Context , Result } ;
9
10
use log:: info;
10
11
use sha2:: { Digest , Sha256 } ;
12
+ use tokio:: io:: { AsyncReadExt , AsyncWriteExt } ;
13
+ use tokio:: process:: Command ;
14
+ use tokio:: time:: { self , Instant , Timeout } ;
11
15
12
16
pub fn ensure_dir < P : AsRef < Path > > ( dir : P ) -> Result < ( ) > {
13
17
match fs:: read_dir ( dir. as_ref ( ) ) {
@@ -81,38 +85,48 @@ impl Cmd {
81
85
Cmd { cmd, input }
82
86
}
83
87
84
- pub fn execute ( & mut self ) -> Result < Option < Vec < u8 > > > {
88
+ pub async fn execute ( & mut self ) -> Result < Option < Vec < u8 > > > {
85
89
let mut child = match self . cmd . spawn ( ) {
86
90
Ok ( child) => child,
87
91
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" ) ;
96
93
}
94
+ Err ( e) => return Err ( e) . context ( "cannot launch command" ) ,
97
95
} ;
98
96
99
97
if let Some ( input) = & self . input {
100
98
let handle = child. stdin . as_mut ( ) . unwrap ( ) ;
101
99
handle
102
100
. write_all ( input)
103
- . with_context ( || format ! ( "write input to command `{}`" , self . get_name( ) ) ) ?;
101
+ . await
102
+ . context ( "write input to command" ) ?;
104
103
drop ( child. stdin . take ( ) ) ;
105
104
}
106
105
107
106
let mut stdout = child. stdout . take ( ) ;
108
107
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
+ } ;
110
123
let output = match stdout. as_mut ( ) {
111
124
Some ( stdout) => {
112
125
let mut out = Vec :: new ( ) ;
113
126
stdout
114
127
. read_to_end ( & mut out)
115
- . with_context ( || format ! ( "read stdout from command `{}`" , self . get_name( ) ) ) ?;
128
+ . await
129
+ . context ( "read stdout from command" ) ?;
116
130
Some ( out)
117
131
}
118
132
None => None ,
@@ -128,11 +142,6 @@ impl Cmd {
128
142
None => bail ! ( "command exited with unknown code" ) ,
129
143
}
130
144
}
131
-
132
- #[ inline]
133
- fn get_name ( & self ) -> & str {
134
- self . cmd . get_program ( ) . to_str ( ) . unwrap_or ( "<unknown>" )
135
- }
136
145
}
137
146
138
147
pub fn get_digest ( data : & [ u8 ] ) -> String {
@@ -147,3 +156,12 @@ pub fn shellexpand(s: impl AsRef<str>) -> Result<String> {
147
156
. with_context ( || format ! ( "expand env for '{}'" , s. as_ref( ) ) )
148
157
. map ( |s| s. into_owned ( ) )
149
158
}
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