Skip to content

Commit f624860

Browse files
authored
fix(task): forward signals to spawned sub-processes on unix (denoland#27141)
Closes denoland#18445
1 parent 8626ec7 commit f624860

File tree

15 files changed

+589
-287
lines changed

15 files changed

+589
-287
lines changed

Cargo.lock

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

cli/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ deno_path_util.workspace = true
8282
deno_resolver.workspace = true
8383
deno_runtime = { workspace = true, features = ["include_js_files_for_snapshotting"] }
8484
deno_semver.workspace = true
85-
deno_task_shell = "=0.18.1"
85+
deno_task_shell = "=0.20.1"
8686
deno_telemetry.workspace = true
8787
deno_terminal.workspace = true
8888
libsui = "0.5.0"

cli/lsp/tsc.rs

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1297,16 +1297,10 @@ impl TsServer {
12971297
{
12981298
// When an LSP request is cancelled by the client, the future this is being
12991299
// executed under and any local variables here will be dropped at the next
1300-
// await point. To pass on that cancellation to the TS thread, we make this
1301-
// wrapper which cancels the request's token on drop.
1302-
struct DroppableToken(CancellationToken);
1303-
impl Drop for DroppableToken {
1304-
fn drop(&mut self) {
1305-
self.0.cancel();
1306-
}
1307-
}
1300+
// await point. To pass on that cancellation to the TS thread, we use drop_guard
1301+
// which cancels the request's token on drop.
13081302
let token = token.child_token();
1309-
let droppable_token = DroppableToken(token.clone());
1303+
let droppable_token = token.clone().drop_guard();
13101304
let (tx, mut rx) = oneshot::channel::<Result<String, AnyError>>();
13111305
let change = self.pending_change.lock().take();
13121306

@@ -1320,7 +1314,7 @@ impl TsServer {
13201314
tokio::select! {
13211315
value = &mut rx => {
13221316
let value = value??;
1323-
drop(droppable_token);
1317+
droppable_token.disarm();
13241318
Ok(serde_json::from_str(&value)?)
13251319
}
13261320
_ = token.cancelled() => {

cli/npm/managed/resolvers/common/lifecycle_scripts.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use deno_npm::resolution::NpmResolutionSnapshot;
99
use deno_runtime::deno_io::FromRawIoHandle;
1010
use deno_semver::package::PackageNv;
1111
use deno_semver::Version;
12+
use deno_task_shell::KillSignal;
1213
use std::borrow::Cow;
1314
use std::collections::HashSet;
1415
use std::rc::Rc;
@@ -155,6 +156,29 @@ impl<'a> LifecycleScripts<'a> {
155156
packages: &[NpmResolutionPackage],
156157
root_node_modules_dir_path: &Path,
157158
progress_bar: &ProgressBar,
159+
) -> Result<(), AnyError> {
160+
let kill_signal = KillSignal::default();
161+
let _drop_signal = kill_signal.clone().drop_guard();
162+
// we don't run with signals forwarded because once signals
163+
// are setup then they're process wide.
164+
self
165+
.finish_with_cancellation(
166+
snapshot,
167+
packages,
168+
root_node_modules_dir_path,
169+
progress_bar,
170+
kill_signal,
171+
)
172+
.await
173+
}
174+
175+
async fn finish_with_cancellation(
176+
self,
177+
snapshot: &NpmResolutionSnapshot,
178+
packages: &[NpmResolutionPackage],
179+
root_node_modules_dir_path: &Path,
180+
progress_bar: &ProgressBar,
181+
kill_signal: KillSignal,
158182
) -> Result<(), AnyError> {
159183
self.warn_not_run_scripts()?;
160184
let get_package_path =
@@ -246,6 +270,7 @@ impl<'a> LifecycleScripts<'a> {
246270
stderr: TaskStdio::piped(),
247271
stdout: TaskStdio::piped(),
248272
}),
273+
kill_signal: kill_signal.clone(),
249274
},
250275
)
251276
.await?;

cli/task_runner.rs

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use deno_runtime::deno_node::NodeResolver;
1414
use deno_semver::package::PackageNv;
1515
use deno_task_shell::ExecutableCommand;
1616
use deno_task_shell::ExecuteResult;
17+
use deno_task_shell::KillSignal;
1718
use deno_task_shell::ShellCommand;
1819
use deno_task_shell::ShellCommandContext;
1920
use deno_task_shell::ShellPipeReader;
@@ -22,6 +23,7 @@ use lazy_regex::Lazy;
2223
use regex::Regex;
2324
use tokio::task::JoinHandle;
2425
use tokio::task::LocalSet;
26+
use tokio_util::sync::CancellationToken;
2527

2628
use crate::npm::CliNpmResolver;
2729
use crate::npm::InnerCliNpmResolverRef;
@@ -45,9 +47,11 @@ impl TaskStdio {
4547
pub fn stdout() -> Self {
4648
Self(None, ShellPipeWriter::stdout())
4749
}
50+
4851
pub fn stderr() -> Self {
4952
Self(None, ShellPipeWriter::stderr())
5053
}
54+
5155
pub fn piped() -> Self {
5256
let (r, w) = deno_task_shell::pipe();
5357
Self(Some(r), w)
@@ -62,8 +66,8 @@ pub struct TaskIo {
6266
impl Default for TaskIo {
6367
fn default() -> Self {
6468
Self {
65-
stderr: TaskStdio::stderr(),
6669
stdout: TaskStdio::stdout(),
70+
stderr: TaskStdio::stderr(),
6771
}
6872
}
6973
}
@@ -78,6 +82,7 @@ pub struct RunTaskOptions<'a> {
7882
pub custom_commands: HashMap<String, Rc<dyn ShellCommand>>,
7983
pub root_node_modules_dir: Option<&'a Path>,
8084
pub stdio: Option<TaskIo>,
85+
pub kill_signal: KillSignal,
8186
}
8287

8388
pub type TaskCustomCommands = HashMap<String, Rc<dyn ShellCommand>>;
@@ -96,8 +101,12 @@ pub async fn run_task(
96101
.with_context(|| format!("Error parsing script '{}'.", opts.task_name))?;
97102
let env_vars =
98103
prepare_env_vars(opts.env_vars, opts.init_cwd, opts.root_node_modules_dir);
99-
let state =
100-
deno_task_shell::ShellState::new(env_vars, opts.cwd, opts.custom_commands);
104+
let state = deno_task_shell::ShellState::new(
105+
env_vars,
106+
opts.cwd,
107+
opts.custom_commands,
108+
opts.kill_signal,
109+
);
101110
let stdio = opts.stdio.unwrap_or_default();
102111
let (
103112
TaskStdio(stdout_read, stdout_write),
@@ -537,6 +546,80 @@ fn resolve_managed_npm_commands(
537546
Ok(result)
538547
}
539548

549+
/// Runs a deno task future forwarding any signals received
550+
/// to the process.
551+
///
552+
/// Signal listeners and ctrl+c listening will be setup.
553+
pub async fn run_future_forwarding_signals<TOutput>(
554+
kill_signal: KillSignal,
555+
future: impl std::future::Future<Output = TOutput>,
556+
) -> TOutput {
557+
fn spawn_future_with_cancellation(
558+
future: impl std::future::Future<Output = ()> + 'static,
559+
token: CancellationToken,
560+
) {
561+
deno_core::unsync::spawn(async move {
562+
tokio::select! {
563+
_ = future => {}
564+
_ = token.cancelled() => {}
565+
}
566+
});
567+
}
568+
569+
let token = CancellationToken::new();
570+
let _token_drop_guard = token.clone().drop_guard();
571+
let _drop_guard = kill_signal.clone().drop_guard();
572+
573+
spawn_future_with_cancellation(
574+
listen_ctrl_c(kill_signal.clone()),
575+
token.clone(),
576+
);
577+
#[cfg(unix)]
578+
spawn_future_with_cancellation(
579+
listen_and_forward_all_signals(kill_signal),
580+
token,
581+
);
582+
583+
future.await
584+
}
585+
586+
async fn listen_ctrl_c(kill_signal: KillSignal) {
587+
while let Ok(()) = tokio::signal::ctrl_c().await {
588+
kill_signal.send(deno_task_shell::SignalKind::SIGINT)
589+
}
590+
}
591+
592+
#[cfg(unix)]
593+
async fn listen_and_forward_all_signals(kill_signal: KillSignal) {
594+
use deno_core::futures::FutureExt;
595+
use deno_runtime::signal::SIGNAL_NUMS;
596+
597+
// listen and forward every signal we support
598+
let mut futures = Vec::with_capacity(SIGNAL_NUMS.len());
599+
for signo in SIGNAL_NUMS.iter().copied() {
600+
if signo == libc::SIGKILL || signo == libc::SIGSTOP {
601+
continue; // skip, can't listen to these
602+
}
603+
604+
let kill_signal = kill_signal.clone();
605+
futures.push(
606+
async move {
607+
let Ok(mut stream) = tokio::signal::unix::signal(
608+
tokio::signal::unix::SignalKind::from_raw(signo),
609+
) else {
610+
return;
611+
};
612+
let signal_kind: deno_task_shell::SignalKind = signo.into();
613+
while let Some(()) = stream.recv().await {
614+
kill_signal.send(signal_kind);
615+
}
616+
}
617+
.boxed_local(),
618+
)
619+
}
620+
futures::future::join_all(futures).await;
621+
}
622+
540623
#[cfg(test)]
541624
mod test {
542625

0 commit comments

Comments
 (0)