---
date: 2025-10-10T14:30:00+08:00
researcher: Claude (Sonnet 4.5)
git_commit: 1e1f887560c1a00107ff7114b79ca97d0a41c509
branch: makefile
repository: git@github.com:kentchiu/fwauto.git
topic: "Build node 執行 make 時卡住，但 flash node 正常"
tags: [research, codebase, build, flash, subprocess, windows, makefile, hang]
status: complete
last_updated: 2025-10-10
last_updated_by: Claude (Sonnet 4.5)
---

# Research: Build Node 執行 Make 時卡住問題

**Date**: 2025-10-10T14:30:00+08:00
**Researcher**: Claude (Sonnet 4.5)
**Git Commit**: 1e1f887560c1a00107ff7114b79ca97d0a41c509
**Branch**: makefile
**Repository**: git@github.com:kentchiu/fwauto.git

## Research Question

為什麼 build node 在執行 make 時會卡住，但 flash node 不會？直接在 terminal 執行 make 都能正常完成。

## Summary

經過全面研究發現：**build.py 和 flash.py 使用完全相同的 subprocess 實作**，差異在於**執行的底層工具不同**：

- **Build**: 執行 Keil uVision 4/5 (GUI 編譯器)
- **Flash**: 執行 ST-LINK CLI (純命令列工具)

**核心問題**：Keil UV4 作為 GUI 應用程式，在透過 subprocess.Popen 執行時，缺少 Windows 特定的 `creationflags` 設定，可能導致：
1. 輸出緩衝問題 (stdout 未正確刷新)
2. 子進程或視窗管理問題
3. Line buffering 在 GUI 應用程式上無效

而 ST-LINK CLI 作為純命令列工具，天生適合 subprocess 調用，因此不會遇到相同問題。

## Detailed Findings

### 1. Subprocess 實作完全相同

**對比分析**：

```mermaid
graph LR
    A[build.py:83-102] --> C[相同的 Popen 設定]
    B[flash.py:83-102] --> C
    C --> D[bufsize=1]
    C --> E[text=True]
    C --> F[stdout=PIPE]
    C --> G[stderr=STDOUT]
```

| 項目 | Build Node | Flash Node | 差異 |
|------|-----------|-----------|------|
| 檔案路徑 | `src/fwauto/nodes/build.py` | `src/fwauto/nodes/flash.py` | - |
| subprocess 呼叫位置 | Line 83-92 | Line 83-92 | - |
| bufsize 參數 | `1` (line buffered) | `1` (line buffered) | ✅ 相同 |
| text 模式 | `True` | `True` | ✅ 相同 |
| encoding | `"utf-8"` | `"utf-8"` | ✅ 相同 |
| stdout 處理 | `subprocess.PIPE` | `subprocess.PIPE` | ✅ 相同 |
| stderr 處理 | `subprocess.STDOUT` | `subprocess.STDOUT` | ✅ 相同 |
| 輸出讀取 | Line 97-99 迴圈 | Line 97-99 迴圈 | ✅ 相同 |
| process.wait() | Line 101 | Line 101 | ✅ 相同 |

**結論**：subprocess 實作層面**沒有差異**。

### 2. 執行的底層工具差異

**Build Makefile** ([src/fwauto/templates/build/Makefile.minimal:19](src/fwauto/templates/build/Makefile.minimal#L19)):

```makefile
@"$(KEIL_UV4)" -b "$(PROJECT_FILE)" -j0
```

- **工具**: Keil uVision 4/5 (`C:\Keil_v5\UV4\UV4.exe`)
- **類型**: GUI 應用程式
- **參數**:
  - `-b`: Batch build mode (批次編譯模式)
  - `-j0`: 未知參數 (可能是 jobs/threads 相關)
- **特性**:
  - 原本是 IDE 環境
  - 可能創建子進程
  - 可能有視窗管理邏輯
  - stdout 輸出行為不保證

**Flash Makefile** ([src/fwauto/templates/flash/Makefile.minimal:60](src/fwauto/templates/flash/Makefile.minimal#L60)):

```makefile
$(STLINK_CLI) -c SWD -P "$(HEX_FILE)" -V -Rst
```

- **工具**: ST-LINK CLI (`C:\Program Files (x86)\STMicroelectronics\...\ST-LINK_CLI.exe`)
- **類型**: 純命令列工具
- **參數**:
  - `-c SWD`: 連接模式
  - `-P`: 燒錄
  - `-V`: 驗證
  - `-Rst`: 重置
- **特性**:
  - 專為命令列設計
  - 標準 stdout/stderr 輸出
  - 無 GUI 組件

**架構差異圖**：

```mermaid
graph TD
    A[Python subprocess.Popen] --> B[Make Process]
    B --> C[Keil UV4<br/>GUI Application]
    B --> D[ST-LINK CLI<br/>CLI Tool]

    C --> E1[可能創建視窗]
    C --> E2[輸出緩衝不確定]
    C --> E3[子進程管理]

    D --> F1[標準 stdout]
    D --> F2[正常輸出刷新]
    D --> F3[無視窗]

    E2 -.可能導致.-> G[Hang]
    F2 --> H[正常完成]

    style C fill:#ffcccc
    style D fill:#ccffcc
    style G fill:#ff6666
    style H fill:#66ff66
```

### 3. Windows Subprocess 缺少的關鍵設定

**目前實作** ([build.py:83-92](src/fwauto/nodes/build.py#L83-L92)):

```python
process = subprocess.Popen(
    cmd,
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT,
    text=True,
    encoding="utf-8",
    errors="replace",
    cwd=str(makefile_dir),
    bufsize=1,  # Line buffered
)
```

**缺少的 Windows 特定參數**：

| 參數 | 目前值 | 建議值 | 用途 |
|------|-------|-------|------|
| `creationflags` | 未設置 (預設 0) | `subprocess.CREATE_NO_WINDOW` | 防止創建新視窗 |
| `shell` | 未設置 (預設 False) | `False` (已正確) | 避免額外 shell 進程 |
| timeout (wait) | 無 | 300 秒 | 防止無限等待 |

**Windows 特定問題**：

1. **GUI 應用程式與 subprocess**
   - Windows 上執行 GUI exe 可能觸發視窗創建邏輯
   - 即使使用 `-b` batch mode，Keil 可能仍保留部分 GUI 行為
   - 沒有 `CREATE_NO_WINDOW` flag，可能導致進程管理問題

2. **Line Buffering 限制**
   - `bufsize=1` 僅對文字模式的「行緩衝」有效
   - GUI 應用程式可能不遵循標準 stdout 緩衝規則
   - CLI 工具通常正確實作 line-buffered stdout

3. **輸出讀取可能阻塞的位置**
   - Line 97: `for line in process.stdout:` - 等待下一行
   - Line 101: `process.wait()` - 等待進程結束

### 4. 歷史脈絡與相關問題

**Commit b7afeb8** (2025-10-10) - "refactor(build): add real-time output streaming"
- 從 `subprocess.run()` 改為 `subprocess.Popen()`
- 目的：實作即時輸出顯示
- **潛在影響**：引入了目前的 hang 問題

**相關研究**：[.ai/rpi/research/2025-10-10-integration-testing-layer2-summary.md](.ai/rpi/research/2025-10-10-integration-testing-layer2-summary.md)
- Layer 2 整合測試成功發現了 `process.returncode` bug
- 但測試使用 mock Makefile (replay_output.py)，未執行真實 Keil UV4
- 因此無法捕獲 Windows GUI 應用程式的特定問題

### 5. Terminal 直接執行 Make 為何正常？

**關鍵差異**：

```mermaid
graph LR
    A[Terminal 執行] --> B[Make Process]
    B --> C[Keil UV4]
    C --> D[繼承 Terminal 的 stdout]
    D --> E[正常輸出到 Console]

    F[Python subprocess] --> G[Make Process]
    G --> H[Keil UV4]
    H --> I[stdout 被 PIPE 捕獲]
    I --> J{緩衝問題?}
    J --> K[Hang]

    style E fill:#66ff66
    style K fill:#ff6666
```

**解釋**：
1. **Terminal 執行**：Keil 直接輸出到 Console，系統處理所有緩衝
2. **subprocess.Popen**：Keil 輸出到 PIPE，Python 逐行讀取
   - 如果 Keil 沒有正確刷新 stdout
   - 或輸出不包含換行符
   - Python 的 `for line in stdout` 會永遠等待

### 6. Flash Node 為何不受影響？

**ST-LINK CLI 的優勢**：

| 特性 | Keil UV4 | ST-LINK CLI | 影響 |
|------|---------|-------------|------|
| 應用程式類型 | GUI | CLI | CLI 天生適合 subprocess |
| stdout 輸出 | 不保證 | 標準化 | CLI 保證正確輸出 |
| 緩衝行為 | 不確定 | Line-buffered | CLI 正確刷新 |
| 視窗管理 | 可能有 | 無 | CLI 無視窗問題 |
| 子進程 | 可能有 | 無 | CLI 進程管理簡單 |

**實測驗證** (來自用戶描述)：
- ✅ Flash node 使用 ST-LINK CLI - 正常完成
- ❌ Build node 使用 Keil UV4 - 卡住
- ✅ Terminal 直接執行 make (build) - 正常完成

## Code References

| 檔案路徑 | 行號 | 說明 |
|---------|------|------|
| `src/fwauto/nodes/build.py` | 83-92 | subprocess.Popen 設定 (與 flash 相同) |
| `src/fwauto/nodes/build.py` | 97-99 | 輸出讀取迴圈 (可能阻塞點) |
| `src/fwauto/nodes/build.py` | 101 | process.wait() (可能阻塞點) |
| `src/fwauto/nodes/flash.py` | 83-92 | subprocess.Popen 設定 (相同實作) |
| `src/fwauto/templates/build/Makefile.minimal` | 19 | Keil UV4 執行命令 |
| `src/fwauto/templates/flash/Makefile.minimal` | 60 | ST-LINK CLI 執行命令 |
| `src/fwauto/config.py` | 14 | 平台檢測 (未用於 subprocess 調整) |

## Architecture Insights

### 目前的架構設計

```mermaid
sequenceDiagram
    participant P as Python (build.py)
    participant M as Make Process
    participant K as Keil UV4

    P->>M: subprocess.Popen(make build)
    M->>K: 執行 Keil -b -j0

    Note over K: GUI 應用程式<br/>可能不正確輸出到 stdout

    P->>P: for line in stdout:<br/>等待換行符...

    Note over P: 可能在此 Hang<br/>等待永遠不會來的輸出

    K-->>M: (輸出被緩衝)
    M-->>P: (沒有輸出到達)

    rect rgb(255, 200, 200)
        Note over P,K: 死鎖狀態<br/>Python 等待輸出<br/>Keil 可能已完成但未刷新
    end
```

### 設計模式觀察

1. **統一的 Subprocess 抽象**
   - 優點：build 和 flash 使用相同邏輯，程式碼簡潔
   - 缺點：無法針對不同工具類型 (GUI vs CLI) 做最佳化

2. **Make 作為中間層**
   - 優點：解耦 Python 與實際工具 (Keil/ST-LINK)
   - 缺點：多一層間接性，問題診斷更困難

3. **即時輸出需求 vs 可靠性**
   - 目標：顯示即時編譯進度
   - 風險：依賴工具正確實作 line-buffered stdout
   - Trade-off：可考慮回退到 `communicate()` 獲取完整輸出

### 可擴展方向

1. **工具類型感知**
   - 檢測執行的是 GUI 應用程式還是 CLI 工具
   - 針對 GUI 應用使用不同的 subprocess 參數

2. **Platform-specific 處理**
   - 已有平台檢測 ([config.py:14](src/fwauto/config.py#L14))
   - 可擴展用於設定 Windows 特定的 creationflags

3. **Timeout 保護機制**
   - 避免無限等待
   - 提供使用者友善的錯誤訊息

4. **輸出緩衝策略彈性**
   - CLI 工具：使用 line-buffered 即時輸出
   - GUI 工具：使用 `communicate()` 獲取完整輸出

## Technical Feasibility

### 可行的修復方向

#### 方向 1: 添加 Windows 特定 creationflags ⭐ 推薦

**可行性**: ✅ 高

**方法**:
```python
import sys
import subprocess

creationflags = 0
if sys.platform == 'win32':
    creationflags = subprocess.CREATE_NO_WINDOW

process = subprocess.Popen(
    cmd,
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT,
    text=True,
    encoding="utf-8",
    errors="replace",
    cwd=str(makefile_dir),
    bufsize=1,
    creationflags=creationflags,  # ← 新增
)
```

**優點**:
- 最小改動
- 明確針對 Windows GUI 應用問題
- 不影響 Linux/macOS 行為

**缺點**:
- 需要驗證 Keil UV4 在 CREATE_NO_WINDOW 下是否正常

#### 方向 2: 添加 Timeout 保護

**可行性**: ✅ 高

**方法**:
```python
try:
    process.wait(timeout=300)  # 5 分鐘 timeout
except subprocess.TimeoutExpired:
    process.kill()
    error_msg = "Build timeout (5 minutes)"
    # ... 錯誤處理
```

**優點**:
- 防止無限 hang
- 提供使用者反饋

**缺點**:
- 不解決根本問題，只是緩解

#### 方向 3: 針對 GUI 工具使用 communicate()

**可行性**: ⚠️ 中等

**方法**:
```python
# 判斷是否為 GUI 工具
is_gui_tool = "UV4.exe" in cmd[0]

if is_gui_tool:
    # GUI 工具：使用 communicate() 獲取完整輸出
    stdout, _ = process.communicate(timeout=300)
    # 一次性輸出
    print(stdout)
else:
    # CLI 工具：使用 line-by-line 即時輸出
    for line in process.stdout:
        print(line, end="", flush=True)
```

**優點**:
- 針對性處理不同工具類型
- GUI 工具不再依賴 line buffering

**缺點**:
- 失去即時輸出 (編譯完成後才顯示)
- 需要維護工具類型列表

#### 方向 4: 驗證 Keil UV4 -b 模式行為

**可行性**: ✅ 必要

**方法**:
1. 使用 Process Monitor 追蹤 Keil UV4 行為
2. 測試 Keil -b 是否正確輸出到 stdout
3. 檢查 Keil 文件確認 -j0 參數意義

**重要性**: 高 - 必須確認問題根源

### 推薦的驗證步驟

```mermaid
graph TD
    A[1. 驗證 Keil -b 行為] --> B{stdout 正常?}
    B -->|是| C[2. 測試 CREATE_NO_WINDOW]
    B -->|否| D[考慮 communicate 方案]

    C --> E{解決?}
    E -->|是| F[3. 添加 timeout 保護]
    E -->|否| D

    D --> G[4. 實作工具類型感知]

    F --> H[完成]
    G --> H

    style A fill:#ffffcc
    style H fill:#ccffcc
```

## Historical Context (from .ai/rpi)

### 相關研究文件

1. **[2025-10-10-integration-testing-layer2-summary.md](.ai/rpi/research/2025-10-10-integration-testing-layer2-summary.md)**
   - 發現並修復 `process.returncode` bug
   - Layer 2 測試使用 mock Makefile，未執行真實 Keil
   - 因此未捕獲此 Windows GUI 特定問題

2. **[2025-10-10-wsl-test-failures.md](.ai/rpi/research/2025-10-10-wsl-test-failures.md)**
   - 分析 Windows vs WSL 測試差異
   - 發現 Makefile Tab 字元問題
   - 跨平台測試的重要性

3. **[2025-10-09-makefile-integration-research.md](.ai/rpi/research/2025-10-09-makefile-integration-research.md)**
   - 原始 Makefile 整合設計
   - 定義 build/flash Makefile 契約
   - 硬編碼 Keil 和 ST-LINK 路徑

### 演進歷史

```mermaid
timeline
    title FWAuto Subprocess 演進
    2025-10-09 : Makefile 整合設計
              : 使用 subprocess.run()
    2025-10-10 : 重構為 Popen (b7afeb8)
              : 目標: 即時輸出
              : 引入: 潛在 hang 問題
    2025-10-10 : 修復 returncode bug (070830e)
              : Layer 2 測試捕獲
    2025-10-10 : 本次研究
              : 發現: Windows GUI 應用特定問題
```

## Open Questions

### 需要進一步驗證的問題

1. **Keil UV4 -b 模式的實際行為**
   - `-b` 是否真的是 non-interactive batch mode？
   - 是否正確輸出到 stdout？
   - `-j0` 參數的確切意義？

2. **CREATE_NO_WINDOW 的副作用**
   - Keil UV4 在 CREATE_NO_WINDOW 下是否正常運作？
   - 是否有授權或 dongle 檢查問題？

3. **為何 Terminal 執行正常？**
   - Terminal 的 stdout 處理與 subprocess.PIPE 有何不同？
   - 是否因為 Terminal 有 TTY 屬性？

4. **其他用戶是否遇到相同問題？**
   - 這是個案還是普遍問題？
   - 不同 Keil 版本是否有差異？

### 建議的診斷方法

1. **Process Monitor 追蹤**
   ```
   - 執行 build node (hang 狀態)
   - 使用 Process Monitor 記錄：
     - Keil UV4 創建的進程
     - 檔案 I/O 操作
     - Registry 存取
   ```

2. **手動 subprocess 測試**
   ```python
   # 簡化測試腳本
   import subprocess

   cmd = [r"C:\Keil_v5\UV4\UV4.exe", "-b", "project.uvprojx"]

   # 測試 1: 不使用 PIPE
   subprocess.run(cmd)

   # 測試 2: 使用 PIPE
   result = subprocess.run(cmd, capture_output=True, text=True)
   print(result.stdout)

   # 測試 3: 使用 Popen + line iteration
   process = subprocess.Popen(cmd, stdout=subprocess.PIPE, text=True, bufsize=1)
   for line in process.stdout:
       print(line)
   ```

3. **Keil 命令列文件查詢**
   - 確認 `-b` 和 `-j0` 參數的官方說明
   - 檢查是否有其他相關參數 (如強制 stdout flush)

## Related Research

- [2025-10-10-integration-testing-layer2-summary.md](.ai/rpi/research/2025-10-10-integration-testing-layer2-summary.md) - Layer 2 測試實施與 bug 發現
- [2025-10-10-wsl-test-failures.md](.ai/rpi/research/2025-10-10-wsl-test-failures.md) - 跨平台測試問題分析
- [2025-10-09-makefile-integration-research.md](.ai/rpi/research/2025-10-09-makefile-integration-research.md) - Makefile 整合設計原始研究
