1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-01-03 04:48:52 -05:00

fix(task): forward signals to spawned sub-processes on unix (#27141)

Closes https://github.com/denoland/deno/issues/18445
This commit is contained in:
David Sherret 2024-11-29 17:36:43 -05:00 committed by GitHub
parent 8626ec7c25
commit f6248601f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 590 additions and 288 deletions

6
Cargo.lock generated
View file

@ -2136,19 +2136,19 @@ dependencies = [
[[package]]
name = "deno_task_shell"
version = "0.18.1"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f444918f7102c1a5a143e9d57809e499fb4d365070519bf2e8bdb16d586af2a"
checksum = "01e09966ce29f8d26b652a43355397e1df43b85759e7824196bf0ceaeaa9a2f4"
dependencies = [
"anyhow",
"futures",
"glob",
"monch",
"nix",
"os_pipe",
"path-dedot",
"thiserror",
"tokio",
"tokio-util",
]
[[package]]

View file

@ -82,7 +82,7 @@ deno_path_util.workspace = true
deno_resolver.workspace = true
deno_runtime = { workspace = true, features = ["include_js_files_for_snapshotting"] }
deno_semver.workspace = true
deno_task_shell = "=0.18.1"
deno_task_shell = "=0.20.1"
deno_telemetry.workspace = true
deno_terminal.workspace = true
libsui = "0.5.0"

View file

@ -1297,16 +1297,10 @@ impl TsServer {
{
// When an LSP request is cancelled by the client, the future this is being
// executed under and any local variables here will be dropped at the next
// await point. To pass on that cancellation to the TS thread, we make this
// wrapper which cancels the request's token on drop.
struct DroppableToken(CancellationToken);
impl Drop for DroppableToken {
fn drop(&mut self) {
self.0.cancel();
}
}
// await point. To pass on that cancellation to the TS thread, we use drop_guard
// which cancels the request's token on drop.
let token = token.child_token();
let droppable_token = DroppableToken(token.clone());
let droppable_token = token.clone().drop_guard();
let (tx, mut rx) = oneshot::channel::<Result<String, AnyError>>();
let change = self.pending_change.lock().take();
@ -1320,7 +1314,7 @@ impl TsServer {
tokio::select! {
value = &mut rx => {
let value = value??;
drop(droppable_token);
droppable_token.disarm();
Ok(serde_json::from_str(&value)?)
}
_ = token.cancelled() => {

View file

@ -9,6 +9,7 @@ use deno_npm::resolution::NpmResolutionSnapshot;
use deno_runtime::deno_io::FromRawIoHandle;
use deno_semver::package::PackageNv;
use deno_semver::Version;
use deno_task_shell::KillSignal;
use std::borrow::Cow;
use std::collections::HashSet;
use std::rc::Rc;
@ -155,6 +156,29 @@ impl<'a> LifecycleScripts<'a> {
packages: &[NpmResolutionPackage],
root_node_modules_dir_path: &Path,
progress_bar: &ProgressBar,
) -> Result<(), AnyError> {
let kill_signal = KillSignal::default();
let _drop_signal = kill_signal.clone().drop_guard();
// we don't run with signals forwarded because once signals
// are setup then they're process wide.
self
.finish_with_cancellation(
snapshot,
packages,
root_node_modules_dir_path,
progress_bar,
kill_signal,
)
.await
}
async fn finish_with_cancellation(
self,
snapshot: &NpmResolutionSnapshot,
packages: &[NpmResolutionPackage],
root_node_modules_dir_path: &Path,
progress_bar: &ProgressBar,
kill_signal: KillSignal,
) -> Result<(), AnyError> {
self.warn_not_run_scripts()?;
let get_package_path =
@ -246,6 +270,7 @@ impl<'a> LifecycleScripts<'a> {
stderr: TaskStdio::piped(),
stdout: TaskStdio::piped(),
}),
kill_signal: kill_signal.clone(),
},
)
.await?;

View file

@ -14,6 +14,7 @@ use deno_runtime::deno_node::NodeResolver;
use deno_semver::package::PackageNv;
use deno_task_shell::ExecutableCommand;
use deno_task_shell::ExecuteResult;
use deno_task_shell::KillSignal;
use deno_task_shell::ShellCommand;
use deno_task_shell::ShellCommandContext;
use deno_task_shell::ShellPipeReader;
@ -22,6 +23,7 @@ use lazy_regex::Lazy;
use regex::Regex;
use tokio::task::JoinHandle;
use tokio::task::LocalSet;
use tokio_util::sync::CancellationToken;
use crate::npm::CliNpmResolver;
use crate::npm::InnerCliNpmResolverRef;
@ -45,9 +47,11 @@ impl TaskStdio {
pub fn stdout() -> Self {
Self(None, ShellPipeWriter::stdout())
}
pub fn stderr() -> Self {
Self(None, ShellPipeWriter::stderr())
}
pub fn piped() -> Self {
let (r, w) = deno_task_shell::pipe();
Self(Some(r), w)
@ -62,8 +66,8 @@ pub struct TaskIo {
impl Default for TaskIo {
fn default() -> Self {
Self {
stderr: TaskStdio::stderr(),
stdout: TaskStdio::stdout(),
stderr: TaskStdio::stderr(),
}
}
}
@ -78,6 +82,7 @@ pub struct RunTaskOptions<'a> {
pub custom_commands: HashMap<String, Rc<dyn ShellCommand>>,
pub root_node_modules_dir: Option<&'a Path>,
pub stdio: Option<TaskIo>,
pub kill_signal: KillSignal,
}
pub type TaskCustomCommands = HashMap<String, Rc<dyn ShellCommand>>;
@ -96,8 +101,12 @@ pub async fn run_task(
.with_context(|| format!("Error parsing script '{}'.", opts.task_name))?;
let env_vars =
prepare_env_vars(opts.env_vars, opts.init_cwd, opts.root_node_modules_dir);
let state =
deno_task_shell::ShellState::new(env_vars, opts.cwd, opts.custom_commands);
let state = deno_task_shell::ShellState::new(
env_vars,
opts.cwd,
opts.custom_commands,
opts.kill_signal,
);
let stdio = opts.stdio.unwrap_or_default();
let (
TaskStdio(stdout_read, stdout_write),
@ -537,6 +546,80 @@ fn resolve_managed_npm_commands(
Ok(result)
}
/// Runs a deno task future forwarding any signals received
/// to the process.
///
/// Signal listeners and ctrl+c listening will be setup.
pub async fn run_future_forwarding_signals<TOutput>(
kill_signal: KillSignal,
future: impl std::future::Future<Output = TOutput>,
) -> TOutput {
fn spawn_future_with_cancellation(
future: impl std::future::Future<Output = ()> + 'static,
token: CancellationToken,
) {
deno_core::unsync::spawn(async move {
tokio::select! {
_ = future => {}
_ = token.cancelled() => {}
}
});
}
let token = CancellationToken::new();
let _token_drop_guard = token.clone().drop_guard();
let _drop_guard = kill_signal.clone().drop_guard();
spawn_future_with_cancellation(
listen_ctrl_c(kill_signal.clone()),
token.clone(),
);
#[cfg(unix)]
spawn_future_with_cancellation(
listen_and_forward_all_signals(kill_signal),
token,
);
future.await
}
async fn listen_ctrl_c(kill_signal: KillSignal) {
while let Ok(()) = tokio::signal::ctrl_c().await {
kill_signal.send(deno_task_shell::SignalKind::SIGINT)
}
}
#[cfg(unix)]
async fn listen_and_forward_all_signals(kill_signal: KillSignal) {
use deno_core::futures::FutureExt;
use deno_runtime::signal::SIGNAL_NUMS;
// listen and forward every signal we support
let mut futures = Vec::with_capacity(SIGNAL_NUMS.len());
for signo in SIGNAL_NUMS.iter().copied() {
if signo == libc::SIGKILL || signo == libc::SIGSTOP {
continue; // skip, can't listen to these
}
let kill_signal = kill_signal.clone();
futures.push(
async move {
let Ok(mut stream) = tokio::signal::unix::signal(
tokio::signal::unix::SignalKind::from_raw(signo),
) else {
return;
};
let signal_kind: deno_task_shell::SignalKind = signo.into();
while let Some(()) = stream.recv().await {
kill_signal.send(signal_kind);
}
}
.boxed_local(),
)
}
futures::future::join_all(futures).await;
}
#[cfg(test)]
mod test {

View file

@ -26,6 +26,7 @@ use deno_core::futures::StreamExt;
use deno_core::url::Url;
use deno_path_util::normalize_path;
use deno_runtime::deno_node::NodeResolver;
use deno_task_shell::KillSignal;
use deno_task_shell::ShellCommand;
use indexmap::IndexMap;
use regex::Regex;
@ -37,6 +38,7 @@ use crate::colors;
use crate::factory::CliFactory;
use crate::npm::CliNpmResolver;
use crate::task_runner;
use crate::task_runner::run_future_forwarding_signals;
use crate::util::fs::canonicalize_path;
#[derive(Debug)]
@ -226,28 +228,33 @@ pub async fn execute_script(
concurrency: no_of_concurrent_tasks.into(),
};
if task_flags.eval {
return task_runner
.run_deno_task(
&Url::from_directory_path(cli_options.initial_cwd()).unwrap(),
"",
&TaskDefinition {
command: task_flags.task.as_ref().unwrap().to_string(),
dependencies: vec![],
description: None,
},
)
.await;
}
for task_config in &packages_task_configs {
let exit_code = task_runner.run_tasks(task_config).await?;
if exit_code > 0 {
return Ok(exit_code);
let kill_signal = KillSignal::default();
run_future_forwarding_signals(kill_signal.clone(), async {
if task_flags.eval {
return task_runner
.run_deno_task(
&Url::from_directory_path(cli_options.initial_cwd()).unwrap(),
"",
&TaskDefinition {
command: task_flags.task.as_ref().unwrap().to_string(),
dependencies: vec![],
description: None,
},
kill_signal,
)
.await;
}
}
Ok(0)
for task_config in &packages_task_configs {
let exit_code = task_runner.run_tasks(task_config, &kill_signal).await?;
if exit_code > 0 {
return Ok(exit_code);
}
}
Ok(0)
})
.await
}
struct RunSingleOptions<'a> {
@ -255,6 +262,7 @@ struct RunSingleOptions<'a> {
script: &'a str,
cwd: &'a Path,
custom_commands: HashMap<String, Rc<dyn ShellCommand>>,
kill_signal: KillSignal,
}
struct TaskRunner<'a> {
@ -270,9 +278,10 @@ impl<'a> TaskRunner<'a> {
pub async fn run_tasks(
&self,
pkg_tasks_config: &PackageTaskInfo,
kill_signal: &KillSignal,
) -> Result<i32, deno_core::anyhow::Error> {
match sort_tasks_topo(pkg_tasks_config) {
Ok(sorted) => self.run_tasks_in_parallel(sorted).await,
Ok(sorted) => self.run_tasks_in_parallel(sorted, kill_signal).await,
Err(err) => match err {
TaskError::NotFound(name) => {
if self.task_flags.is_run {
@ -307,6 +316,7 @@ impl<'a> TaskRunner<'a> {
async fn run_tasks_in_parallel(
&self,
tasks: Vec<ResolvedTask<'a>>,
kill_signal: &KillSignal,
) -> Result<i32, deno_core::anyhow::Error> {
struct PendingTasksContext<'a> {
completed: HashSet<usize>,
@ -327,6 +337,7 @@ impl<'a> TaskRunner<'a> {
fn get_next_task<'b>(
&mut self,
runner: &'b TaskRunner<'b>,
kill_signal: &KillSignal,
) -> Option<
LocalBoxFuture<'b, Result<(i32, &'a ResolvedTask<'a>), AnyError>>,
>
@ -349,15 +360,23 @@ impl<'a> TaskRunner<'a> {
}
self.running.insert(task.id);
let kill_signal = kill_signal.clone();
return Some(
async move {
match task.task_or_script {
TaskOrScript::Task(_, def) => {
runner.run_deno_task(task.folder_url, task.name, def).await
runner
.run_deno_task(task.folder_url, task.name, def, kill_signal)
.await
}
TaskOrScript::Script(scripts, _) => {
runner
.run_npm_script(task.folder_url, task.name, scripts)
.run_npm_script(
task.folder_url,
task.name,
scripts,
kill_signal,
)
.await
}
}
@ -380,7 +399,7 @@ impl<'a> TaskRunner<'a> {
while context.has_remaining_tasks() {
while queue.len() < self.concurrency {
if let Some(task) = context.get_next_task(self) {
if let Some(task) = context.get_next_task(self, kill_signal) {
queue.push(task);
} else {
break;
@ -409,6 +428,7 @@ impl<'a> TaskRunner<'a> {
dir_url: &Url,
task_name: &str,
definition: &TaskDefinition,
kill_signal: KillSignal,
) -> Result<i32, deno_core::anyhow::Error> {
let cwd = match &self.task_flags.cwd {
Some(path) => canonicalize_path(&PathBuf::from(path))
@ -426,6 +446,7 @@ impl<'a> TaskRunner<'a> {
script: &definition.command,
cwd: &cwd,
custom_commands,
kill_signal,
})
.await
}
@ -435,6 +456,7 @@ impl<'a> TaskRunner<'a> {
dir_url: &Url,
task_name: &str,
scripts: &IndexMap<String, String>,
kill_signal: KillSignal,
) -> Result<i32, deno_core::anyhow::Error> {
// ensure the npm packages are installed if using a managed resolver
if let Some(npm_resolver) = self.npm_resolver.as_managed() {
@ -466,6 +488,7 @@ impl<'a> TaskRunner<'a> {
script,
cwd: &cwd,
custom_commands: custom_commands.clone(),
kill_signal: kill_signal.clone(),
})
.await?;
if exit_code > 0 {
@ -486,6 +509,7 @@ impl<'a> TaskRunner<'a> {
script,
cwd,
custom_commands,
kill_signal,
} = opts;
output_task(
@ -504,6 +528,7 @@ impl<'a> TaskRunner<'a> {
argv: self.cli_options.argv(),
root_node_modules_dir: self.npm_resolver.root_node_modules_path(),
stdio: None,
kill_signal,
})
.await?
.exit_code,

View file

@ -34,6 +34,7 @@ pub mod inspector_server;
pub mod js;
pub mod ops;
pub mod permissions;
pub mod signal;
pub mod snapshot;
pub mod sys_info;
pub mod tokio_util;

View file

@ -256,9 +256,7 @@ impl TryFrom<ExitStatus> for ChildStatus {
success: false,
code: 128 + signal,
#[cfg(unix)]
signal: Some(
crate::ops::signal::signal_int_to_str(signal)?.to_string(),
),
signal: Some(crate::signal::signal_int_to_str(signal)?.to_string()),
#[cfg(not(unix))]
signal: None,
}
@ -1076,7 +1074,8 @@ mod deprecated {
#[cfg(unix)]
pub fn kill(pid: i32, signal: &str) -> Result<(), ProcessError> {
let signo = super::super::signal::signal_str_to_int(signal)?;
let signo = crate::signal::signal_str_to_int(signal)
.map_err(SignalError::InvalidSignalStr)?;
use nix::sys::signal::kill as unix_kill;
use nix::sys::signal::Signal;
use nix::unistd::Pid;
@ -1099,7 +1098,12 @@ mod deprecated {
use winapi::um::winnt::PROCESS_TERMINATE;
if !matches!(signal, "SIGKILL" | "SIGTERM") {
Err(SignalError::InvalidSignalStr(signal.to_string()).into())
Err(
SignalError::InvalidSignalStr(crate::signal::InvalidSignalStrError(
signal.to_string(),
))
.into(),
)
} else if pid <= 0 {
Err(ProcessError::InvalidPid)
} else {

View file

@ -46,34 +46,10 @@ deno_core::extension!(
#[derive(Debug, thiserror::Error)]
pub enum SignalError {
#[cfg(any(
target_os = "android",
target_os = "linux",
target_os = "openbsd",
target_os = "openbsd",
target_os = "macos",
target_os = "solaris",
target_os = "illumos"
))]
#[error("Invalid signal: {0}")]
InvalidSignalStr(String),
#[cfg(any(
target_os = "android",
target_os = "linux",
target_os = "openbsd",
target_os = "openbsd",
target_os = "macos",
target_os = "solaris",
target_os = "illumos"
))]
#[error("Invalid signal: {0}")]
InvalidSignalInt(libc::c_int),
#[cfg(target_os = "windows")]
#[error("Windows only supports ctrl-c (SIGINT) and ctrl-break (SIGBREAK), but got {0}")]
InvalidSignalStr(String),
#[cfg(target_os = "windows")]
#[error("Windows only supports ctrl-c (SIGINT) and ctrl-break (SIGBREAK), but got {0}")]
InvalidSignalInt(libc::c_int),
#[error(transparent)]
InvalidSignalStr(#[from] crate::signal::InvalidSignalStrError),
#[error(transparent)]
InvalidSignalInt(#[from] crate::signal::InvalidSignalIntError),
#[error("Binding to signal '{0}' is not allowed")]
SignalNotAllowed(String),
#[error("{0}")]
@ -181,218 +157,6 @@ impl Resource for SignalStreamResource {
}
}
macro_rules! first_literal {
($head:literal $(, $tail:literal)*) => {
$head
};
}
macro_rules! signal_dict {
($(($number:literal, $($name:literal)|+)),*) => {
pub fn signal_str_to_int(s: &str) -> Result<libc::c_int, SignalError> {
match s {
$($($name)|* => Ok($number),)*
_ => Err(SignalError::InvalidSignalStr(s.to_string())),
}
}
pub fn signal_int_to_str(s: libc::c_int) -> Result<&'static str, SignalError> {
match s {
$($number => Ok(first_literal!($($name),+)),)*
_ => Err(SignalError::InvalidSignalInt(s)),
}
}
}
}
#[cfg(target_os = "freebsd")]
signal_dict!(
(1, "SIGHUP"),
(2, "SIGINT"),
(3, "SIGQUIT"),
(4, "SIGILL"),
(5, "SIGTRAP"),
(6, "SIGABRT" | "SIGIOT"),
(7, "SIGEMT"),
(8, "SIGFPE"),
(9, "SIGKILL"),
(10, "SIGBUS"),
(11, "SIGSEGV"),
(12, "SIGSYS"),
(13, "SIGPIPE"),
(14, "SIGALRM"),
(15, "SIGTERM"),
(16, "SIGURG"),
(17, "SIGSTOP"),
(18, "SIGTSTP"),
(19, "SIGCONT"),
(20, "SIGCHLD"),
(21, "SIGTTIN"),
(22, "SIGTTOU"),
(23, "SIGIO"),
(24, "SIGXCPU"),
(25, "SIGXFSZ"),
(26, "SIGVTALRM"),
(27, "SIGPROF"),
(28, "SIGWINCH"),
(29, "SIGINFO"),
(30, "SIGUSR1"),
(31, "SIGUSR2"),
(32, "SIGTHR"),
(33, "SIGLIBRT")
);
#[cfg(target_os = "openbsd")]
signal_dict!(
(1, "SIGHUP"),
(2, "SIGINT"),
(3, "SIGQUIT"),
(4, "SIGILL"),
(5, "SIGTRAP"),
(6, "SIGABRT" | "SIGIOT"),
(7, "SIGEMT"),
(8, "SIGKILL"),
(10, "SIGBUS"),
(11, "SIGSEGV"),
(12, "SIGSYS"),
(13, "SIGPIPE"),
(14, "SIGALRM"),
(15, "SIGTERM"),
(16, "SIGURG"),
(17, "SIGSTOP"),
(18, "SIGTSTP"),
(19, "SIGCONT"),
(20, "SIGCHLD"),
(21, "SIGTTIN"),
(22, "SIGTTOU"),
(23, "SIGIO"),
(24, "SIGXCPU"),
(25, "SIGXFSZ"),
(26, "SIGVTALRM"),
(27, "SIGPROF"),
(28, "SIGWINCH"),
(29, "SIGINFO"),
(30, "SIGUSR1"),
(31, "SIGUSR2"),
(32, "SIGTHR")
);
#[cfg(any(target_os = "android", target_os = "linux"))]
signal_dict!(
(1, "SIGHUP"),
(2, "SIGINT"),
(3, "SIGQUIT"),
(4, "SIGILL"),
(5, "SIGTRAP"),
(6, "SIGABRT" | "SIGIOT"),
(7, "SIGBUS"),
(8, "SIGFPE"),
(9, "SIGKILL"),
(10, "SIGUSR1"),
(11, "SIGSEGV"),
(12, "SIGUSR2"),
(13, "SIGPIPE"),
(14, "SIGALRM"),
(15, "SIGTERM"),
(16, "SIGSTKFLT"),
(17, "SIGCHLD"),
(18, "SIGCONT"),
(19, "SIGSTOP"),
(20, "SIGTSTP"),
(21, "SIGTTIN"),
(22, "SIGTTOU"),
(23, "SIGURG"),
(24, "SIGXCPU"),
(25, "SIGXFSZ"),
(26, "SIGVTALRM"),
(27, "SIGPROF"),
(28, "SIGWINCH"),
(29, "SIGIO" | "SIGPOLL"),
(30, "SIGPWR"),
(31, "SIGSYS" | "SIGUNUSED")
);
#[cfg(target_os = "macos")]
signal_dict!(
(1, "SIGHUP"),
(2, "SIGINT"),
(3, "SIGQUIT"),
(4, "SIGILL"),
(5, "SIGTRAP"),
(6, "SIGABRT" | "SIGIOT"),
(7, "SIGEMT"),
(8, "SIGFPE"),
(9, "SIGKILL"),
(10, "SIGBUS"),
(11, "SIGSEGV"),
(12, "SIGSYS"),
(13, "SIGPIPE"),
(14, "SIGALRM"),
(15, "SIGTERM"),
(16, "SIGURG"),
(17, "SIGSTOP"),
(18, "SIGTSTP"),
(19, "SIGCONT"),
(20, "SIGCHLD"),
(21, "SIGTTIN"),
(22, "SIGTTOU"),
(23, "SIGIO"),
(24, "SIGXCPU"),
(25, "SIGXFSZ"),
(26, "SIGVTALRM"),
(27, "SIGPROF"),
(28, "SIGWINCH"),
(29, "SIGINFO"),
(30, "SIGUSR1"),
(31, "SIGUSR2")
);
#[cfg(any(target_os = "solaris", target_os = "illumos"))]
signal_dict!(
(1, "SIGHUP"),
(2, "SIGINT"),
(3, "SIGQUIT"),
(4, "SIGILL"),
(5, "SIGTRAP"),
(6, "SIGABRT" | "SIGIOT"),
(7, "SIGEMT"),
(8, "SIGFPE"),
(9, "SIGKILL"),
(10, "SIGBUS"),
(11, "SIGSEGV"),
(12, "SIGSYS"),
(13, "SIGPIPE"),
(14, "SIGALRM"),
(15, "SIGTERM"),
(16, "SIGUSR1"),
(17, "SIGUSR2"),
(18, "SIGCHLD"),
(19, "SIGPWR"),
(20, "SIGWINCH"),
(21, "SIGURG"),
(22, "SIGPOLL"),
(23, "SIGSTOP"),
(24, "SIGTSTP"),
(25, "SIGCONT"),
(26, "SIGTTIN"),
(27, "SIGTTOU"),
(28, "SIGVTALRM"),
(29, "SIGPROF"),
(30, "SIGXCPU"),
(31, "SIGXFSZ"),
(32, "SIGWAITING"),
(33, "SIGLWP"),
(34, "SIGFREEZE"),
(35, "SIGTHAW"),
(36, "SIGCANCEL"),
(37, "SIGLOST"),
(38, "SIGXRES"),
(39, "SIGJVM1"),
(40, "SIGJVM2")
);
#[cfg(target_os = "windows")]
signal_dict!((2, "SIGINT"), (21, "SIGBREAK"));
#[cfg(unix)]
#[op2(fast)]
#[smi]
@ -400,7 +164,7 @@ fn op_signal_bind(
state: &mut OpState,
#[string] sig: &str,
) -> Result<ResourceId, SignalError> {
let signo = signal_str_to_int(sig)?;
let signo = crate::signal::signal_str_to_int(sig)?;
if signal_hook_registry::FORBIDDEN.contains(&signo) {
return Err(SignalError::SignalNotAllowed(sig.to_string()));
}
@ -437,7 +201,7 @@ fn op_signal_bind(
state: &mut OpState,
#[string] sig: &str,
) -> Result<ResourceId, SignalError> {
let signo = signal_str_to_int(sig)?;
let signo = crate::signal::signal_str_to_int(sig)?;
let resource = SignalStreamResource {
signal: AsyncRefCell::new(match signo {
// SIGINT

257
runtime/signal.rs Normal file
View file

@ -0,0 +1,257 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
#[cfg(target_os = "windows")]
#[derive(Debug, thiserror::Error)]
#[error("Windows only supports ctrl-c (SIGINT) and ctrl-break (SIGBREAK), but got {0}")]
pub struct InvalidSignalStrError(pub String);
#[cfg(any(
target_os = "android",
target_os = "linux",
target_os = "openbsd",
target_os = "openbsd",
target_os = "macos",
target_os = "solaris",
target_os = "illumos"
))]
#[derive(Debug, thiserror::Error)]
#[error("Invalid signal: {0}")]
pub struct InvalidSignalStrError(pub String);
#[cfg(target_os = "windows")]
#[derive(Debug, thiserror::Error)]
#[error("Windows only supports ctrl-c (SIGINT) and ctrl-break (SIGBREAK), but got {0}")]
pub struct InvalidSignalIntError(pub libc::c_int);
#[cfg(any(
target_os = "android",
target_os = "linux",
target_os = "openbsd",
target_os = "openbsd",
target_os = "macos",
target_os = "solaris",
target_os = "illumos"
))]
#[derive(Debug, thiserror::Error)]
#[error("Invalid signal: {0}")]
pub struct InvalidSignalIntError(pub libc::c_int);
macro_rules! first_literal {
($head:literal $(, $tail:literal)*) => {
$head
};
}
macro_rules! signal_dict {
($(($number:literal, $($name:literal)|+)),*) => {
pub const SIGNAL_NUMS: &'static [libc::c_int] = &[
$(
$number
),*
];
pub fn signal_str_to_int(s: &str) -> Result<libc::c_int, InvalidSignalStrError> {
match s {
$($($name)|* => Ok($number),)*
_ => Err(InvalidSignalStrError(s.to_string())),
}
}
pub fn signal_int_to_str(s: libc::c_int) -> Result<&'static str, InvalidSignalIntError> {
match s {
$($number => Ok(first_literal!($($name),+)),)*
_ => Err(InvalidSignalIntError(s)),
}
}
}
}
#[cfg(target_os = "freebsd")]
signal_dict!(
(1, "SIGHUP"),
(2, "SIGINT"),
(3, "SIGQUIT"),
(4, "SIGILL"),
(5, "SIGTRAP"),
(6, "SIGABRT" | "SIGIOT"),
(7, "SIGEMT"),
(8, "SIGFPE"),
(9, "SIGKILL"),
(10, "SIGBUS"),
(11, "SIGSEGV"),
(12, "SIGSYS"),
(13, "SIGPIPE"),
(14, "SIGALRM"),
(15, "SIGTERM"),
(16, "SIGURG"),
(17, "SIGSTOP"),
(18, "SIGTSTP"),
(19, "SIGCONT"),
(20, "SIGCHLD"),
(21, "SIGTTIN"),
(22, "SIGTTOU"),
(23, "SIGIO"),
(24, "SIGXCPU"),
(25, "SIGXFSZ"),
(26, "SIGVTALRM"),
(27, "SIGPROF"),
(28, "SIGWINCH"),
(29, "SIGINFO"),
(30, "SIGUSR1"),
(31, "SIGUSR2"),
(32, "SIGTHR"),
(33, "SIGLIBRT")
);
#[cfg(target_os = "openbsd")]
signal_dict!(
(1, "SIGHUP"),
(2, "SIGINT"),
(3, "SIGQUIT"),
(4, "SIGILL"),
(5, "SIGTRAP"),
(6, "SIGABRT" | "SIGIOT"),
(7, "SIGEMT"),
(8, "SIGKILL"),
(10, "SIGBUS"),
(11, "SIGSEGV"),
(12, "SIGSYS"),
(13, "SIGPIPE"),
(14, "SIGALRM"),
(15, "SIGTERM"),
(16, "SIGURG"),
(17, "SIGSTOP"),
(18, "SIGTSTP"),
(19, "SIGCONT"),
(20, "SIGCHLD"),
(21, "SIGTTIN"),
(22, "SIGTTOU"),
(23, "SIGIO"),
(24, "SIGXCPU"),
(25, "SIGXFSZ"),
(26, "SIGVTALRM"),
(27, "SIGPROF"),
(28, "SIGWINCH"),
(29, "SIGINFO"),
(30, "SIGUSR1"),
(31, "SIGUSR2"),
(32, "SIGTHR")
);
#[cfg(any(target_os = "android", target_os = "linux"))]
signal_dict!(
(1, "SIGHUP"),
(2, "SIGINT"),
(3, "SIGQUIT"),
(4, "SIGILL"),
(5, "SIGTRAP"),
(6, "SIGABRT" | "SIGIOT"),
(7, "SIGBUS"),
(8, "SIGFPE"),
(9, "SIGKILL"),
(10, "SIGUSR1"),
(11, "SIGSEGV"),
(12, "SIGUSR2"),
(13, "SIGPIPE"),
(14, "SIGALRM"),
(15, "SIGTERM"),
(16, "SIGSTKFLT"),
(17, "SIGCHLD"),
(18, "SIGCONT"),
(19, "SIGSTOP"),
(20, "SIGTSTP"),
(21, "SIGTTIN"),
(22, "SIGTTOU"),
(23, "SIGURG"),
(24, "SIGXCPU"),
(25, "SIGXFSZ"),
(26, "SIGVTALRM"),
(27, "SIGPROF"),
(28, "SIGWINCH"),
(29, "SIGIO" | "SIGPOLL"),
(30, "SIGPWR"),
(31, "SIGSYS" | "SIGUNUSED")
);
#[cfg(target_os = "macos")]
signal_dict!(
(1, "SIGHUP"),
(2, "SIGINT"),
(3, "SIGQUIT"),
(4, "SIGILL"),
(5, "SIGTRAP"),
(6, "SIGABRT" | "SIGIOT"),
(7, "SIGEMT"),
(8, "SIGFPE"),
(9, "SIGKILL"),
(10, "SIGBUS"),
(11, "SIGSEGV"),
(12, "SIGSYS"),
(13, "SIGPIPE"),
(14, "SIGALRM"),
(15, "SIGTERM"),
(16, "SIGURG"),
(17, "SIGSTOP"),
(18, "SIGTSTP"),
(19, "SIGCONT"),
(20, "SIGCHLD"),
(21, "SIGTTIN"),
(22, "SIGTTOU"),
(23, "SIGIO"),
(24, "SIGXCPU"),
(25, "SIGXFSZ"),
(26, "SIGVTALRM"),
(27, "SIGPROF"),
(28, "SIGWINCH"),
(29, "SIGINFO"),
(30, "SIGUSR1"),
(31, "SIGUSR2")
);
#[cfg(any(target_os = "solaris", target_os = "illumos"))]
signal_dict!(
(1, "SIGHUP"),
(2, "SIGINT"),
(3, "SIGQUIT"),
(4, "SIGILL"),
(5, "SIGTRAP"),
(6, "SIGABRT" | "SIGIOT"),
(7, "SIGEMT"),
(8, "SIGFPE"),
(9, "SIGKILL"),
(10, "SIGBUS"),
(11, "SIGSEGV"),
(12, "SIGSYS"),
(13, "SIGPIPE"),
(14, "SIGALRM"),
(15, "SIGTERM"),
(16, "SIGUSR1"),
(17, "SIGUSR2"),
(18, "SIGCHLD"),
(19, "SIGPWR"),
(20, "SIGWINCH"),
(21, "SIGURG"),
(22, "SIGPOLL"),
(23, "SIGSTOP"),
(24, "SIGTSTP"),
(25, "SIGCONT"),
(26, "SIGTTIN"),
(27, "SIGTTOU"),
(28, "SIGVTALRM"),
(29, "SIGPROF"),
(30, "SIGXCPU"),
(31, "SIGXFSZ"),
(32, "SIGWAITING"),
(33, "SIGLWP"),
(34, "SIGFREEZE"),
(35, "SIGTHAW"),
(36, "SIGCANCEL"),
(37, "SIGLOST"),
(38, "SIGXRES"),
(39, "SIGJVM1"),
(40, "SIGJVM2")
);
#[cfg(target_os = "windows")]
signal_dict!((2, "SIGINT"), (21, "SIGBREAK"));

View file

@ -0,0 +1,8 @@
{
// signals don't really exist on windows
"if": "unix",
// this runs a deno task
"args": "run -A --check sender.ts",
// just ensure this doesn't hang and completes successfully
"output": "[WILDCARD]"
}

View file

@ -0,0 +1,5 @@
{
"tasks": {
"listener": "deno run listener.ts"
}
}

View file

@ -0,0 +1,16 @@
import { signals } from "./signals.ts";
for (const signal of signals) {
Deno.addSignalListener(signal, () => {
console.log("Received", signal);
if (signal === "SIGTERM") {
Deno.exit(0);
}
});
}
setInterval(() => {
// keep alive
}, 1000);
console.log("Ready");

View file

@ -0,0 +1,55 @@
import { signals } from "./signals.ts";
class StdoutReader {
readonly #reader: ReadableStreamDefaultReader<string>;
#text = "";
constructor(stream: ReadableStream<Uint8Array>) {
const textStream = stream.pipeThrough(new TextDecoderStream());
this.#reader = textStream.getReader();
}
[Symbol.dispose]() {
this.#reader.releaseLock();
}
async waitForText(waitingText: string) {
if (this.#text.includes(waitingText)) {
return;
}
while (true) {
const { value, done } = await this.#reader.read();
if (value) {
this.#text += value;
if (this.#text.includes(waitingText)) {
break;
}
}
if (done) {
throw new Error("Did not find text: " + waitingText);
}
}
}
}
const command = new Deno.Command(Deno.execPath(), {
args: ["task", "listener"],
stdout: "piped",
});
const child = command.spawn();
const reader = new StdoutReader(child.stdout!);
await reader.waitForText("Ready");
for (const signal of signals) {
if (signal === "SIGTERM") {
continue;
}
console.error("Sending", signal);
child.kill(signal);
await reader.waitForText("Received " + signal);
}
console.error("Sending SIGTERM");
child.kill("SIGTERM");

View file

@ -0,0 +1,65 @@
const signals = [
"SIGABRT",
"SIGALRM",
"SIGBUS",
"SIGCHLD",
"SIGCONT",
"SIGEMT",
"SIGFPE",
"SIGHUP",
"SIGILL",
"SIGINFO",
"SIGINT",
"SIGIO",
"SIGPOLL",
"SIGPIPE",
"SIGPROF",
"SIGPWR",
"SIGQUIT",
"SIGSEGV",
"SIGSTKFLT",
"SIGSYS",
"SIGTERM",
"SIGTRAP",
"SIGTSTP",
"SIGTTIN",
"SIGTTOU",
"SIGURG",
"SIGUSR1",
"SIGUSR2",
"SIGVTALRM",
"SIGWINCH",
"SIGXCPU",
"SIGXFSZ",
] as const;
// SIGKILL and SIGSTOP are not stoppable, SIGBREAK is for windows, and SIGUNUSED is not defined
type SignalsToTest = Exclude<
Deno.Signal,
"SIGKILL" | "SIGSTOP" | "SIGBREAK" | "SIGUNUSED"
>;
type EnsureAllSignalsIncluded = SignalsToTest extends typeof signals[number]
? typeof signals[number] extends SignalsToTest ? true
: never
: never;
const _checkSignals: EnsureAllSignalsIncluded = true;
const osSpecificSignals = signals.filter((s) => {
switch (s) {
case "SIGEMT":
return Deno.build.os === "darwin";
case "SIGINFO":
case "SIGFPE":
case "SIGILL":
case "SIGSEGV":
return Deno.build.os === "freebsd";
case "SIGPOLL":
case "SIGPWR":
case "SIGSTKFLT":
return Deno.build.os === "linux";
default:
return true;
}
});
export { osSpecificSignals as signals };