Skip to content

Commit b34f2e2

Browse files
fix: use signal handler in shim (#4313)
### Description I believe this fixes #3711 #4196 added graceful shutting down when calling `go-turbo`, this does the same but for when we need to invoke the correct Rust binary in `shim.rs`. I extracted the logic added in #4196 to a function that can be used throughout the codebase. ### Testing Instructions Currently only doing manual via the reproduction provided in #3711. Edit: I originally wanted to add integration test to cover these cases, this has proven to be a challenge. Will still look at trying to orchestrate a test for this, but considering how much traction the related issues have, I don't want to block on getting an integration test.
1 parent 29c71c6 commit b34f2e2

File tree

13 files changed

+107
-35
lines changed

13 files changed

+107
-35
lines changed

Cargo.lock

+3-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
Setup
2+
$ . ${TESTDIR}/../setup.sh
3+
$ . ${TESTDIR}/setup.sh $(pwd)
4+
5+
Run script with INT handler and verify that INT gets passed to script
6+
7+
Start turbo in the background
8+
$ ${TURBO} trap &
9+
Save the PID of turbo
10+
$ TURBO_PID=$!
11+
We send INT to turbo, but with a delay to give us time to bring turbo back to
12+
the foreground.
13+
$ sh -c "sleep 1 && kill -2 ${TURBO_PID}" &
14+
Bring turbo back to the foreground
15+
$ fg 1
16+
${TURBO} trap
17+
\xe2\x80\xa2 Packages in scope: test (esc)
18+
\xe2\x80\xa2 Running trap in 1 packages (esc)
19+
\xe2\x80\xa2 Remote caching disabled (esc)
20+
test:trap: cache miss, executing d25759ee0a8e12ae
21+
test:trap:
22+
test:trap: > trap
23+
test:trap: > trap 'echo trap hit; sleep 1; echo trap finish' INT; sleep 5 && echo 'script finish'
24+
test:trap:
25+
test:trap: trap hit
26+
test:trap: trap finish
27+
test:trap: npm ERR! Lifecycle script `trap` failed with error:
28+
test:trap: npm ERR! Error: command failed
29+
test:trap: npm ERR! in workspace: test
30+
test:trap: npm ERR! at location: .*ctrlc.t/apps/test (re)
31+
[1]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/bin/bash
2+
# Enable jobcontrol as we need it for fg to work
3+
set -m
4+
5+
SCRIPT_DIR=$(dirname ${BASH_SOURCE[0]})
6+
TARGET_DIR=$1
7+
cp -a ${SCRIPT_DIR}/test_repo/. ${TARGET_DIR}/
8+
${SCRIPT_DIR}/../setup_git.sh ${TARGET_DIR}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "test",
3+
"scripts": {
4+
"trap": "trap 'echo trap hit; sleep 1; echo trap finish' INT; sleep 5 && echo 'script finish'"
5+
}
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "signal-test",
3+
"workspaces": [
4+
"apps/*"
5+
],
6+
"packageManager": "npm@8.0.0"
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"$schema": "https://turbo.build/schema.json",
3+
"pipeline": {
4+
"trap": {}
5+
}
6+
}

cli/internal/ffi/libgit2.a

5.93 MB
Binary file not shown.

crates/turborepo-lib/Cargo.toml

+3
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ clap_complete = { workspace = true }
3131
command-group = { version = "2.1.0", features = ["with-tokio"] }
3232
config = "0.13"
3333
console = { workspace = true }
34+
ctrlc = { version = "3.2.5", features = ["termination"] }
3435
dialoguer = { workspace = true, features = ["fuzzy-select"] }
3536
dirs-next = "2.0.0"
3637
dunce = { workspace = true }
@@ -42,6 +43,7 @@ hostname = "0.3.1"
4243
humantime = "2.1.0"
4344
indicatif = { workspace = true }
4445
lazy_static = { workspace = true }
46+
libc = "0.2.140"
4547
log = { workspace = true }
4648
notify = { version = "5.1.0", default-features = false, features = [
4749
"macos_kqueue",
@@ -55,6 +57,7 @@ serde = { workspace = true, features = ["derive"] }
5557
serde_json = { workspace = true }
5658
serde_yaml = { workspace = true }
5759
sha2 = "0.10.6"
60+
shared_child = "1.0.0"
5861
sysinfo = "0.27.7"
5962
thiserror = "1.0.38"
6063
tiny-gradient = { workspace = true }

crates/turborepo-lib/src/child.rs

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
use std::{process::Command, sync::Arc};
2+
3+
use anyhow::Result;
4+
use shared_child::SharedChild;
5+
6+
/// Spawns a child in a way where SIGINT is correctly forwarded to the child
7+
pub fn spawn_child(mut command: Command) -> Result<Arc<SharedChild>> {
8+
let shared_child = Arc::new(SharedChild::spawn(&mut command)?);
9+
let handler_shared_child = shared_child.clone();
10+
11+
ctrlc::set_handler(move || {
12+
// on windows, we can't send signals so just kill
13+
// we are quiting anyways so just ignore
14+
#[cfg(target_os = "windows")]
15+
handler_shared_child.kill().ok();
16+
17+
// on unix, we should send a SIGTERM to the child
18+
// so that go can gracefully shut down process groups
19+
// SAFETY: we could pull in the nix crate to handle this
20+
// 'safely' but nix::sys::signal::kill just calls libc::kill
21+
#[cfg(not(target_os = "windows"))]
22+
unsafe {
23+
libc::kill(handler_shared_child.id() as i32, libc::SIGTERM);
24+
}
25+
})
26+
.expect("handler set");
27+
28+
Ok(shared_child)
29+
}

crates/turborepo-lib/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#![feature(assert_matches)]
22

3+
mod child;
34
mod cli;
45
mod client;
56
mod commands;
@@ -11,6 +12,7 @@ mod shim;
1112
mod ui;
1213

1314
use anyhow::Result;
15+
pub use child::spawn_child;
1416
use log::error;
1517

1618
pub use crate::cli::Args;

crates/turborepo-lib/src/shim.rs

+9-6
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use serde::{Deserialize, Serialize};
2121
use tiny_gradient::{GradientStr, RGB};
2222
use turbo_updater::check_for_updates;
2323

24-
use crate::{cli, get_version, package_manager::Globs, PackageManager, Payload};
24+
use crate::{cli, get_version, package_manager::Globs, spawn_child, PackageManager, Payload};
2525

2626
// all arguments that result in a stdout that much be directly parsable and
2727
// should not be paired with additional output (from the update notifier for
@@ -649,18 +649,21 @@ impl RepoState {
649649

650650
// We spawn a process that executes the local turbo
651651
// that we've found in node_modules/.bin/turbo.
652-
let mut command = process::Command::new(local_turbo_path)
652+
let mut command = process::Command::new(local_turbo_path);
653+
command
653654
.args(&raw_args)
654655
// rather than passing an argument that local turbo might not understand, set
655656
// an environment variable that can be optionally used
656657
.env(cli::INVOCATION_DIR_ENV_VAR, &shim_args.invocation_dir)
657658
.current_dir(cwd)
658659
.stdout(Stdio::inherit())
659-
.stderr(Stdio::inherit())
660-
.spawn()
661-
.expect("Failed to execute turbo.");
660+
.stderr(Stdio::inherit());
662661

663-
Ok(command.wait()?.code().unwrap_or(2))
662+
let child = spawn_child(command)?;
663+
664+
let exit_code = child.wait()?.code().unwrap_or(2);
665+
666+
Ok(exit_code)
664667
}
665668
}
666669

crates/turborepo/Cargo.toml

-3
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,11 @@ anyhow = { workspace = true, features = ["backtrace"] }
2727
clap = { workspace = true, features = ["derive"] }
2828
clap_complete = { workspace = true }
2929
command-group = { version = "2.0.1", features = ["with-tokio"] }
30-
ctrlc = { version = "3.2.5", features = ["termination"] }
3130
dunce = { workspace = true }
32-
libc = "0.2.140"
3331
log = { workspace = true }
3432
serde = { workspace = true, features = ["derive"] }
3533
serde_json = { workspace = true }
3634
serde_yaml = { workspace = true }
37-
shared_child = "1.0.0"
3835
tiny-gradient = { workspace = true }
3936
tokio-util = { version = "0.7.7", features = ["io"] }
4037
turborepo-lib = { workspace = true }

crates/turborepo/src/main.rs

+3-23
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use std::{
77
use anyhow::Result;
88
use dunce::canonicalize as fs_canonicalize;
99
use log::{debug, error, trace};
10-
use turborepo_lib::{Args, Payload};
10+
use turborepo_lib::{spawn_child, Args, Payload};
1111

1212
fn run_go_binary(args: Args) -> Result<i32> {
1313
// canonicalize the binary path to ensure we can find go-turbo
@@ -49,28 +49,8 @@ fn run_go_binary(args: Args) -> Result<i32> {
4949
.stdout(Stdio::inherit())
5050
.stderr(Stdio::inherit());
5151

52-
let shared_child = shared_child::SharedChild::spawn(&mut command).unwrap();
53-
let child_arc = std::sync::Arc::new(shared_child);
54-
55-
let child_arc_clone = child_arc.clone();
56-
ctrlc::set_handler(move || {
57-
// on windows, we can't send signals so just kill
58-
// we are quiting anyways so just ignore
59-
#[cfg(target_os = "windows")]
60-
child_arc_clone.kill().ok();
61-
62-
// on unix, we should send a SIGTERM to the child
63-
// so that go can gracefully shut down process groups
64-
// SAFETY: we could pull in the nix crate to handle this
65-
// 'safely' but nix::sys::signal::kill just calls libc::kill
66-
#[cfg(not(target_os = "windows"))]
67-
unsafe {
68-
libc::kill(child_arc_clone.id() as i32, libc::SIGTERM);
69-
}
70-
})
71-
.expect("handler set");
72-
73-
let exit_code = child_arc.wait()?.code().unwrap_or(2);
52+
let child = spawn_child(command)?;
53+
let exit_code = child.wait()?.code().unwrap_or(2);
7454

7555
Ok(exit_code)
7656
}

0 commit comments

Comments
 (0)