fix(task): use process groups to kill child process trees on Unix#8279
fix(task): use process groups to kill child process trees on Unix#8279
Conversation
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>
Summary of ChangesHello @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
Changelog
Activity
Using Gemini Code AssistThe 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
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 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
|
There was a problem hiding this comment.
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.
| let _ = nix::unistd::setpgid( | ||
| nix::unistd::Pid::from_raw(0), | ||
| nix::unistd::Pid::from_raw(0), | ||
| ); |
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
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}");
}There was a problem hiding this comment.
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)))] | |||
There was a problem hiding this comment.
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.
| #[cfg(not(any(test, windows)))] | |
| #[cfg(not(any(test, target_os = "windows")))] |
| let _ = nix::unistd::setpgid( | ||
| nix::unistd::Pid::from_raw(0), | ||
| nix::unistd::Pid::from_raw(0), | ||
| ); |
There was a problem hiding this comment.
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.
| 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}"); | |
| } |
| 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); |
There was a problem hiding this comment.
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.
| 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}"); | |
| } |
Hyperfine Performance
|
| 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% |
### 🚀 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)
## [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)
## 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>
…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>
…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>
…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>
### 🚀 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)
) ## 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>
…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>
Summary
setpgidinpre_exec, then usekillpg(withkillfallback) inkill_all()and signal forwardingnpm startspawning webpack) are properly killed on Ctrl+C, timeout, or task failure instead of being orphanedTest plan
cargo buildcompiles cleanlymise run lintpassesmise run test:unit— all 481 tests passmise 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
setpgidinCmdLineRunner::execute, and termination logic targets the process group usingkillpg(with akillfallback).This updates
CmdLineRunner::kill_alland 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.