Skip to content

fix(task): use process groups to kill child process trees on Unix#8279

Merged
jdx merged 1 commit intomainfrom
fix/task-process-group-kill
Feb 21, 2026
Merged

fix(task): use process groups to kill child process trees on Unix#8279
jdx merged 1 commit intomainfrom
fix/task-process-group-kill

Conversation

@jdx
Copy link
Owner

@jdx jdx commented Feb 21, 2026

Summary

  • Place each task child process in its own process group via setpgid in pre_exec, then use killpg (with kill fallback) in kill_all() and signal forwarding
  • Ensures grandchild processes (e.g., npm start spawning webpack) are properly killed on Ctrl+C, timeout, or task failure instead of being orphaned
  • Removes SIGINT forwarding guard since children in their own process group no longer receive terminal SIGINT directly

Test plan

  • cargo build compiles cleanly
  • mise run lint passes
  • mise run test:unit — all 481 tests pass
  • mise run test:e2e test_task_timeout — timeout tests pass

🤖 Generated with Claude Code


Note

Medium Risk
Changes Unix process spawning and signal propagation behavior; mistakes could cause under/over-killing processes or leave tasks running, but the change is scoped and has fallbacks.

Overview
On Unix, spawned task commands are now placed into their own process group via setpgid in CmdLineRunner::execute, and termination logic targets the process group using killpg (with a kill fallback).

This updates CmdLineRunner::kill_all and signal forwarding to operate on the process group ID, ensuring grandchild processes are terminated on Ctrl+C, timeouts, or task failure rather than being left orphaned.

Written by Cursor Bugbot for commit 46765f9. This will update automatically on new commits. Configure here.

Place each task child process in its own process group via setpgid in
pre_exec, then use killpg (with kill fallback) in kill_all and signal
forwarding. This ensures grandchild processes (e.g., npm start spawning
webpack) are properly killed on Ctrl+C, timeout, or task failure instead
of being orphaned.

Also removes the SIGINT forwarding guard since children in their own
process group no longer receive terminal SIGINT directly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 21, 2026 15:44
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @jdx, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a critical issue where grandchild processes were not being properly terminated, leading to orphaned processes. By leveraging Unix process groups, the changes ensure that when a parent task is killed due to a timeout, Ctrl+C, or failure, its entire tree of child and grandchild processes is reliably terminated. This significantly improves the stability and resource management of task execution.

Highlights

  • Process Group Management: Implemented the use of process groups for child processes on Unix systems to ensure robust termination of process trees.
  • Child Process Termination: Modified signal handling in kill_all and ChildProcessOutput::Signal to use killpg for terminating entire process groups, with a fallback to kill for individual processes.
  • Pre-execution Hook: Utilized pre_exec to call setpgid for each spawned child process, placing it into its own process group.
  • SIGINT Forwarding Guard Removal: Removed the specific guard for SIGINT forwarding, as process group management now handles signal propagation more effectively.
Changelog
  • src/cmd.rs
    • Added CommandExt import for Unix-specific process control functionalities.
    • Modified the kill_all function to attempt killpg for process group termination before falling back to kill for individual processes.
    • Introduced a pre_exec hook to set the process group ID (setpgid) for new child processes on Unix.
    • Updated the ChildProcessOutput::Signal handling to forward signals to process groups via killpg and removed the SIGINT exclusion.
Activity
  • The author jdx has confirmed that cargo build compiles cleanly.
  • The author jdx has verified that mise run lint passes.
  • The author jdx has ensured all 481 unit tests pass with mise run test:unit.
  • The author jdx has confirmed that timeout tests pass with mise run test:e2e test_task_timeout.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request correctly implements process groups to ensure that child and grandchild processes are properly terminated, which is a solid improvement for task management on Unix. I have a couple of suggestions to enhance error handling and maintain consistency. One is a critical fix to propagate errors from setpgid to ensure the feature's reliability. The other is a minor change to log errors on the fallback kill for better debugging and consistency.

Comment on lines +312 to +315
let _ = nix::unistd::setpgid(
nix::unistd::Pid::from_raw(0),
nix::unistd::Pid::from_raw(0),
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The result of nix::unistd::setpgid should be handled. If setpgid fails, the child process will not be in a new process group, and killpg will not work as intended. This would silently defeat the purpose of this pull request. By propagating the error, you ensure that spawn will fail if the process group cannot be set.

Suggested change
let _ = nix::unistd::setpgid(
nix::unistd::Pid::from_raw(0),
nix::unistd::Pid::from_raw(0),
);
nix::unistd::setpgid(
nix::unistd::Pid::from_raw(0),
nix::unistd::Pid::from_raw(0),
)?;

let pgid = nix::unistd::Pid::from_raw(id as i32);
let sig = nix::sys::signal::Signal::try_from(sig).unwrap();
if nix::sys::signal::killpg(pgid, sig).is_err() {
let _ = nix::sys::signal::kill(pgid, sig);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The error from the fallback kill call is currently ignored. This is inconsistent with the implementation in kill_all and the previous behavior which propagated errors. For better debugging and consistency, it's advisable to log this error if it occurs.

                        if let Err(e) = nix::sys::signal::kill(pgid, sig) {
                            debug!("Failed to kill cmd {id}: {e}");
                        }

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a critical issue where child processes (especially nested processes like npm start spawning webpack) were not properly terminated when tasks were interrupted, timed out, or failed, resulting in orphaned processes.

Changes:

  • Adds process group management on Unix systems to ensure all descendant processes are killed together
  • Updates signal handling to forward all signals (including SIGINT) to child process groups
  • Implements graceful fallback from process group signals to individual process signals

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@@ -383,11 +400,11 @@ impl<'a> CmdLineRunner<'a> {
}
#[cfg(not(any(test, windows)))]
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cfg attribute should use target_os = "windows" instead of just windows to match the pattern used consistently throughout this file (lines 18, 20, 360, 362, 376, 571) and to match the enum variant definition of ChildProcessOutput::Signal at line 571.

Suggested change
#[cfg(not(any(test, windows)))]
#[cfg(not(any(test, target_os = "windows")))]

Copilot uses AI. Check for mistakes.
Comment on lines +312 to +315
let _ = nix::unistd::setpgid(
nix::unistd::Pid::from_raw(0),
nix::unistd::Pid::from_raw(0),
);
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silently ignoring errors from setpgid could lead to situations where the process group setup fails without any indication. If setpgid fails, the child process won't be in its own process group, which means killpg operations will fail and the intended behavior of killing entire process trees won't work. Consider logging a warning or trace message when setpgid fails to aid debugging, while still allowing execution to continue as a fallback.

Suggested change
let _ = nix::unistd::setpgid(
nix::unistd::Pid::from_raw(0),
nix::unistd::Pid::from_raw(0),
);
if let Err(e) = nix::unistd::setpgid(
nix::unistd::Pid::from_raw(0),
nix::unistd::Pid::from_raw(0),
) {
warn!("Failed to set process group with setpgid: {e}");
}

Copilot uses AI. Check for mistakes.
let pgid = nix::unistd::Pid::from_raw(id as i32);
let sig = nix::sys::signal::Signal::try_from(sig).unwrap();
if nix::sys::signal::killpg(pgid, sig).is_err() {
let _ = nix::sys::signal::kill(pgid, sig);
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error from the fallback kill operation is silently ignored. For consistency with the kill_all function (lines 147-149), consider logging a debug or trace message when the fallback kill fails, similar to how kill_all logs "Failed to kill cmd" messages. This aids in debugging situations where signal forwarding fails.

Suggested change
let _ = nix::sys::signal::kill(pgid, sig);
if let Err(e) = nix::sys::signal::kill(pgid, sig) {
debug!("Failed to send fallback signal {sig} to pid {pgid}: {e}");
}

Copilot uses AI. Check for mistakes.
@jdx jdx enabled auto-merge (squash) February 21, 2026 15:53
@jdx jdx merged commit 8c40c38 into main Feb 21, 2026
41 checks passed
@jdx jdx deleted the fix/task-process-group-kill branch February 21, 2026 15:58
@github-actions
Copy link

Hyperfine Performance

mise x -- echo

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.2.17 x -- echo 22.6 ± 1.1 21.6 37.6 1.00
mise x -- echo 23.0 ± 0.6 21.9 26.4 1.02 ± 0.06

mise env

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.2.17 env 22.6 ± 0.9 21.4 31.0 1.00 ± 0.05
mise env 22.6 ± 0.6 21.4 25.2 1.00

mise hook-env

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.2.17 hook-env 22.6 ± 0.6 21.9 29.4 1.00
mise hook-env 22.7 ± 0.3 22.1 25.5 1.01 ± 0.03

mise ls

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.2.17 ls 20.6 ± 0.5 19.7 23.1 1.00
mise ls 21.2 ± 0.6 20.0 23.0 1.03 ± 0.04

xtasks/test/perf

Command mise-2026.2.17 mise Variance
install (cached) 129ms 127ms +1%
ls (cached) 79ms 79ms +0%
bin-paths (cached) 84ms 83ms +1%
task-ls (cached) 820ms 819ms +0%

jdx pushed a commit that referenced this pull request Feb 21, 2026
### 🚀 Features

- **(install)** auto-lock all platforms after tool installation by @jdx
in [#8277](#8277)

### 🐛 Bug Fixes

- **(config)** respect --yes flag for config trust prompts by @jdx in
[#8288](#8288)
- **(exec)** strip shims from PATH on Unix to prevent infinite recursion
by @jdx in [#8276](#8276)
- **(install)** validate --locked before --dry-run short-circuit by
@altendky in [#8290](#8290)
- **(release)** refresh PATH after mise up in release-plz by @jdx in
[#8292](#8292)
- **(schema)** replace unevaluatedProperties with additionalProperties
by @jdx in [#8285](#8285)
- **(task)** avoid duplicated stderr on task failure in replacing mode
by @jdx in [#8275](#8275)
- **(task)** use process groups to kill child process trees on Unix by
@jdx in [#8279](#8279)
- **(task)** run depends_post tasks even when parent task fails by @jdx
in [#8274](#8274)
- **(task)** suggest similar commands when mistyping a CLI subcommand by
@jdx in [#8286](#8286)
- **(task)** execute monorepo subdirectory prepare steps from root by
@jdx in [#8291](#8291)
- **(upgrade)** don't force-reinstall already installed versions by @jdx
in [#8282](#8282)
- **(watch)** restore terminal state after watchexec exits by @jdx in
[#8273](#8273)

### 📚 Documentation

- clarify that MISE_CEILING_PATHS excludes the ceiling directory itself
by @jdx in [#8283](#8283)

### Chore

- replace gen-release-notes script with communique by @jdx in
[#8289](#8289)

### New Contributors

- @altendky made their first contribution in
[#8290](#8290)

## 📦 Aqua Registry Updates

#### New Packages (4)

-
[`Skarlso/crd-to-sample-yaml`](https://github.com/Skarlso/crd-to-sample-yaml)
-
[`kunobi-ninja/kunobi-releases`](https://github.com/kunobi-ninja/kunobi-releases)
-
[`swanysimon/markdownlint-rs`](https://github.com/swanysimon/markdownlint-rs)
- [`tmux/tmux-builds`](https://github.com/tmux/tmux-builds)

#### Updated Packages (2)

-
[`firecow/gitlab-ci-local`](https://github.com/firecow/gitlab-ci-local)
- [`k1LoW/runn`](https://github.com/k1LoW/runn)
netbsd-srcmastr pushed a commit to NetBSD/pkgsrc that referenced this pull request Feb 22, 2026
## [2026.2.18](https://github.com/jdx/mise/compare/v2026.2.17..v2026.2.18) - 2026-02-21

### 🚀 Features

- **(install)** auto-lock all platforms after tool installation by @jdx in [#8277](jdx/mise#8277)

### 🐛 Bug Fixes

- **(config)** respect --yes flag for config trust prompts by @jdx in [#8288](jdx/mise#8288)
- **(exec)** strip shims from PATH on Unix to prevent infinite recursion by @jdx in [#8276](jdx/mise#8276)
- **(install)** validate --locked before --dry-run short-circuit by @altendky in [#8290](jdx/mise#8290)
- **(release)** refresh PATH after mise up in release-plz by @jdx in [#8292](jdx/mise#8292)
- **(schema)** replace unevaluatedProperties with additionalProperties by @jdx in [#8285](jdx/mise#8285)
- **(task)** avoid duplicated stderr on task failure in replacing mode by @jdx in [#8275](jdx/mise#8275)
- **(task)** use process groups to kill child process trees on Unix by @jdx in [#8279](jdx/mise#8279)
- **(task)** run depends_post tasks even when parent task fails by @jdx in [#8274](jdx/mise#8274)
- **(task)** suggest similar commands when mistyping a CLI subcommand by @jdx in [#8286](jdx/mise#8286)
- **(task)** execute monorepo subdirectory prepare steps from root by @jdx in [#8291](jdx/mise#8291)
- **(upgrade)** don't force-reinstall already installed versions by @jdx in [#8282](jdx/mise#8282)
- **(watch)** restore terminal state after watchexec exits by @jdx in [#8273](jdx/mise#8273)

### 📚 Documentation

- clarify that MISE_CEILING_PATHS excludes the ceiling directory itself by @jdx in [#8283](jdx/mise#8283)

### Chore

- replace gen-release-notes script with communique by @jdx in [#8289](jdx/mise#8289)

### New Contributors

- @altendky made their first contribution in [#8290](jdx/mise#8290)

### 📦 Aqua Registry Updates

#### New Packages (4)

- [`Skarlso/crd-to-sample-yaml`](https://github.com/Skarlso/crd-to-sample-yaml)
- [`kunobi-ninja/kunobi-releases`](https://github.com/kunobi-ninja/kunobi-releases)
- [`swanysimon/markdownlint-rs`](https://github.com/swanysimon/markdownlint-rs)
- [`tmux/tmux-builds`](https://github.com/tmux/tmux-builds)

#### Updated Packages (2)

- [`firecow/gitlab-ci-local`](https://github.com/firecow/gitlab-ci-local)
- [`k1LoW/runn`](https://github.com/k1LoW/runn)

## [2026.2.17](https://github.com/jdx/mise/compare/v2026.2.16..v2026.2.17) - 2026-02-19

### 🚀 Features

- **(prepare)** update mtime of outputs after command is run by @halms in [#8243](jdx/mise#8243)

### 🐛 Bug Fixes

- **(install)** use backend bin paths for per-tool postinstall hooks by @jdx in [#8234](jdx/mise#8234)
- **(use)** write to config.toml instead of config.local.toml by @jdx in [#8240](jdx/mise#8240)
- default legacy .mise.backend installs to non-explicit by @jean-humann in [#8245](jdx/mise#8245)

### 🚜 Refactor

- **(config)** consolidate flat task_* settings into nested task.* by @jdx in [#8239](jdx/mise#8239)

### Chore

- **(prepare)** refactor common code into ProviderBase by @halms in [#8246](jdx/mise#8246)

### 📦 Aqua Registry Updates

#### Updated Packages (1)

- [`namespacelabs/foundation/nsc`](https://github.com/namespacelabs/foundation/nsc)
jdx added a commit that referenced this pull request Feb 22, 2026
## Summary

PR #8279 placed every task child process in its own process group via
`setpgid(0, 0)`. This broke interactive tools like Tilt that read from
stdin.

**Root cause:** The terminal driver only delivers keyboard input to the
*foreground process group*. When a child is moved to a new process
group, it's no longer the terminal's foreground group, so any attempt to
`read()` from stdin causes the kernel to send `SIGTTIN`, which silently
stops the process. Result: the process hangs, Ctrl-C has no effect, and
the UI never loads.

**Fix:** In `pre_exec`, check `std::io::stdin().is_terminal()`. If stdin
is a TTY (inherited from the terminal), skip `setpgid` — the process
stays in the terminal's foreground group and gets full keyboard I/O. If
stdin is not a TTY (null/piped, i.e. non-interactive tasks), `setpgid`
fires as before, preserving grandchild cleanup on Ctrl-C.

This keeps the grandchild-kill benefit of #8279 for all normal
(non-interactive) tasks while restoring correct behavior for interactive
ones.

Closes #8300

## Test plan
- [x] `cargo build` compiles cleanly
- [x] Verified `mise run tilt:up` (or equivalent interactive task) no
longer hangs
- [x] Non-interactive tasks still get their own process group for clean
teardown

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Small, Unix-only change gated on an `is_terminal` check; primary risk
is behavior change in process-group/signal handling for commands with
TTY stdin.
> 
> **Overview**
> Fixes Unix task execution for interactive commands by **only calling
`setpgid(0,0)` when stdin is not a TTY**.
> 
> In `CmdLineRunner::execute`’s `pre_exec` hook, stdin is checked via
`BorrowedFd::borrow_raw(0)` + `IsTerminal`; interactive children stay in
the terminal’s foreground process group to avoid `SIGTTIN` hangs, while
non-interactive tasks keep the separate process group behavior for
signal/cleanup handling.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
4bf5aef. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
jdx added a commit that referenced this pull request Feb 25, 2026
…mise tasks

Reverts the setpgid/killpg changes from #8279 and #8301. Placing each
child process in its own process group via setpgid(0,0) breaks external
tools (e.g. Playwright) that rely on process-group-based kills to tear
down process trees. When Playwright SIGKILLs the webServer process
group, grandchild processes in a separate group (created by inner mise's
setpgid) survive, holding pipes open and causing the parent to hang
indefinitely.

Fixes #8345

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
jdx added a commit that referenced this pull request Feb 25, 2026
…mise tasks (#8347)

## Summary
- Reverts the `setpgid`/`killpg` changes from #8279 and #8301
- Placing each child process in its own process group via `setpgid(0,0)`
breaks external tools (e.g. Playwright) that rely on process-group-based
kills to tear down process trees
- When Playwright `SIGKILL`s the webServer process group, grandchild
processes in a separate group (created by inner mise's `setpgid`)
survive, holding pipes open and causing the parent to hang indefinitely

## Root Cause

The process group change created a hierarchy where nested `mise run`
calls each placed their children in separate process groups:

```
mise run test  (pgid=terminal)
  └── sh -c "npm test"  (pgid=A, set by outer mise's setpgid)
      └── playwright
          └── mise run serve  (pgid=C, set by Playwright's detached:true)
              └── sh -c "npm run serve"  (pgid=D, set by inner mise's setpgid)
                  └── node main.js  (inherits pgid=D)
```

When Playwright kills pgid=C (`kill(-pid, SIGKILL)`), only processes in
that group die. The actual server in pgid=D survives because `SIGKILL`
cannot be caught/forwarded, and the surviving process holds pipe file
descriptors open, preventing EOF and causing the parent mise to hang
indefinitely.

Fixes #8345

## Test plan
- [x] `cargo build` compiles cleanly
- [x] `mise run lint` passes
- [x] `mise run test:unit` — all 491 tests pass
- [x] `mise run test:e2e test_task_timeout` — timeout tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Moderate risk because it changes how signals/timeouts propagate to
subprocess trees on Unix, which can affect cleanup and
Ctrl-C/termination semantics for running tasks.
> 
> **Overview**
> Stops creating per-command process groups on Unix by removing the
`pre_exec` `setpgid` hook, restoring the prior behavior where child
commands stay in the caller’s process group.
> 
> Replaces all `killpg` usage with direct `kill` calls: timeout
termination (`SIGTERM` then `SIGKILL`), global shutdown via
`CmdLineRunner::kill_all`, and signal forwarding during `execute` now
target only the child PID (and it no longer forwards `SIGINT`).
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
e6ed47d. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
risu729 pushed a commit to risu729/mise that referenced this pull request Feb 27, 2026
…x#8279)

## Summary
- Place each task child process in its own process group via `setpgid`
in `pre_exec`, then use `killpg` (with `kill` fallback) in `kill_all()`
and signal forwarding
- Ensures grandchild processes (e.g., `npm start` spawning webpack) are
properly killed on Ctrl+C, timeout, or task failure instead of being
orphaned
- Removes SIGINT forwarding guard since children in their own process
group no longer receive terminal SIGINT directly

## Test plan
- [x] `cargo build` compiles cleanly
- [x] `mise run lint` passes
- [x] `mise run test:unit` — all 481 tests pass
- [x] `mise run test:e2e test_task_timeout` — timeout tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes Unix process spawning and signal propagation behavior;
mistakes could cause under/over-killing processes or leave tasks
running, but the change is scoped and has fallbacks.
> 
> **Overview**
> On Unix, spawned task commands are now placed into their own process
group via `setpgid` in `CmdLineRunner::execute`, and termination logic
targets the process group using `killpg` (with a `kill` fallback).
> 
> This updates `CmdLineRunner::kill_all` and signal forwarding to
operate on the process group ID, ensuring grandchild processes are
terminated on Ctrl+C, timeouts, or task failure rather than being left
orphaned.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
46765f9. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
risu729 pushed a commit to risu729/mise that referenced this pull request Feb 27, 2026
### 🚀 Features

- **(install)** auto-lock all platforms after tool installation by @jdx
in [jdx#8277](jdx#8277)

### 🐛 Bug Fixes

- **(config)** respect --yes flag for config trust prompts by @jdx in
[jdx#8288](jdx#8288)
- **(exec)** strip shims from PATH on Unix to prevent infinite recursion
by @jdx in [jdx#8276](jdx#8276)
- **(install)** validate --locked before --dry-run short-circuit by
@altendky in [jdx#8290](jdx#8290)
- **(release)** refresh PATH after mise up in release-plz by @jdx in
[jdx#8292](jdx#8292)
- **(schema)** replace unevaluatedProperties with additionalProperties
by @jdx in [jdx#8285](jdx#8285)
- **(task)** avoid duplicated stderr on task failure in replacing mode
by @jdx in [jdx#8275](jdx#8275)
- **(task)** use process groups to kill child process trees on Unix by
@jdx in [jdx#8279](jdx#8279)
- **(task)** run depends_post tasks even when parent task fails by @jdx
in [jdx#8274](jdx#8274)
- **(task)** suggest similar commands when mistyping a CLI subcommand by
@jdx in [jdx#8286](jdx#8286)
- **(task)** execute monorepo subdirectory prepare steps from root by
@jdx in [jdx#8291](jdx#8291)
- **(upgrade)** don't force-reinstall already installed versions by @jdx
in [jdx#8282](jdx#8282)
- **(watch)** restore terminal state after watchexec exits by @jdx in
[jdx#8273](jdx#8273)

### 📚 Documentation

- clarify that MISE_CEILING_PATHS excludes the ceiling directory itself
by @jdx in [jdx#8283](jdx#8283)

### Chore

- replace gen-release-notes script with communique by @jdx in
[jdx#8289](jdx#8289)

### New Contributors

- @altendky made their first contribution in
[jdx#8290](jdx#8290)

## 📦 Aqua Registry Updates

#### New Packages (4)

-
[`Skarlso/crd-to-sample-yaml`](https://github.com/Skarlso/crd-to-sample-yaml)
-
[`kunobi-ninja/kunobi-releases`](https://github.com/kunobi-ninja/kunobi-releases)
-
[`swanysimon/markdownlint-rs`](https://github.com/swanysimon/markdownlint-rs)
- [`tmux/tmux-builds`](https://github.com/tmux/tmux-builds)

#### Updated Packages (2)

-
[`firecow/gitlab-ci-local`](https://github.com/firecow/gitlab-ci-local)
- [`k1LoW/runn`](https://github.com/k1LoW/runn)
risu729 pushed a commit to risu729/mise that referenced this pull request Feb 27, 2026
)

## Summary

PR jdx#8279 placed every task child process in its own process group via
`setpgid(0, 0)`. This broke interactive tools like Tilt that read from
stdin.

**Root cause:** The terminal driver only delivers keyboard input to the
*foreground process group*. When a child is moved to a new process
group, it's no longer the terminal's foreground group, so any attempt to
`read()` from stdin causes the kernel to send `SIGTTIN`, which silently
stops the process. Result: the process hangs, Ctrl-C has no effect, and
the UI never loads.

**Fix:** In `pre_exec`, check `std::io::stdin().is_terminal()`. If stdin
is a TTY (inherited from the terminal), skip `setpgid` — the process
stays in the terminal's foreground group and gets full keyboard I/O. If
stdin is not a TTY (null/piped, i.e. non-interactive tasks), `setpgid`
fires as before, preserving grandchild cleanup on Ctrl-C.

This keeps the grandchild-kill benefit of jdx#8279 for all normal
(non-interactive) tasks while restoring correct behavior for interactive
ones.

Closes jdx#8300

## Test plan
- [x] `cargo build` compiles cleanly
- [x] Verified `mise run tilt:up` (or equivalent interactive task) no
longer hangs
- [x] Non-interactive tasks still get their own process group for clean
teardown

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Small, Unix-only change gated on an `is_terminal` check; primary risk
is behavior change in process-group/signal handling for commands with
TTY stdin.
> 
> **Overview**
> Fixes Unix task execution for interactive commands by **only calling
`setpgid(0,0)` when stdin is not a TTY**.
> 
> In `CmdLineRunner::execute`’s `pre_exec` hook, stdin is checked via
`BorrowedFd::borrow_raw(0)` + `IsTerminal`; interactive children stay in
the terminal’s foreground process group to avoid `SIGTTIN` hangs, while
non-interactive tasks keep the separate process group behavior for
signal/cleanup handling.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
4bf5aef. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
risu729 pushed a commit to risu729/mise that referenced this pull request Feb 27, 2026
…mise tasks (jdx#8347)

## Summary
- Reverts the `setpgid`/`killpg` changes from jdx#8279 and jdx#8301
- Placing each child process in its own process group via `setpgid(0,0)`
breaks external tools (e.g. Playwright) that rely on process-group-based
kills to tear down process trees
- When Playwright `SIGKILL`s the webServer process group, grandchild
processes in a separate group (created by inner mise's `setpgid`)
survive, holding pipes open and causing the parent to hang indefinitely

## Root Cause

The process group change created a hierarchy where nested `mise run`
calls each placed their children in separate process groups:

```
mise run test  (pgid=terminal)
  └── sh -c "npm test"  (pgid=A, set by outer mise's setpgid)
      └── playwright
          └── mise run serve  (pgid=C, set by Playwright's detached:true)
              └── sh -c "npm run serve"  (pgid=D, set by inner mise's setpgid)
                  └── node main.js  (inherits pgid=D)
```

When Playwright kills pgid=C (`kill(-pid, SIGKILL)`), only processes in
that group die. The actual server in pgid=D survives because `SIGKILL`
cannot be caught/forwarded, and the surviving process holds pipe file
descriptors open, preventing EOF and causing the parent mise to hang
indefinitely.

Fixes jdx#8345

## Test plan
- [x] `cargo build` compiles cleanly
- [x] `mise run lint` passes
- [x] `mise run test:unit` — all 491 tests pass
- [x] `mise run test:e2e test_task_timeout` — timeout tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Moderate risk because it changes how signals/timeouts propagate to
subprocess trees on Unix, which can affect cleanup and
Ctrl-C/termination semantics for running tasks.
> 
> **Overview**
> Stops creating per-command process groups on Unix by removing the
`pre_exec` `setpgid` hook, restoring the prior behavior where child
commands stay in the caller’s process group.
> 
> Replaces all `killpg` usage with direct `kill` calls: timeout
termination (`SIGTERM` then `SIGKILL`), global shutdown via
`CmdLineRunner::kill_all`, and signal forwarding during `execute` now
target only the child PID (and it no longer forwards `SIGINT`).
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
e6ed47d. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants