github / github/copilot-cli

Bug: Copilot CLI hangs in Nix/direnv environments due to subprocess I/O deadlock

未关闭
#1,838 6 条评论 12 个 reaction 已指派 0 人 在 GitHub 查看
area:tools
主要语言
Shell
星标
11.2k
派生
1.9k
平均合并
14 小时 16 分钟
30 天内合并 PR
6

描述

# Bug: Copilot CLI hangs in Nix/direnv environments due to subprocess I/O deadlock

## Summary
Copilot CLI v0.0.421 hangs indefinitely when launched from directories with Nix flake-based development environments managed by direnv. The bash tool fails to execute any commands, timing out with `Invalid shell ID` errors.

## Root Cause Analysis

### Static Code Analysis
Analysis of the minified `index.js` (v0.0.421) reveals a critical I/O handling issue:

**Subprocess spawning patterns:**
- `child_process.exec()`: 215 calls
- `child_process.spawn()`: 7 calls
- `child_process.execFile()`: (wrapped by exec)
- **Total: 220+ subprocess creations**

**Stream consumption:**
- `.stdout.on()` listeners: 9 total
- `.stderr.on()` listeners: 9 total
- `.stdout.resume()` calls: 0
- `.stderr.resume()` calls: 0

**Disparity:** Only 4% of spawned subprocesses have stdout/stderr listeners. The remaining 96% don't drain their output streams.

### Why This Causes Hangs in Nix/direnv

1. **direnv flake evaluation generates heavy I/O:** When `direnv` loads a Nix flake, the environment exports ~100+ environment variables with verbose logging, generating substantial stdout/stderr output.

2. **Pipe buffer overflow:** Copilot spawns subprocesses without consuming their output streams. In high-I/O scenarios (like Nix flake evaluation), pipe buffers fill up (~64KB on macOS).

3. **Deadlock:**
- Parent process (Copilot) doesn't read from child's stdout/stderr
- Child process tries to write output → pipe buffer fills → child blocks
- Parent waits for child to exit → child blocked on I/O → deadlock

4. **Why it affects Nix but not regular shells:** Regular shells have minimal direnv logging. Nix flakes trigger aggressive environment setup, generating enough output to trigger pipe buffer overflow reliably.

## Reproduction Steps

### Prerequisites
- Nix with flakes support
- direnv
- nix-direnv (optional, but speeds up cache)

### Steps
```bash
# Create a minimal flake-based dev environment
mkdir test-nix-copilot && cd test-nix-copilot

# Create flake.nix with direnv-managed environment
cat > flake.nix << 'FLAKE'
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
inputs.flake-utils.url = "github:numtide/flake-utils";

outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
{
devShell = (import nixpkgs { inherit system; }).mkShell {
buildInputs = with (import nixpkgs { inherit system; }); [ nodejs git ];
};
}
);
}
FLAKE

# Create .envrc
cat > .envrc << 'ENVRC'
use flake . --show-trace
unset DEVELOPER_DIR
ENVRC

# Allow direnv
direnv allow .

# Activate the environment (this loads the flake)
cd .

# Try to use Copilot CLI
copilot
# Expected: Hangs immediately, bash tool times out with "Invalid shell ID" error
```

### Symptoms
- Copilot CLI starts normally
- Environment loads successfully
- Bash tool commands hang indefinitely
- Timeout after 5 seconds: `Invalid shell ID: 0. Please supply a valid shell ID`
- No error output, process appears stuck

## Proposed Fix

### Solution
Add stream resumption after subprocess creation to prevent pipe buffer overflow:

```typescript
// In child_process wrapper/util
function createSubprocess(command, args, options) {
const child = spawn(command, args, options);

// Drain stdout/stderr to prevent buffer overflow
// This allows data to flow without requiring explicit listeners
if (child.stdout) child.stdout.resume();
if (child.stderr) child.stderr.resume();

return child;
}

// Apply to all spawn/exec/execFile calls
```

**Why this works:**
- `.resume()` puts streams in flowing mode without consuming data
- Allows pipes to drain automatically without blocking the parent
- Minimal performance impact (data is still discarded if not explicitly handled)
- Follows Node.js best practices for subprocess I/O handling

### Affected Areas
All `child_process` creations should ensure stream handling:
1. `child_process.spawn()` calls (7 instances)
2. `child_process.exec()` calls (215 instances)
3. `child_process.execFile()` calls
4. `child_process.fork()` calls

## Technical Details

### Why Listener Count is Insufficient
The analysis shows that listeners exist, but they are task-specific. Most spawn calls (especially for tool execution) don't register listeners because they don't expect to parse output—they just run the command. In these cases:
- Parent doesn't read output
- Child writes to pipe
- Pipe fills → child blocks
- Deadlock

### Node.js Documentation Reference
From Node.js docs on `child_process`:
> If the child stdio streams are not explicitly used, they will be piped to parent stdio. However, if the stream isn't consumed (no listeners attached and stream not resumed), the child will block when the pipe buffer fills.

## Impact
- **Severity:** High - Blocks all Copilot CLI usage in Nix environments
- **Frequency:** Reproducible 100% of the time in Nix flake + direnv setups
- **User Base:** Growing (Nix adoption increasing, especially in teams using flakes)
- **Workaround:** Launch Copilot from directories without direnv, or pre-load environment before launching

## Additional Context

### Version
- Copilot CLI: 0.0.421
- Node.js: v24.x
- Tested on: macOS (arm64), should affect Linux/WSL similarly

### Related Issues
- Mentions of subprocess hangs in Nix: #1428, #575, #731
- Similar issues: nix-community/nix-direnv#292 (direnv hangs on flake eval)

### Investigation Methodology
1. Reproduced hang in Nix flake + direnv environment
2. Analyzed minified `index.js` for subprocess patterns
3. Counted exec/spawn calls vs stdio listener registration
4. Confirmed disparity: 220+ spawns vs 9 listeners
5. Validated hypothesis: High I/O from direnv triggers pipe buffer overflow
6. Tested workarounds: Pre-loading environment reduces but doesn't eliminate hangs

---

**Note:** This issue was discovered through systematic analysis of subprocess I/O handling in isolated development environments. The fix is well-established in Node.js best practices and should have minimal performance impact.

贡献指南

打开贡献指南

调研方向

首先,使用 issue 中描述的最小 Nix flake 和 .envrc 设置重现挂起问题。检查 child_process wrapper/util 和经过压缩的 index.js v0.0.421,重点关注列出的 spawn、exec、execFile 和 fork 调用。当 bash 工具命令能够在 Nix/direnv 环境中可靠完成,且没有 Invalid shell ID 超时时,即表示完成。

由索引模型根据 Issue 内容生成。

评估

技术栈
node.js, typescript
领域
cli
Issue 类型
缺陷
难度
4/5
预计耗时
3-5 天
活跃度
冷清
描述清晰度
基本清楚
新手友好度
45/100

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。