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:
@@ -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 || {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user