Skip to content

fix(task): skip setpgid for TTY stdin to fix interactive tasks#8301

Merged
jdx merged 2 commits intomainfrom
fix/task-interactive-setpgid
Feb 22, 2026
Merged

fix(task): skip setpgid for TTY stdin to fix interactive tasks#8301
jdx merged 2 commits intomainfrom
fix/task-interactive-setpgid

Conversation

@jdx
Copy link
Owner

@jdx jdx commented 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

  • cargo build compiles cleanly
  • Verified mise run tilt:up (or equivalent interactive task) no longer hangs
  • Non-interactive tasks still get their own process group for clean teardown

🤖 Generated with Claude Code


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.

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

Processes with inherited TTY stdin (e.g. Tilt, interactive CLIs) must
stay in the terminal's foreground process group. Moving them to a new
group via setpgid causes SIGTTIN, stopping them silently.

Only call setpgid when stdin is not a TTY; non-interactive tasks
(stdin null/piped) still get their own process group for proper
grandchild cleanup on Ctrl-C.

Fixes #8300

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 22, 2026 15:21
@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 resolves a critical issue where interactive tasks would hang due to incorrect process group management introduced in a previous change. By intelligently applying process group isolation only to non-interactive tasks, it restores full functionality for interactive command-line tools while preserving the benefits of robust process termination for background operations.

Highlights

  • Interactive Task Fix: Addresses an issue where interactive tools (e.g., Tilt) would hang when setpgid moved them to a new process group, preventing them from receiving keyboard input.
  • Conditional setpgid Application: setpgid is now conditionally applied only when stdin is not a TTY, ensuring interactive tasks remain in the terminal's foreground process group.
  • Grandchild Cleanup Preservation: Non-interactive tasks still benefit from setpgid for clean teardown and grandchild process termination, maintaining the original intent of PR fix(task): use process groups to kill child process trees on Unix #8279.
Changelog
  • src/cmd.rs
    • Modified the pre_exec hook to conditionally call setpgid based on whether stdin is a TTY.
Activity
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

The PR correctly addresses the issue where interactive tasks hang by skipping setpgid when stdin is a TTY. This ensures the process remains in the terminal's foreground process group. However, the current implementation uses std::io::stdin() inside the pre_exec closure, which is not async-signal-safe and could lead to deadlocks in a multi-threaded environment like mise. I recommend using nix::unistd::isatty(0) instead, as it is safe to call in a post-fork environment.

src/cmd.rs Outdated
// Interactive tools (e.g. Tilt) need to stay in the terminal's
// foreground process group; moving them to a new one triggers
// SIGTTIN and hangs them.
if !std::io::IsTerminal::is_terminal(&std::io::stdin()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

high

Calling std::io::stdin() inside a pre_exec closure is not async-signal-safe. In a multi-threaded process, fork() only duplicates the calling thread. If another thread in the parent process held the internal lock for stdin at the moment of the fork, the child process will deadlock when it attempts to acquire that same lock.

Since this code is already within a #[cfg(unix)] block and uses the nix crate, it is safer to use nix::unistd::isatty(0) which directly performs the system call without high-level synchronization and is async-signal-safe.

Suggested change
if !std::io::IsTerminal::is_terminal(&std::io::stdin()) {
if !nix::unistd::isatty(0).unwrap_or(false) {

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 regression introduced in #8279 where interactive tasks that read from stdin (like Tilt) would hang when executed. The fix conditionally skips process group creation via setpgid when the child process has a TTY stdin, allowing interactive tools to remain in the terminal's foreground process group and receive keyboard input properly.

Changes:

  • Add conditional check in pre_exec closure to skip setpgid when stdin is a terminal
  • Preserves grandchild cleanup benefit from #8279 for non-interactive tasks
  • Restores interactive task functionality

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

src/cmd.rs Outdated
// Interactive tools (e.g. Tilt) need to stay in the terminal's
// foreground process group; moving them to a new one triggers
// SIGTTIN and hangs them.
if !std::io::IsTerminal::is_terminal(&std::io::stdin()) {
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

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

Calling std::io::stdin() in pre_exec may not be async-signal-safe as it could involve allocation. The pre_exec closure runs after fork but before exec and must only use async-signal-safe functions. Consider using nix::unistd::isatty(0) instead, which directly calls the isatty syscall without allocating. This would make the code more robust and properly follow pre_exec safety requirements.

Suggested change
if !std::io::IsTerminal::is_terminal(&std::io::stdin()) {
if !matches!(nix::unistd::isatty(0), Ok(true)) {

Copilot uses AI. Check for mistakes.
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is ON, but a Cloud Agent failed to start.

@github-actions
Copy link

github-actions bot commented Feb 22, 2026

Hyperfine Performance

mise x -- echo

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.2.18 x -- echo 23.1 ± 0.3 22.5 25.5 1.00
mise x -- echo 23.3 ± 0.3 22.6 25.0 1.01 ± 0.02

mise env

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.2.18 env 22.0 ± 0.6 21.4 27.8 1.00
mise env 22.1 ± 0.3 21.5 23.8 1.00 ± 0.03

mise hook-env

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.2.18 hook-env 22.7 ± 0.4 22.1 26.5 1.00
mise hook-env 22.8 ± 0.3 22.2 24.2 1.00 ± 0.02

mise ls

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.2.18 ls 20.2 ± 0.3 19.6 22.1 1.00
mise ls 20.3 ± 0.9 19.7 32.9 1.01 ± 0.04

xtasks/test/perf

Command mise-2026.2.18 mise Variance
install (cached) 125ms 124ms +0%
ls (cached) 75ms 75ms +0%
bin-paths (cached) 80ms 80ms +0%
task-ls (cached) 805ms 817ms -1%

std::io::stdin() accesses a static OnceLock post-fork, which can
deadlock if a thread held the lock at fork time. Replace with
BorrowedFd::borrow_raw(0).is_terminal() which calls libc::isatty(0)
directly — no allocation, no locks, async-signal-safe.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@jdx jdx merged commit c1f69b4 into main Feb 22, 2026
33 of 35 checks passed
@jdx jdx deleted the fix/task-interactive-setpgid branch February 22, 2026 15:54
jdx pushed a commit that referenced this pull request Feb 22, 2026
### 🐛 Bug Fixes

- **(docs)** correct ripgrep command by @nguyenvulong in
[#8299](#8299)
- **(task)** skip setpgid for TTY stdin to fix interactive tasks by @jdx
in [#8301](#8301)
- clean up empty parent install dir on failed install by @jdx in
[#8302](#8302)

### Chore

- **(release)** run communique via mise x for PATH resolution by @jdx in
[#8294](#8294)

## 📦 Aqua Registry Updates

#### New Packages (2)

- [`kubie-org/kubie`](https://github.com/kubie-org/kubie)
- [`steipete/gogcli`](https://github.com/steipete/gogcli)
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
)

## 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
### 🐛 Bug Fixes

- **(docs)** correct ripgrep command by @nguyenvulong in
[jdx#8299](jdx#8299)
- **(task)** skip setpgid for TTY stdin to fix interactive tasks by @jdx
in [jdx#8301](jdx#8301)
- clean up empty parent install dir on failed install by @jdx in
[jdx#8302](jdx#8302)

### Chore

- **(release)** run communique via mise x for PATH resolution by @jdx in
[jdx#8294](jdx#8294)

## 📦 Aqua Registry Updates

#### New Packages (2)

- [`kubie-org/kubie`](https://github.com/kubie-org/kubie)
- [`steipete/gogcli`](https://github.com/steipete/gogcli)
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