Skip to content

Commit 2f25603

Browse files
wolfvbaszalmstra
andauthored
fix: forward CTRL+C signal to deno_task_shell (#4243)
Co-authored-by: Bas Zalmstra <[email protected]>
1 parent 3484c4f commit 2f25603

File tree

9 files changed

+1183
-204
lines changed

9 files changed

+1183
-204
lines changed

Cargo.lock

Lines changed: 218 additions & 171 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ crossbeam-channel = "0.5.14"
3333
csv = "1.3.1"
3434
ctrlc = "3.4.5"
3535
dashmap = "6.1.0"
36-
deno_task_shell = "0.24.0"
36+
deno_task_shell = "0.26.0"
3737
derive_more = "2.0.1"
3838
dialoguer = "0.11.0"
3939
digest = "0.10"

src/cli/run.rs

Lines changed: 102 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -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

128
use clap::Parser;
9+
use deno_task_shell::KillSignal;
1310
use dialoguer::theme::ColorfulTheme;
1411
use fancy_display::FancyDisplay;
1512
use itertools::Itertools;
@@ -18,6 +15,7 @@ use pixi_config::{ConfigCli, ConfigCliActivation};
1815
use pixi_manifest::{FeaturesExt, TaskName};
1916
use rattler_conda_types::Platform;
2017
use thiserror::Error;
18+
use tokio_util::sync::CancellationToken;
2119
use tracing::Level;
2220

2321
use 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 {
377370
async 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
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ pub mod workspace;
1616
mod reporters;
1717

1818
mod rlimit;
19+
mod signals;
1920
mod uv_reporter;
2021
pub mod variants;
2122

0 commit comments

Comments
 (0)