fix(natives): stopped detached internal shell jobs

Abort shell-internal background tasks when their owning session is dropped, and propagate task abortion into blocking utility cancellation so infinite writers stop.

Fixes #8341
This commit is contained in:
roboomp
2026-08-12 15:13:28 +00:00
parent 06aecdd51f
commit 110f3aac9d
3 changed files with 49 additions and 2 deletions
+9
View File
@@ -101,6 +101,14 @@ pub(crate) struct Host {
stdin_is_search_input: bool,
}
struct CancelOnDrop(Arc<AtomicBool>);
impl Drop for CancelOnDrop {
fn drop(&mut self) {
self.0.store(true, Ordering::Relaxed);
}
}
impl Host {
/// The name the utility was invoked as. Differs from [`Utility::NAME`] when
/// one implementation backs several builtins (`grep` and `rg`).
@@ -578,6 +586,7 @@ async fn run_utility<U: Utility, SE: ShellExtensions>(
let mut host = build_host(&context, U::NAME)?;
let cancel = context.cancel_token();
let cancel_flag = host.cancel_flag();
let _cancel_on_drop = CancelOnDrop(Arc::clone(&cancel_flag));
drop(context);
let mut handle = tokio::task::spawn_blocking(move || {
+36 -2
View File
@@ -38,6 +38,12 @@ struct ShellSessionCore {
shell: BrushShell,
}
impl Drop for ShellSessionCore {
fn drop(&mut self) {
terminate_internal_background_jobs(&mut self.shell);
}
}
#[derive(Clone, Default)]
struct ShellAbortState(Arc<TokioMutex<Option<AbortToken>>>);
@@ -1408,10 +1414,16 @@ async fn terminate_run(registry: &process::SpawnRegistry) {
}
}
}
fn terminate_background_jobs(shell: &mut BrushShell) {
let mut targets = process::TerminationTargets::new();
fn terminate_internal_background_jobs(shell: &mut BrushShell) {
for job in &mut shell.jobs_mut().jobs {
job.abort_internal_tasks();
}
}
fn terminate_background_jobs(shell: &mut BrushShell) {
let mut targets = process::TerminationTargets::new();
terminate_internal_background_jobs(shell);
for job in &shell.jobs().jobs {
if let Some(pgid) = job.process_group_id() {
targets.add_pgid(pgid);
}
@@ -4432,6 +4444,28 @@ replace = [{ pattern = "hello", replacement = "HI" }]
}
}
#[tokio::test(flavor = "multi_thread")]
async fn one_shot_completion_aborts_internal_background_jobs() {
let marker = tempfile::NamedTempFile::new().expect("marker file");
let marker_path = marker.path().to_string_lossy();
std::fs::remove_file(marker.path()).expect("remove initial marker");
let command = format!("{{ sleep 1; echo leaked > {}; }} &", quote_arg(&marker_path));
execute_shell(
ShellExecuteOptions { command, ..Default::default() },
None,
CancelToken::default(),
)
.await
.expect("one-shot shell execution");
time::sleep(Duration::from_millis(100)).await;
assert!(
!marker.path().exists(),
"an internal background job outlived its one-shot shell session"
);
}
/// `live_background_job_count` reports 0 when the session has no live
/// external background jobs and 1 while one is running. The host relies on
/// this to retain a per-call shell whose `&`/`nohup` child is still alive