@@ -3,13 +3,10 @@ use std::{
33 convert:: identity,
44 ffi:: OsString ,
55 string:: String ,
6- sync:: {
7- Arc ,
8- atomic:: { AtomicBool , Ordering } ,
9- } ,
106} ;
117
128use clap:: Parser ;
9+ use deno_task_shell:: KillSignal ;
1310use dialoguer:: theme:: ColorfulTheme ;
1411use fancy_display:: FancyDisplay ;
1512use itertools:: Itertools ;
@@ -18,6 +15,7 @@ use pixi_config::{ConfigCli, ConfigCliActivation};
1815use pixi_manifest:: { FeaturesExt , TaskName } ;
1916use rattler_conda_types:: Platform ;
2017use thiserror:: Error ;
18+ use tokio_util:: sync:: CancellationToken ;
2119use tracing:: Level ;
2220
2321use super :: cli_config:: LockFileUpdateConfig ;
@@ -134,16 +132,12 @@ pub async fn execute(args: Args) -> miette::Result<()> {
134132 } )
135133 . await ?;
136134
137- let ctrlc_should_exit_process = Arc :: new ( AtomicBool :: new ( true ) ) ;
138- let ctrlc_should_exit_process_clone = Arc :: clone ( & ctrlc_should_exit_process) ;
139-
140- ctrlc:: set_handler ( move || {
141- reset_cursor ( ) ;
142- if ctrlc_should_exit_process_clone. load ( Ordering :: Relaxed ) {
143- exit_process_on_sigint ( ) ;
135+ // Spawn a task that listens for ctrl+c and resets the cursor.
136+ tokio:: spawn ( async {
137+ if tokio:: signal:: ctrl_c ( ) . await . is_ok ( ) {
138+ reset_cursor ( ) ;
144139 }
145- } )
146- . into_diagnostic ( ) ?;
140+ } ) ;
147141
148142 // Construct a task graph from the input arguments
149143 let search_environment = SearchEnvironments :: from_opt_env (
@@ -173,6 +167,10 @@ pub async fn execute(args: Args) -> miette::Result<()> {
173167 // task.
174168 let mut task_idx = 0 ;
175169 let mut task_envs = HashMap :: new ( ) ;
170+ let signal = KillSignal :: default ( ) ;
171+ // make sure that child processes are killed when pixi stops
172+ let _drop_guard = signal. clone ( ) . drop_guard ( ) ;
173+
176174 for task_id in task_graph. topological_order ( ) {
177175 let executable_task = ExecutableTask :: from_task_graph ( & task_graph, task_id) ;
178176
@@ -279,8 +277,6 @@ pub async fn execute(args: Args) -> miette::Result<()> {
279277 }
280278 } ;
281279
282- ctrlc_should_exit_process. store ( false , Ordering :: Relaxed ) ;
283-
284280 let task_env = task_env
285281 . iter ( )
286282 . map ( |( k, v) | ( OsString :: from ( k) , OsString :: from ( v) ) )
@@ -289,7 +285,7 @@ pub async fn execute(args: Args) -> miette::Result<()> {
289285 // Execute the task itself within the command environment. If one of the tasks
290286 // failed with a non-zero exit code, we exit this parent process with
291287 // the same code.
292- match execute_task ( & executable_task, & task_env) . await {
288+ match execute_task ( & executable_task, & task_env, signal . clone ( ) ) . await {
293289 Ok ( _) => {
294290 task_idx += 1 ;
295291 }
@@ -302,9 +298,6 @@ pub async fn execute(args: Args) -> miette::Result<()> {
302298 Err ( err) => return Err ( err. into ( ) ) ,
303299 }
304300
305- // Handle CTRL-C ourselves again
306- ctrlc_should_exit_process. store ( true , Ordering :: Relaxed ) ;
307-
308301 // Update the task cache with the new hash
309302 executable_task
310303 . save_cache ( lock_file. as_lock_file ( ) , task_cache)
@@ -377,21 +370,22 @@ enum TaskExecutionError {
377370async fn execute_task (
378371 task : & ExecutableTask < ' _ > ,
379372 command_env : & HashMap < OsString , OsString > ,
373+ kill_signal : KillSignal ,
380374) -> Result < ( ) , TaskExecutionError > {
381375 let Some ( script) = task. as_deno_script ( ) ? else {
382376 return Ok ( ( ) ) ;
383377 } ;
384378 let cwd = task. working_directory ( ) ?;
385-
386- let status_code = deno_task_shell:: execute (
379+ let execute_future = deno_task_shell:: execute (
387380 script,
388381 command_env. clone ( ) ,
389382 cwd,
390383 Default :: default ( ) ,
391- Default :: default ( ) ,
392- )
393- . await ;
384+ kill_signal. clone ( ) ,
385+ ) ;
394386
387+ // Execute the process and forward signals.
388+ let status_code = run_future_forwarding_signals ( kill_signal, execute_future) . await ;
395389 if status_code != 0 {
396390 return Err ( TaskExecutionError :: NonZeroExitCode ( status_code) ) ;
397391 }
@@ -443,13 +437,89 @@ fn reset_cursor() {
443437 let _ = term. show_cursor ( ) ;
444438}
445439
446- /// Exit the process with the appropriate exit code for a SIGINT.
447- fn exit_process_on_sigint ( ) {
448- // https://learn.microsoft.com/en-us/cpp/c-runtime-library/signal-constants
449- #[ cfg( target_os = "windows" ) ]
450- std:: process:: exit ( 3 ) ;
440+ // /// Exit the process with the appropriate exit code for a SIGINT.
441+ // fn exit_process_on_sigint() {
442+ // // https://learn.microsoft.com/en-us/cpp/c-runtime-library/signal-constants
443+ // #[cfg(target_os = "windows")]
444+ // std::process::exit(3);
445+ //
446+ // // POSIX compliant OSs: 128 + SIGINT (2)
447+ // #[cfg(not(target_os = "windows"))]
448+ // std::process::exit(130);
449+ // }
450+
451+ /// Runs a task future forwarding any signals received to the process.
452+ ///
453+ /// Signal listeners and ctrl+c listening will be setup.
454+ pub async fn run_future_forwarding_signals < TOutput > (
455+ kill_signal : KillSignal ,
456+ future : impl std:: future:: Future < Output = TOutput > ,
457+ ) -> TOutput {
458+ fn spawn_future_with_cancellation (
459+ future : impl std:: future:: Future < Output = ( ) > + ' static ,
460+ token : CancellationToken ,
461+ ) {
462+ tokio:: task:: spawn_local ( async move {
463+ tokio:: select! {
464+ _ = future => { }
465+ _ = token. cancelled( ) => { }
466+ }
467+ } ) ;
468+ }
469+
470+ let token = CancellationToken :: new ( ) ;
471+ let _token_drop_guard = token. clone ( ) . drop_guard ( ) ;
472+ let local_set = tokio:: task:: LocalSet :: new ( ) ;
473+
474+ local_set
475+ . run_until ( async move {
476+ spawn_future_with_cancellation ( listen_ctrl_c ( kill_signal. clone ( ) ) , token. clone ( ) ) ;
477+ #[ cfg( unix) ]
478+ spawn_future_with_cancellation ( listen_and_forward_all_signals ( kill_signal) , token) ;
479+
480+ future. await
481+ } )
482+ . await
483+ }
484+
485+ async fn listen_ctrl_c ( kill_signal : KillSignal ) {
486+ while let Ok ( ( ) ) = tokio:: signal:: ctrl_c ( ) . await {
487+ // On windows, ctrl+c is sent to the process group, so the signal would
488+ // have already been sent to the child process. We still want to listen
489+ // for ctrl+c here to keep the process alive when receiving it, but no
490+ // need to forward the signal because it's already been sent.
491+ if !cfg ! ( windows) {
492+ kill_signal. send ( deno_task_shell:: SignalKind :: SIGINT )
493+ }
494+ }
495+ }
496+
497+ #[ cfg( unix) ]
498+ async fn listen_and_forward_all_signals ( kill_signal : KillSignal ) {
499+ use futures:: FutureExt ;
451500
452- // POSIX compliant OSs: 128 + SIGINT (2)
453- #[ cfg( not( target_os = "windows" ) ) ]
454- std:: process:: exit ( 130 ) ;
501+ use crate :: signals:: SIGNALS ;
502+
503+ // listen and forward every signal we support
504+ let mut futures = Vec :: with_capacity ( SIGNALS . len ( ) ) ;
505+ for signo in SIGNALS . iter ( ) . copied ( ) {
506+ if signo == libc:: SIGKILL || signo == libc:: SIGSTOP {
507+ continue ; // skip, can't listen to these
508+ }
509+
510+ let kill_signal = kill_signal. clone ( ) ;
511+ futures. push (
512+ async move {
513+ let Ok ( mut stream) = tokio:: signal:: unix:: signal ( signo. into ( ) ) else {
514+ return ;
515+ } ;
516+ let signal_kind = signo. into ( ) ;
517+ while let Some ( ( ) ) = stream. recv ( ) . await {
518+ kill_signal. send ( signal_kind) ;
519+ }
520+ }
521+ . boxed_local ( ) ,
522+ )
523+ }
524+ futures:: future:: join_all ( futures) . await ;
455525}
0 commit comments