1
+ use std:: future:: pending;
2
+ use std:: ops:: { Deref , DerefMut } ;
3
+ use std:: time:: Duration ;
1
4
use std:: { ffi:: OsStr , process:: ExitStatus } ;
2
5
3
6
use anyhow:: { Result , anyhow, bail} ;
4
7
use clap:: Parser ;
8
+ use futures:: FutureExt ;
9
+ use rustix:: process:: { Pid , Signal , kill_process_group} ;
5
10
use tokio:: select;
11
+ use tokio:: signal:: unix:: { SignalKind , signal} ;
12
+ use tokio:: time:: sleep;
6
13
use tokio:: {
7
14
process:: { Child , Command } ,
8
15
task:: JoinSet ,
@@ -18,6 +25,10 @@ struct Args {
18
25
#[ clap( long, short) ]
19
26
with : Vec < String > ,
20
27
28
+ /// Optional timeout in seconds.
29
+ #[ clap( long, short) ]
30
+ timeout : Option < u64 > ,
31
+
21
32
/// Main command to execute.
22
33
main : Vec < String > ,
23
34
}
@@ -26,8 +37,16 @@ struct Args {
26
37
async fn main ( ) -> Result < ( ) > {
27
38
let args = Args :: parse ( ) ;
28
39
40
+ let mut term = signal ( SignalKind :: terminate ( ) ) ?;
41
+ let mut intr = signal ( SignalKind :: interrupt ( ) ) ?;
42
+
29
43
for exe in & args. exec {
30
- let status = command ( exe. split_whitespace ( ) ) ?. status ( ) . await ?;
44
+ let mut pg = ProcessGroup :: spawn ( exe. split_whitespace ( ) ) ?;
45
+ let status = select ! {
46
+ s = pg. wait( ) => s?,
47
+ _ = term. recv( ) => return Ok ( ( ) ) ,
48
+ _ = intr. recv( ) => return Ok ( ( ) ) ,
49
+ } ;
31
50
if !status. success ( ) {
32
51
bail ! ( "{exe:?} failed with {:?}" , status. code( ) ) ;
33
52
}
@@ -36,13 +55,19 @@ async fn main() -> Result<()> {
36
55
let mut helpers = JoinSet :: < Result < ExitStatus > > :: new ( ) ;
37
56
for w in args. with {
38
57
helpers. spawn ( async move {
39
- let mut c = spawn_command ( w. split_whitespace ( ) ) ?;
40
- let status = c . wait ( ) . await ?;
58
+ let mut pg = ProcessGroup :: spawn ( w. split_whitespace ( ) ) ?;
59
+ let status = pg . wait ( ) . await ?;
41
60
Ok ( status)
42
61
} ) ;
43
62
}
44
63
45
- let mut main = spawn_command ( args. main ) ?;
64
+ let mut main = ProcessGroup :: spawn ( args. main ) ?;
65
+
66
+ let timeout = if let Some ( d) = args. timeout {
67
+ sleep ( Duration :: from_secs ( d) ) . boxed ( )
68
+ } else {
69
+ pending ( ) . boxed ( )
70
+ } ;
46
71
47
72
select ! {
48
73
status = main. wait( ) => {
@@ -55,32 +80,55 @@ async fn main() -> Result<()> {
55
80
let status = status??;
56
81
bail!( "helper command exited before main with status {:?}" , status)
57
82
}
83
+ _ = term. recv( ) => { }
84
+ _ = intr. recv( ) => { }
85
+ _ = timeout => eprintln!( "timeout" )
58
86
}
59
87
60
88
Ok ( ( ) )
61
89
}
62
90
63
- fn command < I , S > ( it : I ) -> Result < Command >
64
- where
65
- I : IntoIterator < Item = S > ,
66
- S : AsRef < OsStr > ,
67
- {
68
- let mut args = it. into_iter ( ) ;
69
- let exe = args
70
- . next ( )
71
- . ok_or_else ( || anyhow ! ( "invalid command-line args" ) ) ?;
72
- let mut cmd = Command :: new ( exe) ;
73
- for a in args {
74
- cmd. arg ( a) ;
91
+ /// Every command is spawned into its own, newly created process group.
92
+ struct ProcessGroup ( Child , Pid ) ;
93
+
94
+ impl ProcessGroup {
95
+ fn spawn < I , S > ( it : I ) -> Result < Self >
96
+ where
97
+ I : IntoIterator < Item = S > ,
98
+ S : AsRef < OsStr > ,
99
+ {
100
+ let mut args = it. into_iter ( ) ;
101
+ let exe = args
102
+ . next ( )
103
+ . ok_or_else ( || anyhow ! ( "invalid command-line args" ) ) ?;
104
+ let mut cmd = Command :: new ( exe) ;
105
+ for a in args {
106
+ cmd. arg ( a) ;
107
+ }
108
+ cmd. process_group ( 0 ) ;
109
+ let child = cmd. spawn ( ) ?;
110
+ let id = child. id ( ) . ok_or_else ( || anyhow ! ( "child already exited" ) ) ?;
111
+ let pid = Pid :: from_raw ( id. try_into ( ) ?) . ok_or_else ( || anyhow ! ( "invalid pid" ) ) ?;
112
+ Ok ( Self ( child, pid) )
113
+ }
114
+ }
115
+
116
+ impl Drop for ProcessGroup {
117
+ fn drop ( & mut self ) {
118
+ let _ = kill_process_group ( self . 1 , Signal :: KILL ) ;
75
119
}
76
- cmd. kill_on_drop ( true ) ;
77
- Ok ( cmd)
78
120
}
79
121
80
- fn spawn_command < I , S > ( it : I ) -> Result < Child >
81
- where
82
- I : IntoIterator < Item = S > ,
83
- S : AsRef < OsStr > ,
84
- {
85
- command ( it) ?. spawn ( ) . map_err ( |e| e. into ( ) )
122
+ impl Deref for ProcessGroup {
123
+ type Target = Child ;
124
+
125
+ fn deref ( & self ) -> & Self :: Target {
126
+ & self . 0
127
+ }
128
+ }
129
+
130
+ impl DerefMut for ProcessGroup {
131
+ fn deref_mut ( & mut self ) -> & mut Self :: Target {
132
+ & mut self . 0
133
+ }
86
134
}
0 commit comments