@@ -9,6 +9,7 @@ use tokio::{
9
9
use super :: { IntoTransport , Transport } ;
10
10
use crate :: service:: ServiceRole ;
11
11
12
+ const MAX_WAIT_ON_DROP_SECS : u64 = 3 ;
12
13
/// The parts of a child process.
13
14
type ChildProcessParts = (
14
15
Box < dyn TokioChildWrapper > ,
@@ -41,13 +42,35 @@ pub struct TokioChildProcess {
41
42
}
42
43
43
44
pub struct ChildWithCleanup {
44
- inner : Box < dyn TokioChildWrapper > ,
45
+ inner : Option < Box < dyn TokioChildWrapper > > ,
45
46
}
46
47
47
48
impl Drop for ChildWithCleanup {
48
49
fn drop ( & mut self ) {
49
- if let Err ( e) = self . inner . start_kill ( ) {
50
- tracing:: warn!( "Failed to kill child process: {e}" ) ;
50
+ // Close child more graceful
51
+ if let Some ( mut child) = self . inner . take ( ) {
52
+ // Spawn a background task to clean up
53
+ tokio:: spawn ( async move {
54
+ // Wait will drop the stdin
55
+ let wait_fut = Box :: into_pin ( child. wait ( ) ) ;
56
+ tokio:: select! {
57
+ _ = tokio:: time:: sleep( std:: time:: Duration :: from_secs( MAX_WAIT_ON_DROP_SECS ) ) => {
58
+ if let Err ( e) = Box :: into_pin( child. kill( ) ) . await {
59
+ tracing:: warn!( "Error killing child: {e}" ) ;
60
+ }
61
+ } ,
62
+ res = wait_fut => {
63
+ match res {
64
+ Ok ( status) => {
65
+ tracing:: info!( "Child exited gracefully {}" , status) ;
66
+ }
67
+ Err ( e) => {
68
+ tracing:: warn!( "Error waiting for child: {e}" ) ;
69
+ }
70
+ }
71
+ }
72
+ }
73
+ } ) ;
51
74
}
52
75
}
53
76
}
@@ -64,7 +87,7 @@ pin_project_lite::pin_project! {
64
87
impl TokioChildProcessOut {
65
88
/// Get the process ID of the child process.
66
89
pub fn id ( & self ) -> Option < u32 > {
67
- self . child . inner . id ( )
90
+ self . child . inner . as_ref ( ) ? . id ( )
68
91
}
69
92
}
70
93
@@ -92,7 +115,7 @@ impl TokioChildProcess {
92
115
93
116
/// Get the process ID of the child process.
94
117
pub fn id ( & self ) -> Option < u32 > {
95
- self . child . inner . id ( )
118
+ self . child . inner . as_ref ( ) ? . id ( )
96
119
}
97
120
98
121
/// Split this helper into a reader (stdout) and writer (stdin).
@@ -157,7 +180,7 @@ impl TokioChildProcessBuilder {
157
180
let ( child, stdout, stdin, stderr_opt) = child_process ( self . cmd . spawn ( ) ?) ?;
158
181
159
182
let proc = TokioChildProcess {
160
- child : ChildWithCleanup { inner : child } ,
183
+ child : ChildWithCleanup { inner : Some ( child) } ,
161
184
child_stdin : stdin,
162
185
child_stdout : stdout,
163
186
} ;
@@ -183,3 +206,45 @@ impl ConfigureCommandExt for tokio::process::Command {
183
206
self
184
207
}
185
208
}
209
+
210
+ #[ cfg( unix) ]
211
+ #[ cfg( test) ]
212
+ mod tests {
213
+ use tokio:: process:: Command ;
214
+
215
+ use super :: * ;
216
+
217
+ #[ tokio:: test]
218
+ async fn test_tokio_child_process_drop ( ) {
219
+ let r = TokioChildProcess :: new ( Command :: new ( "sleep" ) . configure ( |cmd| {
220
+ cmd. arg ( "30" ) ;
221
+ } ) ) ;
222
+ assert ! ( r. is_ok( ) ) ;
223
+ let child_process = r. unwrap ( ) ;
224
+ let id = child_process. id ( ) ;
225
+ assert ! ( id. is_some( ) ) ;
226
+ let id = id. unwrap ( ) ;
227
+ // Drop the child process
228
+ drop ( child_process) ;
229
+ // Wait a moment to allow the cleanup task to run
230
+ tokio:: time:: sleep ( std:: time:: Duration :: from_secs ( MAX_WAIT_ON_DROP_SECS + 1 ) ) . await ;
231
+ // Check if the process is still running
232
+ let status = Command :: new ( "ps" )
233
+ . arg ( "-p" )
234
+ . arg ( id. to_string ( ) )
235
+ . status ( )
236
+ . await ;
237
+ match status {
238
+ Ok ( status) => {
239
+ assert ! (
240
+ !status. success( ) ,
241
+ "Process with PID {} is still running" ,
242
+ id
243
+ ) ;
244
+ }
245
+ Err ( e) => {
246
+ panic ! ( "Failed to check process status: {}" , e) ;
247
+ }
248
+ }
249
+ }
250
+ }
0 commit comments