1
1
use std:: future:: Future ;
2
- use std:: io:: Write ;
2
+ use std:: io:: { Read , Seek , Write } ;
3
3
use std:: os:: fd:: BorrowedFd ;
4
4
use std:: process:: Command ;
5
5
use std:: time:: Duration ;
@@ -15,17 +15,79 @@ pub(crate) trait CommandRunExt {
15
15
fn run ( & mut self ) -> Result < ( ) > ;
16
16
}
17
17
18
+ /// Helpers intended for [`std::process::ExitStatus`].
19
+ pub ( crate ) trait ExitStatusExt {
20
+ /// If the exit status signals it was not successful, return an error.
21
+ /// Note that we intentionally *don't* include the command string
22
+ /// in the output; we leave it to the caller to add that if they want,
23
+ /// as it may be verbose.
24
+ fn check_status ( & mut self , stderr : std:: fs:: File ) -> Result < ( ) > ;
25
+ }
26
+
27
+ /// Parse the last chunk (e.g. 1024 bytes) from the provided file,
28
+ /// ensure it's UTF-8, and return that value. This function is infallible;
29
+ /// if the file cannot be read for some reason, a copy of a static string
30
+ /// is returned.
31
+ fn last_utf8_content_from_file ( mut f : std:: fs:: File ) -> String {
32
+ // u16 since we truncate to just the trailing bytes here
33
+ // to avoid pathological error messages
34
+ const MAX_STDERR_BYTES : u16 = 1024 ;
35
+ let size = f
36
+ . metadata ( )
37
+ . map_err ( |e| {
38
+ tracing:: warn!( "failed to fstat: {e}" ) ;
39
+ } )
40
+ . map ( |m| m. len ( ) . try_into ( ) . unwrap_or ( u16:: MAX ) )
41
+ . unwrap_or ( 0 ) ;
42
+ let size = size. min ( MAX_STDERR_BYTES ) ;
43
+ let seek_offset = -( size as i32 ) ;
44
+ let mut stderr_buf = Vec :: with_capacity ( size. into ( ) ) ;
45
+ // We should never fail to seek()+read() really, but let's be conservative
46
+ let r = match f
47
+ . seek ( std:: io:: SeekFrom :: End ( seek_offset. into ( ) ) )
48
+ . and_then ( |_| f. read_to_end ( & mut stderr_buf) )
49
+ {
50
+ Ok ( _) => String :: from_utf8_lossy ( & stderr_buf) ,
51
+ Err ( e) => {
52
+ tracing:: warn!( "failed seek+read: {e}" ) ;
53
+ "<failed to read stderr>" . into ( )
54
+ }
55
+ } ;
56
+ ( & * r) . to_owned ( )
57
+ }
58
+
59
+ impl ExitStatusExt for std:: process:: ExitStatus {
60
+ fn check_status ( & mut self , stderr : std:: fs:: File ) -> Result < ( ) > {
61
+ let stderr_buf = last_utf8_content_from_file ( stderr) ;
62
+ if self . success ( ) {
63
+ return Ok ( ( ) ) ;
64
+ }
65
+ anyhow:: bail!( format!( "Subprocess failed: {self:?}\n {stderr_buf}" ) )
66
+ }
67
+ }
68
+
18
69
impl CommandRunExt for Command {
19
70
/// Synchronously execute the child, and return an error if the child exited unsuccessfully.
20
71
fn run ( & mut self ) -> Result < ( ) > {
21
- let st = self . status ( ) ?;
22
- if !st. success ( ) {
23
- // Note that we intentionally *don't* include the command string
24
- // in the output; we leave it to the caller to add that if they want,
25
- // as it may be verbose.
26
- anyhow:: bail!( format!( "Subprocess failed: {st:?}" ) )
27
- }
28
- Ok ( ( ) )
72
+ let stderr = tempfile:: tempfile ( ) ?;
73
+ self . stderr ( stderr. try_clone ( ) ?) ;
74
+ self . status ( ) ?. check_status ( stderr)
75
+ }
76
+ }
77
+
78
+ /// Helpers intended for [`tokio::process::Command`].
79
+ #[ allow( dead_code) ]
80
+ pub ( crate ) trait AsyncCommandRunExt {
81
+ async fn run ( & mut self ) -> Result < ( ) > ;
82
+ }
83
+
84
+ impl AsyncCommandRunExt for tokio:: process:: Command {
85
+ /// Asynchronously execute the child, and return an error if the child exited unsuccessfully.
86
+ ///
87
+ async fn run ( & mut self ) -> Result < ( ) > {
88
+ let stderr = tempfile:: tempfile ( ) ?;
89
+ self . stderr ( stderr. try_clone ( ) ?) ;
90
+ self . status ( ) . await ?. check_status ( stderr)
29
91
}
30
92
}
31
93
@@ -132,14 +194,14 @@ pub(crate) fn medium_visibility_warning(s: &str) {
132
194
/// with an automatic spinner to show that we're not blocked.
133
195
/// Note that generally the called function should not output
134
196
/// anything to stdout as this will interfere with the spinner.
135
- pub ( crate ) async fn async_task_with_spinner < F , T > ( msg : & ' static str , f : F ) -> T
197
+ pub ( crate ) async fn async_task_with_spinner < F , T > ( msg : & str , f : F ) -> T
136
198
where
137
199
F : Future < Output = T > ,
138
200
{
139
201
let pb = indicatif:: ProgressBar :: new_spinner ( ) ;
140
202
let style = indicatif:: ProgressStyle :: default_bar ( ) ;
141
203
pb. set_style ( style. template ( "{spinner} {msg}" ) . unwrap ( ) ) ;
142
- pb. set_message ( msg) ;
204
+ pb. set_message ( msg. to_string ( ) ) ;
143
205
pb. enable_steady_tick ( Duration :: from_millis ( 150 ) ) ;
144
206
// We need to handle the case where we aren't connected to
145
207
// a tty, so indicatif would show nothing by default.
@@ -212,6 +274,43 @@ fn test_sigpolicy_from_opts() {
212
274
213
275
#[ test]
214
276
fn command_run_ext ( ) {
277
+ // The basics
215
278
Command :: new ( "true" ) . run ( ) . unwrap ( ) ;
216
279
assert ! ( Command :: new( "false" ) . run( ) . is_err( ) ) ;
280
+
281
+ // Verify we capture stderr
282
+ let e = Command :: new ( "/bin/sh" )
283
+ . args ( [ "-c" , "echo expected-this-oops-message 1>&2; exit 1" ] )
284
+ . run ( )
285
+ . err ( )
286
+ . unwrap ( ) ;
287
+ similar_asserts:: assert_eq!(
288
+ e. to_string( ) ,
289
+ "Subprocess failed: ExitStatus(unix_wait_status(256))\n expected-this-oops-message\n "
290
+ ) ;
291
+
292
+ // Ignoring invalid UTF-8
293
+ let e = Command :: new ( "/bin/sh" )
294
+ . args ( [
295
+ "-c" ,
296
+ r"echo -e 'expected\xf5\x80\x80\x80\x80-foo\xc0bar\xc0\xc0' 1>&2; exit 1" ,
297
+ ] )
298
+ . run ( )
299
+ . err ( )
300
+ . unwrap ( ) ;
301
+ similar_asserts:: assert_eq!(
302
+ e. to_string( ) ,
303
+ "Subprocess failed: ExitStatus(unix_wait_status(256))\n expected�����-foo�bar��\n "
304
+ ) ;
305
+ }
306
+
307
+ #[ tokio:: test]
308
+ async fn async_command_run_ext ( ) {
309
+ use tokio:: process:: Command as AsyncCommand ;
310
+ let mut success = AsyncCommand :: new ( "true" ) ;
311
+ let mut fail = AsyncCommand :: new ( "false" ) ;
312
+ // Run these in parallel just because we can
313
+ let ( success, fail) = tokio:: join!( success. run( ) , fail. run( ) , ) ;
314
+ success. unwrap ( ) ;
315
+ assert ! ( fail. is_err( ) ) ;
217
316
}
0 commit comments