diff --git a/crates/turborepo-process/README.md b/crates/turborepo-process/README.md index 626c08a6c5715..bfa181eb6bd4a 100644 --- a/crates/turborepo-process/README.md +++ b/crates/turborepo-process/README.md @@ -41,6 +41,9 @@ Process management for running task commands. Spawns and manages child processes - PTY support is inferred from terminal attachment on non-Windows platforms - On Windows, graceful shutdown sends KILL immediately (no SIGINT equivalent) -- On Unix, processes are spawned in their own process group via `setsid()` to enable group signaling +- On Unix, processes are spawned in their own process group to enable group signaling - `stop_tasks()` allows selective process termination without closing the manager (used for watch mode restarts) - Closing stdin on Windows with ConPTY immediately terminates the process, so stdin is kept open in that case +- Graceful shutdown should be handled internally by tracking spawned process groups + until they exit; parent-death cleanup after Turbo crashes or is killed is a + separate concern diff --git a/crates/turborepo-process/src/child.rs b/crates/turborepo-process/src/child.rs index baf915e998dc3..4dc335f562d89 100644 --- a/crates/turborepo-process/src/child.rs +++ b/crates/turborepo-process/src/child.rs @@ -20,11 +20,6 @@ const POST_EXIT_OUTPUT_DRAIN_TIMEOUT: Duration = Duration::from_millis(100); #[cfg(unix)] const PROCESS_GROUP_DRAIN_POLL_INTERVAL: Duration = Duration::from_millis(10); -#[cfg(unix)] -const PARENT_DEATH_ESCALATION_DELAY: Duration = Duration::from_secs(2); - -#[cfg(unix)] -use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd, OwnedFd, RawFd}; use std::{ fmt, io::{self, BufRead, Read, Write}, @@ -91,8 +86,6 @@ struct ChildHandle { shutdown_semantics: ShutdownSemantics, #[cfg(unix)] target_identity: Option, - #[cfg(unix)] - parent_death_guard: Option, #[cfg(windows)] _job: Option, } @@ -123,7 +116,7 @@ impl ShutdownSemantics { fn process_group() -> Self { Self { graceful_interrupt_target: GracefulInterruptTarget::ProcessGroup, - wait_for_process_group_after_child_exit: false, + wait_for_process_group_after_child_exit: true, } } @@ -142,90 +135,6 @@ struct TargetIdentity { session_id: libc::pid_t, } -#[cfg(unix)] -#[derive(Debug)] -struct ParentDeathGuard { - write_fd: Option, - watchdog_pid: Option, -} - -#[cfg(unix)] -impl ParentDeathGuard { - fn spawn_for_pid(target_pid: libc::pid_t) -> io::Result { - let (read_fd, write_fd) = parent_death_pipe()?; - let watchdog_pid = spawn_parent_death_watchdog(target_pid, read_fd)?; - - Ok(Self { - write_fd: Some(write_fd), - watchdog_pid: Some(watchdog_pid), - }) - } - - fn disarm(&mut self) { - let Some(write_fd) = self.write_fd.take() else { - return; - }; - - let _ = unsafe { libc::write(write_fd.as_raw_fd(), [1_u8].as_ptr().cast(), 1) }; - drop(write_fd); - self.reap_watchdog(); - } - - fn reap_watchdog(&mut self) { - let Some(watchdog_pid) = self.watchdog_pid.take() else { - return; - }; - - let mut status = 0; - loop { - let wait_result = unsafe { libc::waitpid(watchdog_pid, &mut status, 0) }; - if wait_result != -1 { - break; - } - - if io::Error::last_os_error().raw_os_error() != Some(libc::EINTR) { - break; - } - } - } -} - -#[cfg(unix)] -impl Drop for ParentDeathGuard { - fn drop(&mut self) { - self.write_fd.take(); - self.reap_watchdog(); - } -} - -#[cfg(unix)] -fn parent_death_pipe() -> io::Result<(OwnedFd, OwnedFd)> { - let mut fds = [0; 2]; - if unsafe { libc::pipe(fds.as_mut_ptr()) } == -1 { - return Err(io::Error::last_os_error()); - } - - let read_fd = unsafe { OwnedFd::from_raw_fd(fds[0]) }; - let write_fd = unsafe { OwnedFd::from_raw_fd(fds[1]) }; - set_cloexec(read_fd.as_raw_fd())?; - set_cloexec(write_fd.as_raw_fd())?; - Ok((read_fd, write_fd)) -} - -#[cfg(unix)] -fn set_cloexec(fd: RawFd) -> io::Result<()> { - let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) }; - if flags == -1 { - return Err(io::Error::last_os_error()); - } - - if unsafe { libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC) } == -1 { - return Err(io::Error::last_os_error()); - } - - Ok(()) -} - #[cfg(unix)] fn target_identity(target_pid: libc::pid_t) -> io::Result { let process_group_id = unsafe { libc::getpgid(target_pid) }; @@ -260,159 +169,11 @@ fn process_group_matches_identity(target_pid: libc::pid_t, identity: TargetIdent result == 0 || io::Error::last_os_error().raw_os_error() == Some(libc::EPERM) } -#[cfg(unix)] -fn close_fd(fd: RawFd) { - if fd >= 0 { - let _ = unsafe { libc::close(fd) }; - } -} - -#[cfg(unix)] -fn close_inherited_fds(pipe_read_fd: RawFd, target_exit_fd: Option) { - let max_fd = unsafe { libc::getdtablesize() }; - let max_fd = if max_fd > 0 { max_fd } else { 1024 }; - - for fd in 0..max_fd { - let fd = fd as RawFd; - if fd == pipe_read_fd || Some(fd) == target_exit_fd { - continue; - } - close_fd(fd); - } -} - #[cfg(unix)] fn signal_process_group(process_group_id: libc::pid_t, signal: libc::c_int) { let _ = unsafe { libc::kill(-process_group_id, signal) }; } -#[cfg(unix)] -fn sleep_unchecked(duration: Duration) { - let mut remaining = libc::timespec { - tv_sec: duration.as_secs() as libc::time_t, - tv_nsec: duration.subsec_nanos() as libc::c_long, - }; - - loop { - let mut next = libc::timespec { - tv_sec: 0, - tv_nsec: 0, - }; - let result = unsafe { libc::nanosleep(&remaining, &mut next) }; - if result == 0 { - break; - } - - if io::Error::last_os_error().raw_os_error() != Some(libc::EINTR) { - break; - } - remaining = next; - } -} - -#[cfg(unix)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum ParentDeathWatchdogEvent { - Disarmed, - ParentDied, - Error, -} - -#[cfg(unix)] -fn wait_for_parent_death_or_disarm(pipe_read_fd: RawFd) -> ParentDeathWatchdogEvent { - loop { - let mut fd = libc::pollfd { - fd: pipe_read_fd, - events: libc::POLLIN | libc::POLLHUP, - revents: 0, - }; - - let poll_result = unsafe { libc::poll(&mut fd, 1, -1) }; - if poll_result == -1 { - if io::Error::last_os_error().raw_os_error() == Some(libc::EINTR) { - continue; - } - return ParentDeathWatchdogEvent::Error; - } - - if fd.revents == 0 { - continue; - } - - let mut byte = 0_u8; - let read_result = unsafe { libc::read(pipe_read_fd, (&mut byte as *mut u8).cast(), 1) }; - if read_result > 0 { - return ParentDeathWatchdogEvent::Disarmed; - } - if read_result == 0 { - return ParentDeathWatchdogEvent::ParentDied; - } - if io::Error::last_os_error().raw_os_error() == Some(libc::EINTR) { - continue; - } - return ParentDeathWatchdogEvent::Error; - } -} - -#[cfg(unix)] -fn run_parent_death_watchdog( - pipe_read_fd: RawFd, - target_pid: libc::pid_t, - identity: TargetIdentity, -) -> ! { - // The watchdog must not keep unrelated task pipes open. - close_inherited_fds(pipe_read_fd, None); - let event = wait_for_parent_death_or_disarm(pipe_read_fd); - close_fd(pipe_read_fd); - - if event == ParentDeathWatchdogEvent::ParentDied - && process_group_matches_identity(target_pid, identity) - { - signal_process_group(identity.process_group_id, libc::SIGTERM); - sleep_unchecked(PARENT_DEATH_ESCALATION_DELAY); - if process_group_matches_identity(target_pid, identity) { - signal_process_group(identity.process_group_id, libc::SIGKILL); - } - } - - unsafe { libc::_exit(0) } -} - -#[cfg(unix)] -fn spawn_parent_death_watchdog( - target_pid: libc::pid_t, - read_fd: OwnedFd, -) -> io::Result { - let identity = target_identity(target_pid)?; - let read_fd = read_fd.into_raw_fd(); - - match unsafe { libc::fork() } { - -1 => { - let err = io::Error::last_os_error(); - close_fd(read_fd); - Err(err) - } - 0 => run_parent_death_watchdog(read_fd, target_pid, identity), - watchdog_pid => { - close_fd(read_fd); - Ok(watchdog_pid) - } - } -} - -#[cfg(unix)] -fn setup_parent_death_guard(pid: Option) -> Option { - pid.and_then( - |pid| match ParentDeathGuard::spawn_for_pid(pid as libc::pid_t) { - Ok(parent_death_guard) => Some(parent_death_guard), - Err(err) => { - debug!("failed to set up parent-death guard for process {pid}: {err}"); - None - } - }, - ) -} - #[cfg(unix)] fn capture_target_identity(pid: Option) -> Option { pid.and_then(|pid| match target_identity(pid as libc::pid_t) { @@ -440,9 +201,6 @@ impl ChildHandle { #[cfg(unix)] let target_identity = capture_target_identity(pid); - #[cfg(unix)] - let parent_death_guard = setup_parent_death_guard(pid); - #[cfg(windows)] let job = pid.and_then(|pid| { super::job_object::JobObject::new() @@ -469,8 +227,6 @@ impl ChildHandle { shutdown_semantics: ShutdownSemantics::process_group(), #[cfg(unix)] target_identity, - #[cfg(unix)] - parent_death_guard, #[cfg(windows)] _job: job, }, @@ -536,9 +292,6 @@ impl ChildHandle { #[cfg(unix)] let target_identity = capture_target_identity(pid); - #[cfg(unix)] - let parent_death_guard = setup_parent_death_guard(pid); - #[cfg(windows)] let job = pid.and_then(|pid| { super::job_object::JobObject::new() @@ -579,8 +332,6 @@ impl ChildHandle { shutdown_semantics: ShutdownSemantics::direct_child_then_wait_for_process_group(), #[cfg(unix)] target_identity, - #[cfg(unix)] - parent_death_guard, #[cfg(windows)] _job: job, }, @@ -761,13 +512,6 @@ impl ChildHandle { } } } - - #[cfg(unix)] - fn disarm_parent_death_guard(&mut self) { - if let Some(parent_death_guard) = &mut self.parent_death_guard { - parent_death_guard.disarm(); - } - } } struct SpawnResult { @@ -825,6 +569,9 @@ impl ShutdownStyle { child: &mut ChildHandle, command_rx: &mut mpsc::Receiver, ) -> ChildExit { + #[cfg(windows)] + let _ = &command_rx; + match self { // Windows doesn't give the ability to send a signal to a process so we // can't make use of the graceful shutdown timeout. @@ -1034,7 +781,7 @@ impl Child { } status = child.wait() => { drop(controller); - manager.handle_child_exit(status, &mut child).await; + manager.handle_child_exit(status).await; } } @@ -1346,15 +1093,13 @@ impl ChildStateManager { ShutdownStyle::Kill.process(child, command_rx).await } }; - #[cfg(unix)] - child.disarm_parent_death_guard(); // ignore the send error, failure means the channel is dropped trace!("sending child exit after shutdown"); self.exit_tx.send(Some(exit)).ok(); drop(controller); } - async fn handle_child_exit(&self, status: io::Result>, child: &mut ChildHandle) { + async fn handle_child_exit(&self, status: io::Result>) { // If a shutdown was initiated we defer to the exit returned by // `ShutdownStyle::process` as that will have information if the child // responded to a SIGINT or a SIGKILL. The `wait` response this function @@ -1365,8 +1110,6 @@ impl ChildStateManager { } debug!("child process exited normally"); - #[cfg(unix)] - child.disarm_parent_death_guard(); // the child process exited let child_exit = match status { Ok(Some(c)) => ChildExit::Finished(Some(c)), @@ -1394,7 +1137,6 @@ impl Child { mod test { use std::{ assert_matches, io, - process::Stdio, sync::{Arc, Mutex}, time::Duration, }; @@ -1402,15 +1144,12 @@ mod test { use futures::{StreamExt, stream::FuturesUnordered}; use test_case::test_case; use tokio::{ - io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader}, - process::Command as TokioCommand, + io::{AsyncReadExt, AsyncWriteExt}, sync::oneshot, }; use tracing_test::traced_test; use turbopath::AbsoluteSystemPathBuf; - #[cfg(unix)] - use super::ParentDeathGuard; use super::{Child, ChildInput, ChildOutput, Command}; use crate::{ PtySize, @@ -1471,78 +1210,6 @@ mod test { root.join_components(&["crates", "turborepo-process", "test", "scripts"]) } - #[cfg(unix)] - async fn spawn_parent_death_target() -> (tokio::process::Child, libc::pid_t, libc::pid_t) { - let script = find_script_dir().join_component("spawn_child_sleep.js"); - let mut command = TokioCommand::new("node"); - command - .arg(script.as_std_path()) - .stdout(Stdio::piped()) - .stderr(Stdio::null()) - .stdin(Stdio::null()) - .process_group(0); - - let mut child = command.spawn().unwrap(); - let child_pid = child.id().expect("child should have a pid") as libc::pid_t; - let stdout = child.stdout.take().expect("child should have stdout"); - let mut stdout = BufReader::new(stdout); - let mut line = String::new(); - tokio::time::timeout(Duration::from_secs(2), stdout.read_line(&mut line)) - .await - .expect("timed out waiting for child pid") - .expect("failed to read child pid from stdout"); - - let grandchild_pid = line - .trim() - .strip_prefix("CHILD_PID=") - .expect("child pid output should be prefixed") - .parse::() - .expect("child pid should parse"); - - (child, child_pid, grandchild_pid) - } - - #[cfg(unix)] - async fn spawn_term_ignoring_parent_death_target() - -> (tokio::process::Child, libc::pid_t, libc::pid_t) { - let mut command = TokioCommand::new("sh"); - command - .args([ - "-c", - "trap '' TERM; sh -c \"trap '' TERM; while true; do sleep 0.2; done\" & \ - CHILD_PID=$!; echo CHILD_PID=$CHILD_PID; while true; do sleep 0.2; done", - ]) - .stdout(Stdio::piped()) - .stderr(Stdio::null()) - .stdin(Stdio::null()) - .process_group(0); - - let mut child = command.spawn().unwrap(); - let child_pid = child.id().expect("child should have a pid") as libc::pid_t; - let stdout = child.stdout.take().expect("child should have stdout"); - let mut stdout = BufReader::new(stdout); - let mut line = String::new(); - tokio::time::timeout(Duration::from_secs(2), stdout.read_line(&mut line)) - .await - .expect("timed out waiting for child pid") - .expect("failed to read child pid from stdout"); - - let grandchild_pid = line - .trim() - .strip_prefix("CHILD_PID=") - .expect("child pid output should be prefixed") - .parse::() - .expect("child pid should parse"); - - (child, child_pid, grandchild_pid) - } - - #[cfg(unix)] - fn process_exists(pid: libc::pid_t) -> bool { - let result = unsafe { libc::kill(pid, 0) }; - result == 0 || io::Error::last_os_error().raw_os_error() == Some(libc::EPERM) - } - #[test_case(false)] #[test_case(TEST_PTY)] #[tokio::test] @@ -2179,72 +1846,6 @@ mod test { assert_eq!(shutdown.await.unwrap(), Some(ChildExit::Killed)); } - #[cfg(unix)] - #[tokio::test] - async fn test_parent_death_guard_drop_kills_process_group() { - let (mut child, child_pid, grandchild_pid) = spawn_parent_death_target().await; - let guard = ParentDeathGuard::spawn_for_pid(child_pid).unwrap(); - drop(guard); - - tokio::time::timeout(Duration::from_secs(5), child.wait()) - .await - .expect("timed out waiting for watchdog to kill child") - .expect("failed waiting for child process"); - - tokio::time::sleep(Duration::from_millis(100)).await; - assert!( - !process_exists(grandchild_pid), - "watchdog should kill the entire child process group" - ); - } - - #[cfg(unix)] - #[tokio::test] - async fn test_parent_death_guard_disarm_keeps_process_group_alive() { - let (mut child, child_pid, grandchild_pid) = spawn_parent_death_target().await; - let mut guard = ParentDeathGuard::spawn_for_pid(child_pid).unwrap(); - guard.disarm(); - - tokio::time::sleep(Duration::from_millis(100)).await; - - assert!( - process_exists(child_pid), - "child should still be alive after disarm" - ); - assert!( - process_exists(grandchild_pid), - "grandchild should still be alive after disarm" - ); - - unsafe { - libc::kill(-child_pid, libc::SIGKILL); - } - tokio::time::timeout(Duration::from_secs(5), child.wait()) - .await - .expect("timed out waiting for cleanup after SIGKILL") - .expect("failed waiting for child process"); - } - - #[cfg(unix)] - #[tokio::test] - async fn test_parent_death_guard_escalates_after_sigterm() { - let (mut child, child_pid, grandchild_pid) = - spawn_term_ignoring_parent_death_target().await; - let guard = ParentDeathGuard::spawn_for_pid(child_pid).unwrap(); - drop(guard); - - tokio::time::timeout(Duration::from_secs(5), child.wait()) - .await - .expect("timed out waiting for watchdog escalation") - .expect("failed waiting for child process"); - - tokio::time::sleep(Duration::from_millis(100)).await; - assert!( - !process_exists(grandchild_pid), - "watchdog should escalate to SIGKILL for TERM-ignoring process trees" - ); - } - #[test_case(false)] #[test_case(TEST_PTY)] #[tokio::test] diff --git a/crates/turborepo-process/src/lib.rs b/crates/turborepo-process/src/lib.rs index 4ed5d0a2d34e0..8d6057a6b642e 100644 --- a/crates/turborepo-process/src/lib.rs +++ b/crates/turborepo-process/src/lib.rs @@ -299,7 +299,7 @@ impl Default for PtySize { #[cfg(test)] mod test { - use std::time::Instant; + use std::{fs, time::Instant}; use futures::{StreamExt, stream::FuturesUnordered}; use test_case::test_case; @@ -322,6 +322,24 @@ mod test { TaskId::new("test-pkg", "test-task") } + #[cfg(unix)] + fn process_exists(pid: libc::pid_t) -> bool { + let result = unsafe { libc::kill(pid, 0) }; + result == 0 || std::io::Error::last_os_error().raw_os_error() == Some(libc::EPERM) + } + + #[cfg(unix)] + async fn wait_for_process_to_exit(pid: libc::pid_t) { + for _ in 0..50 { + if !process_exists(pid) { + return; + } + sleep(Duration::from_millis(100)).await; + } + + panic!("process {pid} should have exited"); + } + const STOPPED_EXIT: Option = Some(if cfg!(windows) { ChildExit::Killed } else { @@ -625,4 +643,59 @@ mod test { assert_eq!(child.wait().await, Some(ChildExit::Killed)); } + + #[cfg(unix)] + #[tokio::test] + async fn test_shutdown_waits_for_process_group_after_child_exit() { + let manager = ProcessManager::new(false); + let pid_file = + std::env::temp_dir().join(format!("turbo-process-grandchild-{}", std::process::id())); + let _ = fs::remove_file(&pid_file); + + let script = format!( + "trap 'exit 0' INT; sh -c 'trap \"\" INT TERM; while true; do sleep 0.2; done' & echo \ + $! > {}; while true; do sleep 0.2; done", + pid_file.display() + ); + let mut command = Command::new("sh"); + command.args(["-c", &script]); + let mut child = manager + .spawn(command, Duration::from_secs(30), test_task_id()) + .unwrap() + .unwrap(); + + let grandchild_pid = loop { + if let Ok(contents) = fs::read_to_string(&pid_file) + && let Ok(pid) = contents.trim().parse::() + { + break pid; + } + sleep(Duration::from_millis(50)).await; + }; + + let shutdown_manager = manager.clone(); + let shutdown = tokio::spawn(async move { + shutdown_manager.shutdown(None).await; + }); + + sleep(Duration::from_millis(500)).await; + assert!( + !shutdown.is_finished(), + "shutdown should wait for the still-running process group" + ); + assert!( + process_exists(grandchild_pid), + "grandchild should still be running before force kill" + ); + + manager.kill_all().await; + tokio::time::timeout(Duration::from_secs(5), shutdown) + .await + .expect("shutdown should finish after force kill") + .expect("shutdown task should not panic"); + assert_eq!(child.wait().await, Some(ChildExit::Killed)); + wait_for_process_to_exit(grandchild_pid).await; + + let _ = fs::remove_file(pid_file); + } } diff --git a/crates/turborepo/ARCHITECTURE.md b/crates/turborepo/ARCHITECTURE.md index 2453c89066a68..3b2c93c2a4da9 100644 --- a/crates/turborepo/ARCHITECTURE.md +++ b/crates/turborepo/ARCHITECTURE.md @@ -25,6 +25,11 @@ A run consists of the following steps: ### Signal-Driven Shutdown +Graceful shutdown and parent-death cleanup are separate responsibilities. +Graceful shutdown happens while the Turbo process is still alive, so it should +be handled internally by the run and process manager. Parent-death cleanup only +applies when Turbo disappears before Rust cleanup code can run. + - `crates/turborepo-lib/src/commands/run.rs` creates a shared `SignalHandler` and does not return until all shutdown subscribers finish their cleanup work. @@ -37,8 +42,12 @@ A run consists of the following steps: - Task processes are spawned into dedicated process groups so Turbo can signal a task and all of its descendants together. - On the first `SIGINT`/`SIGTERM`, Turbo enters graceful shutdown: it prints a - shutdown message, forwards `SIGINT` to running tasks, and waits for them to - exit. + shutdown message, forwards `SIGINT` to running tasks, and waits for their + process groups to exit. +- Turbo must not treat direct-child exit as task-tree exit. Package managers, + shells, and watch commands can leave descendants running after the leader + exits, so the process manager should track the process targets it spawned and + keep Turbo alive until all tracked process groups are gone. - Close-driven shutdown still flushes cache writes and stops processes, but it does not arm signal-specific force-shutdown timers. - If tasks are still running after 3 seconds, Turbo prints the remaining task @@ -50,13 +59,35 @@ A run consists of the following steps: - On Windows, graceful shutdown falls back to an immediate kill because the platform does not support Unix-style signal forwarding to task process groups. -- On Unix, Turbo also starts a small best-effort parent-death watchdog for each - task process. The watchdog waits on a pipe from the Turbo parent process and, - where supported, also monitors the task leader for exit so it does not signal - a recycled PID. If Turbo disappears unexpectedly, the watchdog first sends - `SIGTERM` to the task process group and then escalates to `SIGKILL` if - anything is still running. On Windows, job objects provide the same - parent-death cleanup behavior. + +Parent-death cleanup is not part of normal graceful shutdown. An in-process map +cannot help after `SIGKILL`, a crash, or OOM because the map dies with Turbo. +Turbo should not start a per-task Unix watchdog for this case. If abnormal +cleanup is required later, prefer a bounded run-level mechanism: + +- A run-level reaper, if used, should be owned by `ProcessManager` and shared by + all tasks in the run. +- Tasks register their process target (`pid`, `pgid`, and session identity) when + spawned and unregister on normal exit or Turbo-managed shutdown. +- If the Turbo process disappears and the control pipe reaches EOF, the reaper + can signal the remaining registered process groups and escalate if needed. +- Linux can use `prctl(PR_SET_PDEATHSIG)` as a best-effort no-helper option, but + it only signals the direct child and cannot provide delayed escalation. +- Windows should continue using job objects for parent-death cleanup. + +Regression coverage for shutdown changes should focus on observable lifecycle +behavior: + +- A direct child exiting during shutdown must not let Turbo exit while a tracked + descendant process group is still alive. +- Graceful shutdown must wait for all tracked process groups, not just all + direct children. +- Forced shutdown must kill stubborn descendants and clear the tracked task + records. +- End-to-end `turbo run` signal tests should assert that descendants are not + leaked after force shutdown. +- Existing final-output coverage should continue proving that shutdown keeps the + UI and log pipeline alive long enough to drain task output. ### 1. Run Builder (`crates/turborepo-lib/src/run/builder.rs`) diff --git a/crates/turborepo/tests/graceful_shutdown_test.rs b/crates/turborepo/tests/graceful_shutdown_test.rs index 7cf2369c919f7..a11ae2f2508b9 100644 --- a/crates/turborepo/tests/graceful_shutdown_test.rs +++ b/crates/turborepo/tests/graceful_shutdown_test.rs @@ -667,33 +667,4 @@ while true; do sleep 0.2 || true; done "expected auto-force banner after timeout\n{combined}" ); } - - #[test] - fn run_kills_task_tree_when_turbo_is_sigkilled() { - let (_tempdir, test_dir) = setup_shutdown_example( - "orphanable.sh", - r#"#!/usr/bin/env bash -set -u -sh -c 'trap "" TERM INT; while true; do sleep 0.2 || true; done' & -child=$! -printf '%s\n' "$child" > child.pid -printf "orphanable ready child=%s\n" "$child" -: > ready -while true; do sleep 0.2 || true; done -"#, - ); - - let ready_file = test_dir.join("apps/app-a/ready"); - let child_pid_file = test_dir.join("apps/app-a/child.pid"); - - let mut child = spawn_noninteractive_turbo(&test_dir); - wait_for_path(&ready_file, START_TIMEOUT); - let task_child_pid = wait_for_pid_file(&child_pid_file, START_TIMEOUT); - - send_signal(child.child_mut().id() as i32, Signal::SIGKILL); - let _status = wait_for_process_exit(child.child_mut(), Duration::from_secs(5)); - let _ = child.child.take(); - - wait_for_process_gone(task_child_pid, Duration::from_secs(5)); - } }