@@ -3,36 +3,93 @@ use tokio::process::Child;
33use tracing:: debug;
44
55/// 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.
610pub ( 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.
1333 #[ cfg( unix) ]
1434 let status = {
35+ use nix:: sys:: signal;
36+ use nix:: unistd:: { getpgid, Pid } ;
1537 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 ;
1750
18- let mut term_signal = signal ( SignalKind :: terminate ( ) ) ?;
1951 loop {
2052 select ! {
2153 result = handle. wait( ) => {
2254 break result;
2355 } ,
56+ _ = sigint_handle. recv( ) => {
57+ // See above for commentary on handling of SIGINT.
58+
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+ }
2471
25- // `SIGTERM`
26- _ = term_signal. recv( ) => {
27- let _ = terminate_process( & mut handle) ;
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 ) ;
2878 }
2979 } ;
3080 }
3181 } ?;
3282
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.
3385 #[ 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+ } ;
3591
92+ // Exit based on the result of the command.
3693 if let Some ( code) = status. code ( ) {
3794 debug ! ( "Command exited with code: {code}" ) ;
3895 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
60117 }
61118}
62119
120+ /// Send a signal to a child process on Unix.
121+ ///
122+ /// Includes a safety check that the process has not exited.
63123#[ 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) ?;
68143
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 ( ( ) )
71145}
0 commit comments