@@ -3,36 +3,93 @@ use tokio::process::Child;
3
3
use tracing:: debug;
4
4
5
5
/// Wait for the child process to complete, handling signals and error codes.
6
+ ///
7
+ /// Note that this registers handles to ignore some signals in the parent process. This is safe as
8
+ /// long as the command is the last thing that runs in this process; otherwise, we'd need to restore
9
+ /// the default signal handlers after the command completes.
6
10
pub ( crate ) async fn run_to_completion ( mut handle : Child ) -> anyhow:: Result < ExitStatus > {
7
- // Ignore signals in the parent process, deferring them to the child. This is safe as long as
8
- // the command is the last thing that runs in this process; otherwise, we'd need to restore the
9
- // signal handlers after the command completes.
10
- let _handler = tokio:: spawn ( async { while tokio:: signal:: ctrl_c ( ) . await . is_ok ( ) { } } ) ;
11
-
12
- // Exit based on the result of the command.
11
+ // On Unix, shells will send SIGINT to the active process group when a user presses `Ctrl-C`. In
12
+ // general, this means that uv should ignore SIGINT, allowing the child process to cleanly exit
13
+ // instead. If uv forwarded the SIGINT immediately, the child process would receive _two_ SIGINT
14
+ // signals which has semantic meaning for some programs, i.e., slow exit on the first signal and
15
+ // fast exit on the second. The exception to this is if a child process changes its process
16
+ // group, in which case the shell will _not_ send SIGINT to the child process and uv must take
17
+ // ownership of forwarding the signal.
18
+ //
19
+ // Note this assumes an interactive shell. If a signal is sent directly to the uv parent process
20
+ // (e.g., `kill -2 <pid>`), the process group is not involved and a signal is not sent to the
21
+ // child by default. In this context, uv must forward the signal to the child. We work around
22
+ // this by forwarding SIGINT if it is received more than once. We could attempt to infer if the
23
+ // parent is a shell using TTY detection(?), but there hasn't been sufficient motivation to
24
+ // explore alternatives yet.
25
+ //
26
+ // Use of SIGTERM is also a bit complicated. If a shell receives a SIGTERM, it just waits for
27
+ // its children to exit — multiple SIGTERMs do not have any effect and the signals are not
28
+ // forwarded to the children. Consequently, the description for SIGINT above does not apply to
29
+ // SIGTERM in shells. It is _possible_ to have a parent process that sends a SIGTERM to the
30
+ // process group; for example, `tini` supports this via a `-g` option. In this case, it's
31
+ // possible that uv will improperly send a second SIGTERM to the child process. However,
32
+ // this seems preferable to not forwarding it in the first place.
13
33
#[ cfg( unix) ]
14
34
let status = {
35
+ use nix:: sys:: signal;
36
+ use nix:: unistd:: { getpgid, Pid } ;
15
37
use tokio:: select;
16
- use tokio:: signal:: unix:: { signal, SignalKind } ;
38
+ use tokio:: signal:: unix:: { signal as handle_signal, SignalKind } ;
39
+
40
+ // Get the parent and child process group ids
41
+ let child_pid = handle
42
+ . id ( )
43
+ . and_then ( |id| id. try_into ( ) . ok ( ) )
44
+ . map ( Pid :: from_raw) ;
45
+ let parent_pgid = getpgid ( None ) ?;
46
+
47
+ let mut sigterm_handle = handle_signal ( SignalKind :: terminate ( ) ) ?;
48
+ let mut sigint_handle = handle_signal ( SignalKind :: interrupt ( ) ) ?;
49
+ let mut sigint_count = 0 ;
17
50
18
- let mut term_signal = signal ( SignalKind :: terminate ( ) ) ?;
19
51
loop {
20
52
select ! {
21
53
result = handle. wait( ) => {
22
54
break result;
23
55
} ,
56
+ _ = sigint_handle. recv( ) => {
57
+ // See above for commentary on handling of SIGINT.
24
58
25
- // `SIGTERM`
26
- _ = term_signal. recv( ) => {
27
- let _ = terminate_process( & mut handle) ;
59
+ // Check if the child pgid has changed
60
+ let child_pgid = getpgid( child_pid) ?;
61
+
62
+ // Increment the number of interrupts seen
63
+ sigint_count += 1 ;
64
+
65
+ // If the pgid _differs_ from the parent, the child will not receive a SIGINT
66
+ // and we should forward it. If we've received multiple SIGINTs, forward it
67
+ // regardless.
68
+ if child_pgid == parent_pgid && sigint_count < 2 {
69
+ continue ;
70
+ }
71
+
72
+ let _ = send_signal( & handle, child_pid, signal:: Signal :: SIGINT ) ;
73
+ } ,
74
+ _ = sigterm_handle. recv( ) => {
75
+ // We unconditionally forward SIGTERM to the child process; unlike SIGINT, this
76
+ // isn't usually handled by the shell and in cases like
77
+ let _ = send_signal( & handle, child_pid, signal:: Signal :: SIGTERM ) ;
28
78
}
29
79
} ;
30
80
}
31
81
} ?;
32
82
83
+ // On Windows, we just ignore the console CTRL_C_EVENT and assume it will always be sent to the
84
+ // child by the console. There's not a clear programmatic way to forward the signal anyway.
33
85
#[ cfg( not( unix) ) ]
34
- let status = handle. wait ( ) . await ?;
86
+ let status = {
87
+ let _ctrl_c_handler =
88
+ tokio:: spawn ( async { while tokio:: signal:: ctrl_c ( ) . await . is_ok ( ) { } } ) ;
89
+ handle. wait ( ) . await ?
90
+ } ?;
35
91
92
+ // Exit based on the result of the command.
36
93
if let Some ( code) = status. code ( ) {
37
94
debug ! ( "Command exited with code: {code}" ) ;
38
95
if let Ok ( code) = u8:: try_from ( code) {
@@ -60,12 +117,29 @@ pub(crate) async fn run_to_completion(mut handle: Child) -> anyhow::Result<ExitS
60
117
}
61
118
}
62
119
120
+ /// Send a signal to a child process on Unix.
121
+ ///
122
+ /// Includes a safety check that the process has not exited.
63
123
#[ cfg( unix) ]
64
- fn terminate_process ( child : & mut Child ) -> anyhow:: Result < ( ) > {
65
- use anyhow:: Context ;
66
- use nix:: sys:: signal:: { self , Signal } ;
67
- use nix:: unistd:: Pid ;
124
+ fn send_signal (
125
+ child : & Child ,
126
+ child_pid : Option < nix:: unistd:: Pid > ,
127
+ signal : nix:: sys:: signal:: Signal ,
128
+ ) -> anyhow:: Result < ( ) > {
129
+ use nix:: sys:: signal;
130
+
131
+ // If the child has already exited, we can't send it signals
132
+ let Some ( child_pid) = child_pid else {
133
+ anyhow:: bail!( "Child process has already exited" ) ;
134
+ } ;
135
+
136
+ // The child can exit and a different process can take its PID; this may be
137
+ // overly defensive but seems better than sending a signal to the wrong process.
138
+ if child. id ( ) . is_none ( ) {
139
+ anyhow:: bail!( "Child process has already exited" ) ;
140
+ }
141
+
142
+ signal:: kill ( child_pid, signal) ?;
68
143
69
- let pid = child. id ( ) . context ( "Failed to get child process ID" ) ?;
70
- signal:: kill ( Pid :: from_raw ( pid. try_into ( ) ?) , Signal :: SIGTERM ) . context ( "Failed to send SIGTERM" )
144
+ Ok ( ( ) )
71
145
}
0 commit comments