第 1 章:进程与标准 I/O

# 第 1 章:进程与标准 I/O

> 任何程序,无论多复杂,最终都运行在一个进程里,通过文件描述符与世界交换数据。

---

## 1.1 核心问题

在构建任何 UI、调用任何 API、执行任何工具之前,我们先回答一个最基础的问题:

**一个 Coding Agent 的进程从哪里读输入,往哪里写输出?**

这个问题的答案决定了整个系统的"外壳"——其他所有层(终端 UI、LLM 客户端、工具引擎)都运行在这个外壳之内。

---

## 1.2 原理讲解

### Node.js 进程模型

每个 Node.js 程序启动时,操作系统分配给它三个标准文件描述符:

| 描述符 | 名称 | Node.js 对象 | 默认连接 |
|--------|------|-------------|---------|
| 0 | stdin | `process.stdin` | 键盘输入 |
| 1 | stdout | `process.stdout` | 终端显示 |
| 2 | stderr | `process.stderr` | 终端显示(错误) |

这三个流是**双向管道**:可以重定向(`echo "hello" | node main.js`),也可以直接读写。

Node.js 的核心是**事件循环**:单线程轮询 I/O 事件,收到数据则触发回调。程序不会因等待 I/O 而阻塞,但同时只能处理一件事。

### TypeScript 的运行方式

Claude Code 使用 TypeScript 编写,通过 `tsx`(基于 esbuild 的即时编译器)直接运行,无需预先编译:

```bash
npx tsx src/main.ts   # 直接运行 .ts 文件
```

生产环境中 Claude Code 使用 Bun 打包,但开发和学习阶段用 `tsx` 足够。

### 进程退出与信号处理

进程退出时返回一个**退出码**(exit code):`0` 表示成功,非零表示错误。

操作系统还可以向进程发送**信号**:

| 信号 | 触发方式 | 默认行为 |
|------|---------|---------|
| `SIGINT` | Ctrl-C | 终止进程 |
| `SIGTERM` | `kill <pid>` | 终止进程 |

Claude Code 拦截这两个信号,在退出前写入会话档案、清理子进程。

### Claude Code 的全局单例状态

`src/bootstrap/state.ts` 定义了一个进程级的单例对象,保存整个会话期间不变的元数据:

```typescript
// src/bootstrap/state.ts(简化)
type State = {
  sessionId: SessionId      // 每次启动唯一的 UUID
  startTime: number         // 进程启动时间戳(ms)
  cwd: string               // 当前工作目录
  originalCwd: string       // 启动时的工作目录(不随 cd 变化)
  projectRoot: string       // 项目根目录(用于 CLAUDE.md 查找)
  isInteractive: boolean    // 是否是交互模式(有 TTY)
  totalCostUSD: number      // 本次会话累计 API 费用
  // ... 以及数十个其他字段
}
```

**为什么需要全局单例?** 因为 Claude Code 的各个模块(UI、工具、LLM 客户端)都需要访问会话 ID、工作目录等共享信息。全局单例比在每个函数调用链中逐层传递参数更实用。

> **注意**:`bootstrap/state.ts` 是有意保持为**导入依赖树的叶节点**(leaf node)——它不能导入其他 Claude Code 模块,防止循环依赖。这是贯穿整个代码库的架构约束。

---

## 1.3 Claude Code 源码中的关键细节

### 启动顺序(`src/main.tsx` 前几行)

```typescript
// 1. 标记启动时间点(用于性能分析)
profileCheckpoint('main_tsx_entry')

// 2. 提前启动需要时间的后台任务(并行化启动耗时)
startMdmRawRead()       // 读取 MDM 企业策略(macOS: plutil,Windows: reg query)
startKeychainPrefetch() // 预取 Keychain 中的 OAuth token 和 API key

// 3. 之后才是 Commander 路由、Ink 启动等主体逻辑
```

这个模式——**在 import 阶段就启动 I/O 密集型后台任务**——使 Claude Code 的冷启动时间从 ~200ms 降到 ~65ms。

### Session ID 的生成

```typescript
// src/bootstrap/state.ts
import { randomUUID } from 'src/utils/crypto.js'

sessionId: randomUUID() as SessionId
```

`sessionId` 用于:
- 本地会话存档文件命名(`~/.claude/projects/<hash>/<sessionId>.jsonl`)  
- API 请求的 `X-Session-Id` 请求头(用于计费追踪)
- 子 Agent 与父 Agent 的关联(`parentSessionId`)

### `isInteractive` 的判断

```typescript
isInteractive: process.stdin.isTTY === true
```

当 stdin 连接到真实终端时为 `true`;通过管道(`echo "..." | claude`)或 CI 环境时为 `false`。  
这个标志决定是否渲染 Ink UI、是否显示权限确认弹窗。

---

## 1.4 最小化产出物

> 代码骨架位于 `../chapters/01/src/`,参考实现位于 `../chapters/01/solution/`。

### 本章要实现什么

在 `../chapters/01/src/state.ts` 中完成以下内容。

**接口规范**(已提供,不要修改):

```typescript
// 全局状态对象
export const state: {
  sessionId: string    // UUID,每次启动唯一
  startTime: number    // 启动时间戳(ms)
  cwd: string          // 当前工作目录
  isInteractive: boolean  // 是否连接到真实终端
}

// 信号处理注册函数
export function registerSignalHandlers(onExit?: () => void): void
```

**你需要实现**:

1. `state` 对象的四个字段(`randomUUID()`、`Date.now()`、`process.cwd()`、`process.stdin.isTTY`)
2. `registerSignalHandlers()`:
   - `SIGINT`(Ctrl-C):打印提示 + 会话时长,调用 `onExit?.()` 后退出
   - `SIGTERM`:调用 `onExit?.()` 后退出

**关键约束**:
- `state.ts` 不能 `import` 任何其他章节的模块(它是整个系列的叶节点)
- 信号处理器注册一次即可,后续章节 import 此文件时自动生效

### 验收

```bash
cd docs/chapters/01
npm install
npm test
```

初始状态会失败(TODO 未填写)。填写 `src/state.ts` 后再次运行,预期输出:

```
=== 第 1 章:进程与标准 I/O 验收 ===

── 文件检查 ──
  ✓ src/state.ts 存在
  ✓ src/main.ts 存在

── 实现检查 ──
  ✓ src/state.ts 已实现

── 功能验收 ──
  ✓ 管道模式:回显输入内容
  ✓ 管道模式:处理多行输入(第1行)
  ✓ 管道模式:处理多行输入(第3行)
  ✓ --debug 输出 sessionId
  ✓ --debug 输出 cwd
  ✓ --debug 输出 isInteractive: false
  ✓ --debug 模式下 stdout 仍正常输出
  ✓ --debug 信息不出现在 stdout
  ✓ state.sessionId 是 UUID(36字符)
  ✓ state.startTime 是数字
  ✓ state.cwd 非空
  ✓ state.isInteractive 为 false(管道环境)
  ✓ registerSignalHandlers 是函数

结果:16 通过,0 失败
✓ 全部通过!可以进入第 2 章了。
```

卡住时查看 `../chapters/01/solution/state.ts`。

---

## 1.5 本章小结

| 概念 | 要记住的要点 |
|------|-------------|
| 三个标准流 | stdin(0) / stdout(1) / stderr(2),可以重定向 |
| 事件循环 | 单线程,非阻塞 I/O,程序通过回调响应事件 |
| tsx | TypeScript 的即时运行器,无需预编译 |
| SIGINT / SIGTERM | Ctrl-C 发送 SIGINT,必须拦截后才能优雅退出 |
| 全局单例 state | 进程级元数据(sessionId / cwd / startTime),是导入树的叶节点 |
| `isInteractive` | 判断是否有 TTY,决定是否启动交互 UI |
| `state.ts` 独立导出 | state 和 registerSignalHandlers 被拆分到独立文件,供第 2、3 章 import 复用 |

这一章的产出物是整个系列的**地基**:一个可以读取输入并回显输出的进程,以及一个可被后续章节直接复用的 `state.ts` 模块。接下来,我们在这个地基上架起终端 UI。

---

## 下一章

→ [第 2 章:终端 UI 外壳(Ink + React)](02-terminal-ui)