# 修復 Build Node Hang 問題實施計劃

## Overview

修復 build node 在 Windows 環境下執行 make 時卡住的問題。透過提取共用的 subprocess 執行邏輯，在 Windows 平台添加 `CREATE_NO_WINDOW` flag，並加入 timeout 保護，同時保持 build 和 flash nodes 的一致性與跨平台相容性。

## Current State Analysis

### 現況

**問題描述**：
- build node 執行 `make build` 時會卡住
- flash node 執行 `make flash` 正常完成
- 直接在 terminal 執行 `make build` 可正常完成

**程式碼現況** (commit: 1e1f887):
- `build.py` 和 `flash.py` 使用**完全相同**的 subprocess.Popen 實作 (lines 83-112)
- 30 行程式碼重複
- 沒有 Windows 特定的 `creationflags` 設定
- 沒有 timeout 保護機制

**根本原因**（來自研究文件 `.ai/rpi/research/2025-10-10-build-make-hang-issue.md`）：
1. **工具類型差異**：
   - Build 執行 Keil UV4（GUI 應用程式）
   - Flash 執行 ST-LINK CLI（純命令列工具）
2. **缺少 Windows 特定處理**：
   - 未設定 `subprocess.CREATE_NO_WINDOW`
   - GUI 應用程式可能創建視窗或不正確刷新 stdout
3. **無 timeout 保護**：
   - `process.wait()` 可能無限等待

### Key Discoveries

- **驗證結果**：直接執行 `make build` 在 `C:\STM32F407\examples\實例1 跑馬燈實例\.fwauto\build\Makefile` 可正常完成 ✅
- **subprocess.CREATE_NO_WINDOW**：
  - 在 Windows 上阻止彈出控制台視窗
  - 在 Linux/macOS 上，`creationflags` 參數必須省略或為 0，否則會拋出 `ValueError`
- **程式碼重複**：build.py 和 flash.py 的 subprocess 邏輯完全相同，適合提取
- **未來擴展性**：基於 Makefile 架構，未來可能需要 `make test`、`make clean` 等 nodes

## Desired End State

### 功能目標

1. ✅ Build node 在 Windows 上執行 make 不會卡住
2. ✅ Build 和 flash nodes 使用一致的 subprocess 處理邏輯
3. ✅ 跨平台相容（Windows、Linux、macOS）
4. ✅ 有 timeout 保護機制（60 秒）
5. ✅ 消除程式碼重複，提升可維護性

### 驗證方式

**自動化驗證**：
- [ ] 所有單元測試通過：`uv run pytest tests/nodes/test_build_makefile.py -v`
- [ ] 所有單元測試通過：`uv run pytest tests/nodes/test_flash_makefile.py -v`
- [ ] 新增的工具函數測試通過：`uv run pytest tests/test_utils.py -v`
- [ ] 所有測試套件通過：`uv run pytest`

**手動驗證**：
- [ ] 在 Windows 環境執行 `fwauto build` 不會卡住且成功完成
- [ ] 在 Windows 環境執行 `fwauto flash` 仍正常運作
- [ ] 在 Linux 環境（如果可用）執行 build 和 flash 正常
- [ ] Timeout 機制有效（可透過創建永不完成的測試 Makefile 驗證）

## What We're NOT Doing

以下項目**不在本次修復範圍內**：

❌ 不修改 Makefile 本身（問題在 Python subprocess 處理，不在 Makefile）
❌ 不改變 build/flash 的輸入輸出介面（保持向後相容）
❌ 不重構其他 nodes（只處理 build 和 flash）
❌ 不添加新的配置選項（timeout 固定為 60 秒）
❌ 不改變日誌格式或輸出方式
❌ 不進行 Keil UV4 或 ST-LINK CLI 的行為驗證（直接執行 make 已證實可行）

## Implementation Approach

### 策略

採用**提取共用邏輯 + 平台特定處理**的方式：

1. **提取原則**：將重複的 subprocess 邏輯提取到 `utils.py`，避免未來維護多處
2. **平台偵測**：使用 `sys.platform == 'win32'` 判斷是否為 Windows
3. **Windows 特定處理**：僅在 Windows 設定 `creationflags=subprocess.CREATE_NO_WINDOW`
4. **安全機制**：添加 60 秒 timeout，防止無限等待
5. **漸進式重構**：分階段修改，每個 phase 都可獨立測試和驗證

### 設計原則

- **KISS**：提取共用邏輯簡化程式碼，而非增加複雜度
- **一致性**：build 和 flash 使用相同的底層實作
- **可測試性**：工具函數可獨立測試，node 測試可 mock 工具函數
- **向後相容**：不改變 node 的外部介面和行為

---

## Phase 1: 提取共用 Subprocess 執行邏輯

### Overview

在 `utils.py` 新增 `execute_make_command()` 函數，包含跨平台的 subprocess 處理邏輯和 timeout 保護。

### Changes Required

#### 1. 更新 `src/fwauto/utils.py`

**File**: `src/fwauto/utils.py`

**Changes**: 在檔案末尾新增以下函數

```python
def execute_make_command(
    makefile_path: Path,
    target: str,
    project_root: Path,
    timeout: int = 60,
) -> tuple[int, str]:
    """
    Execute make command with real-time output and cross-platform compatibility.

    Args:
        makefile_path: Path to Makefile
        target: Make target to execute (e.g., "build", "flash")
        project_root: Project root directory
        timeout: Command timeout in seconds (default: 60)

    Returns:
        Tuple of (returncode, full_output)

    Raises:
        subprocess.TimeoutExpired: If command exceeds timeout
    """
    import subprocess
    import sys

    from .logging_config import get_logger

    makefile_dir = makefile_path.parent
    cmd = ["make", target, f"PROJECT_ROOT={project_root}"]

    logger = get_logger(__name__)
    logger.info(f"Executing: {' '.join(cmd)} in {makefile_dir}")

    # 準備 Popen 參數
    popen_kwargs = {
        "stdout": subprocess.PIPE,
        "stderr": subprocess.STDOUT,  # 合併 stderr 到 stdout
        "text": True,
        "encoding": "utf-8",
        "errors": "replace",
        "cwd": str(makefile_dir),
        "bufsize": 1,  # Line buffered
    }

    # Windows 平台：阻止創建控制台視窗（修復 GUI 應用程式如 Keil UV4 的問題）
    if sys.platform == "win32":
        popen_kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW

    # 執行命令
    process = subprocess.Popen(cmd, **popen_kwargs)

    # 即時輸出並收集
    output_lines = []
    print("")  # 空行分隔
    try:
        for line in process.stdout:
            print(line, end="", flush=True)  # 即時輸出到 console
            output_lines.append(line)  # 同時收集用於 log
    except Exception as e:
        process.kill()
        raise RuntimeError(f"Error reading output: {e}") from e

    # 等待程序結束（帶 timeout）
    try:
        process.wait(timeout=timeout)
    except subprocess.TimeoutExpired:
        process.kill()
        process.wait()  # 確保清理
        raise subprocess.TimeoutExpired(
            cmd=cmd, timeout=timeout, output="".join(output_lines)
        )
    finally:
        print("")  # 空行分隔

    # 組合完整輸出
    full_output = f"Command: {' '.join(cmd)}\n"
    full_output += f"Working directory: {makefile_dir}\n"
    full_output += f"Exit code: {process.returncode}\n"
    full_output += "=" * 60 + "\n\n"
    full_output += "".join(output_lines)

    # Log the full output to file
    logger.info(f"Make output:\n{full_output}")

    return process.returncode, full_output
```

**說明**：
- **跨平台處理**：僅在 Windows 設定 `creationflags`
- **Timeout 保護**：60 秒後自動終止並拋出異常
- **即時輸出**：保持與原實作相同的行為
- **錯誤處理**：讀取輸出時的異常處理
- **清理機制**：timeout 或異常時確保進程被終止

### Success Criteria

#### Automated Verification

- [x] 檔案修改成功：檢查 `src/fwauto/utils.py` 包含新函數
- [x] 無語法錯誤：`python -m py_compile src/fwauto/utils.py`
- [x] Import 測試通過：`python -c "from fwauto.utils import execute_make_command"`

#### Manual Verification

- [x] 程式碼審查：確認邏輯正確，包含所有必要的錯誤處理
- [x] 跨平台檢查：確認 Windows 特定處理的條件判斷正確

**Implementation Note**: 完成此 phase 後，暫停並確認程式碼正確再進入 Phase 2。

---

## Phase 2: 重構 build_node 使用新工具函數

### Overview

修改 `build.py`，將 subprocess 執行邏輯替換為 `execute_make_command()` 呼叫。

### Changes Required

#### 1. 更新 `src/fwauto/nodes/build.py`

**File**: `src/fwauto/nodes/build.py`

**Changes**: 替換 lines 71-149 的 subprocess 執行邏輯

**移除的程式碼區塊** (lines 71-112)：
```python
    # 3. Execute make build
    try:
        makefile_path = project_root_path / config.build_makefile
        makefile_dir = makefile_path.parent
        target = config.build_target

        # Execute make command with real-time output
        cmd = ["make", target, f"PROJECT_ROOT={project_root_path}"]
        logger.info(f"Executing: {' '.join(cmd)} in {makefile_dir}")

        # 使用 Popen 來即時輸出並捕獲結果

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

        # 即時輸出並收集
        output_lines = []
        print("")  # 空行分隔
        for line in process.stdout:
            print(line, end="", flush=True)  # 即時輸出到 console
            output_lines.append(line)  # 同時收集用於 log

        process.wait()  # 等待程序結束
        print("")  # 空行分隔

        # 組合完整輸出
        full_output = f"Command: {' '.join(cmd)}\n"
        full_output += f"Working directory: {makefile_dir}\n"
        full_output += f"Exit code: {process.returncode}\n"
        full_output += "=" * 60 + "\n\n"
        full_output += "".join(output_lines)

        # Log the full output to file
        logger.info(f"Make output:\n{full_output}")
```

**新增的程式碼** (替換上述區塊)：
```python
    # 3. Execute make build
    try:
        makefile_path = project_root_path / config.build_makefile
        target = config.build_target

        # 使用共用工具函數執行 make（包含跨平台處理和 timeout 保護）
        from ..utils import execute_make_command

        returncode, full_output = execute_make_command(
            makefile_path=makefile_path,
            target=target,
            project_root=project_root_path,
            timeout=60,  # 60 秒 timeout
        )
```

**更新的錯誤處理** (lines 114-128，更新為處理 TimeoutExpired)：
```python
        # Check success
        if returncode == 0:
            success_msg = "Build succeeded"
            log_node_success("build", success_msg)

            result_state = {
                **state,
                "build_status": "success",
                "build_errors": [],
                "build_log": f"{full_output}\n✅ {success_msg}",
            }

            log_node_output_state("build", result_state)
            return result_state
        else:
            error_msg = f"Build failed with exit code {returncode}"
            log_node_error("build", error_msg)

            return {
                **state,
                "build_status": "error",
                "build_errors": [error_msg],
                "build_log": f"{full_output}\n❌ {error_msg}",
                "iteration": state.get("iteration", 0) + 1,
            }

    except subprocess.TimeoutExpired as e:
        error_msg = f"Build timeout after {e.timeout} seconds"
        log_node_error("build", error_msg)
        output = e.output if e.output else "No output captured"
        return {
            **state,
            "build_status": "error",
            "build_errors": [error_msg],
            "build_log": f"{output}\n❌ {error_msg}",
            "iteration": state.get("iteration", 0) + 1,
        }
    except Exception as e:
        error_msg = f"Build exception: {str(e)}"
        log_node_error("build", error_msg)
        return {
            **state,
            "build_status": "error",
            "build_errors": [error_msg],
            "build_log": f"❌ {error_msg}",
            "iteration": state.get("iteration", 0) + 1,
        }
```

**移除不再需要的 import** (line 3)：
```python
import subprocess  # ← 移除這行（已由 utils.execute_make_command 處理）
```

### Success Criteria

#### Automated Verification

- [x] 檔案修改成功：檢查 `src/fwauto/nodes/build.py` 已更新
- [x] 無語法錯誤：`python -m py_compile src/fwauto/nodes/build.py`
- [x] Build node 單元測試通過：`uv run pytest tests/nodes/test_build_makefile.py -v`
- [x] 所有測試通過：`uv run pytest tests/ -v`

#### Manual Verification

- [ ] 執行 `fwauto build` 在真實專案上成功完成
- [ ] 輸出格式與修改前一致（即時顯示、格式化輸出）
- [ ] 錯誤處理正確（編譯錯誤時顯示適當訊息）
- [ ] Timeout 機制測試（可選：建立一個永不完成的測試 Makefile）

**Implementation Note**: 完成此 phase 並驗證成功後，再進入 Phase 3。

---

## Phase 3: 重構 flash_node 使用新工具函數

### Overview

修改 `flash.py`，將 subprocess 執行邏輯替換為 `execute_make_command()` 呼叫，保持與 build_node 一致。

### Changes Required

#### 1. 更新 `src/fwauto/nodes/flash.py`

**File**: `src/fwauto/nodes/flash.py`

**Changes**: 替換 lines 71-147 的 subprocess 執行邏輯（與 build.py 的修改類似）

**移除的程式碼區塊** (lines 71-112)：
```python
    # 3. Execute make flash
    try:
        makefile_path = project_root_path / config.flash_makefile
        makefile_dir = makefile_path.parent
        target = config.flash_target

        # Execute make command with real-time output
        cmd = ["make", target, f"PROJECT_ROOT={project_root_path}"]
        logger.info(f"Executing: {' '.join(cmd)} in {makefile_dir}")

        # 使用 Popen 來即時輸出並捕獲結果

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

        # 即時輸出並收集
        output_lines = []
        print("")  # 空行分隔
        for line in process.stdout:
            print(line, end="", flush=True)  # 即時輸出到 console
            output_lines.append(line)  # 同時收集用於 log

        process.wait()  # 等待程序結束
        print("")  # 空行分隔

        # 組合完整輸出
        full_output = f"Command: {' '.join(cmd)}\n"
        full_output += f"Working directory: {makefile_dir}\n"
        full_output += f"Exit code: {process.returncode}\n"
        full_output += "=" * 60 + "\n\n"
        full_output += "".join(output_lines)

        # Log the full output to file
        logger.info(f"Make output:\n{full_output}")
```

**新增的程式碼** (替換上述區塊)：
```python
    # 3. Execute make flash
    try:
        makefile_path = project_root_path / config.flash_makefile
        target = config.flash_target

        # 使用共用工具函數執行 make（包含跨平台處理和 timeout 保護）
        from ..utils import execute_make_command

        returncode, full_output = execute_make_command(
            makefile_path=makefile_path,
            target=target,
            project_root=project_root_path,
            timeout=60,  # 60 秒 timeout
        )
```

**更新的錯誤處理** (lines 114-147)：
```python
        # Check success
        if returncode == 0:
            success_msg = "✅ 燒錄成功!"
            log_node_success("flash", success_msg)

            result_state = {
                **state,
                "flash_status": "success",
                "flash_errors": [],
                "flash_log": f"{full_output}\n{success_msg}",
            }

            log_node_output_state("flash", result_state)
            return result_state
        else:
            error_msg = f"Flash failed with exit code {returncode}"
            log_node_error("flash", error_msg)

            return {
                **state,
                "flash_status": "error",
                "flash_errors": [error_msg],
                "flash_log": f"{full_output}\n❌ {error_msg}",
            }

    except subprocess.TimeoutExpired as e:
        error_msg = f"Flash timeout after {e.timeout} seconds"
        log_node_error("flash", error_msg)
        output = e.output if e.output else "No output captured"
        return {
            **state,
            "flash_status": "error",
            "flash_errors": [error_msg],
            "flash_log": f"{output}\n❌ {error_msg}",
        }
    except Exception as e:
        error_msg = f"Flash exception: {str(e)}"
        log_node_error("flash", error_msg)
        return {
            **state,
            "flash_status": "error",
            "flash_errors": [error_msg],
            "flash_log": f"❌ {error_msg}",
        }
```

**移除不再需要的 import** (line 3)：
```python
import subprocess  # ← 移除這行（已由 utils.execute_make_command 處理）
```

### Success Criteria

#### Automated Verification

- [ ] 檔案修改成功：檢查 `src/fwauto/nodes/flash.py` 已更新
- [ ] 無語法錯誤：`python -m py_compile src/fwauto/nodes/flash.py`
- [ ] Flash node 單元測試通過：`uv run pytest tests/nodes/test_flash_makefile.py -v`
- [ ] 所有測試通過：`uv run pytest tests/ -v`

#### Manual Verification

- [ ] 執行 `fwauto flash` 在真實專案上成功完成
- [ ] 輸出格式與修改前一致
- [ ] 錯誤處理正確（硬體錯誤時顯示適當訊息）
- [ ] Build 和 flash 行為保持一致

**Implementation Note**: 完成此 phase 並驗證成功後，再進入 Phase 4。

---

## Phase 4: 更新測試策略

### Overview

新增 `execute_make_command()` 的單元測試，並調整現有 node 測試的 mock 策略。

### Changes Required

#### 1. 新增 `tests/test_utils.py`

**File**: `tests/test_utils.py`

**Changes**: 新增測試檔案

```python
"""Tests for fwauto.utils module."""

import subprocess
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from fwauto.utils import execute_make_command


class TestExecuteMakeCommand:
    """Tests for execute_make_command function."""

    @patch("fwauto.utils.subprocess.Popen")
    def test_execute_make_command_success(self, mock_popen):
        """Test successful make execution."""
        # Arrange
        mock_process = MagicMock()
        mock_process.returncode = 0
        mock_process.stdout = iter([
            "Building...\n",
            "Compile complete\n",
            "Build succeeded\n",
        ])
        mock_popen.return_value = mock_process

        makefile_path = Path("/fake/project/.fwauto/build/Makefile")
        target = "build"
        project_root = Path("/fake/project")

        # Act
        returncode, output = execute_make_command(
            makefile_path=makefile_path,
            target=target,
            project_root=project_root,
            timeout=60,
        )

        # Assert
        assert returncode == 0
        assert "Building..." in output
        assert "Build succeeded" in output
        assert "Exit code: 0" in output
        mock_popen.assert_called_once()

    @patch("fwauto.utils.subprocess.Popen")
    def test_execute_make_command_failure(self, mock_popen):
        """Test failed make execution."""
        # Arrange
        mock_process = MagicMock()
        mock_process.returncode = 2
        mock_process.stdout = iter([
            "Building...\n",
            "Error: compilation failed\n",
        ])
        mock_popen.return_value = mock_process

        makefile_path = Path("/fake/project/.fwauto/build/Makefile")
        target = "build"
        project_root = Path("/fake/project")

        # Act
        returncode, output = execute_make_command(
            makefile_path=makefile_path,
            target=target,
            project_root=project_root,
            timeout=60,
        )

        # Assert
        assert returncode == 2
        assert "compilation failed" in output
        assert "Exit code: 2" in output

    @patch("fwauto.utils.subprocess.Popen")
    def test_execute_make_command_timeout(self, mock_popen):
        """Test make execution timeout."""
        # Arrange
        mock_process = MagicMock()
        mock_process.stdout = iter([
            "Building...\n",
            "Still building...\n",
        ])
        mock_process.wait.side_effect = subprocess.TimeoutExpired(
            cmd=["make", "build"], timeout=60
        )
        mock_popen.return_value = mock_process

        makefile_path = Path("/fake/project/.fwauto/build/Makefile")
        target = "build"
        project_root = Path("/fake/project")

        # Act & Assert
        with pytest.raises(subprocess.TimeoutExpired):
            execute_make_command(
                makefile_path=makefile_path,
                target=target,
                project_root=project_root,
                timeout=60,
            )

        # Verify process was killed
        mock_process.kill.assert_called_once()

    @patch("fwauto.utils.sys.platform", "win32")
    @patch("fwauto.utils.subprocess.Popen")
    def test_execute_make_command_windows_creationflags(self, mock_popen):
        """Test that CREATE_NO_WINDOW is set on Windows."""
        # Arrange
        mock_process = MagicMock()
        mock_process.returncode = 0
        mock_process.stdout = iter(["Build complete\n"])
        mock_popen.return_value = mock_process

        makefile_path = Path("/fake/project/.fwauto/build/Makefile")
        target = "build"
        project_root = Path("/fake/project")

        # Act
        execute_make_command(
            makefile_path=makefile_path,
            target=target,
            project_root=project_root,
            timeout=60,
        )

        # Assert
        call_kwargs = mock_popen.call_args[1]
        assert "creationflags" in call_kwargs
        assert call_kwargs["creationflags"] == subprocess.CREATE_NO_WINDOW

    @patch("fwauto.utils.sys.platform", "linux")
    @patch("fwauto.utils.subprocess.Popen")
    def test_execute_make_command_linux_no_creationflags(self, mock_popen):
        """Test that creationflags is NOT set on Linux."""
        # Arrange
        mock_process = MagicMock()
        mock_process.returncode = 0
        mock_process.stdout = iter(["Build complete\n"])
        mock_popen.return_value = mock_process

        makefile_path = Path("/fake/project/.fwauto/build/Makefile")
        target = "build"
        project_root = Path("/fake/project")

        # Act
        execute_make_command(
            makefile_path=makefile_path,
            target=target,
            project_root=project_root,
            timeout=60,
        )

        # Assert
        call_kwargs = mock_popen.call_args[1]
        assert "creationflags" not in call_kwargs
```

**說明**：
- 測試成功、失敗、timeout 三種情境
- 驗證 Windows 和 Linux 平台的 creationflags 處理
- 確認 process 在 timeout 時被正確清理

#### 2. 更新現有測試的 mock 策略（可選）

**Files**:
- `tests/nodes/test_build_makefile.py`
- `tests/nodes/test_flash_makefile.py`

**Changes**: 目前的測試仍然有效，因為：
1. 它們 mock `subprocess.Popen`，而 `execute_make_command()` 內部使用它
2. Mock 會被傳遞到工具函數內部
3. 測試行為不變

**可選的優化**（不在此次必要範圍內）：
- 可以改為 mock `execute_make_command` 而非 `subprocess.Popen`
- 這樣測試更直接，但需要重寫所有測試案例

**決策**：保持現有測試不變，僅新增 `test_utils.py`

### Success Criteria

#### Automated Verification

- [ ] 新測試檔案創建成功：檢查 `tests/test_utils.py` 存在
- [ ] 新測試通過：`uv run pytest tests/test_utils.py -v`
- [ ] 所有測試通過：`uv run pytest tests/ -v`
- [ ] 測試覆蓋率維持或提升：`uv run pytest --cov=fwauto --cov-report=term`

#### Manual Verification

- [ ] 檢視測試輸出，確認所有測試案例都正確執行
- [ ] 確認 Windows 和 Linux 的平台特定測試都通過

**Implementation Note**: 完成所有測試後，進行最終的整合驗證。

---

## Testing Strategy

### Unit Tests

**新增測試**：
- `tests/test_utils.py`: 測試 `execute_make_command()` 的各種情境
  - 成功執行
  - 失敗執行（非零 exit code）
  - Timeout 觸發
  - Windows 平台 creationflags 設定
  - Linux 平台無 creationflags

**現有測試**（保持不變）：
- `tests/nodes/test_build_makefile.py`: Build node 的完整測試
- `tests/nodes/test_flash_makefile.py`: Flash node 的完整測試
- `tests/integration/test_build_integration.py`: Layer 2 整合測試

### Integration Tests

**現有的 Layer 2 整合測試仍然有效**：
- 使用真實的 Makefile fixtures
- 測試完整的 build/flash 流程
- Mock 策略不受影響（mock 仍然作用於底層 subprocess.Popen）

### Manual Testing Steps

**Windows 環境**：
1. 在真實 STM32 專案目錄執行：
   ```bash
   cd "C:\STM32F407\examples\實例1 跑馬燈實例"
   fwauto build
   ```
   - ✅ 驗證：不卡住，正常完成
   - ✅ 驗證：輸出即時顯示
   - ✅ 驗證：成功訊息正確

2. 執行 flash：
   ```bash
   fwauto flash
   ```
   - ✅ 驗證：行為與修改前一致
   - ✅ 驗證：燒錄成功

3. 測試 timeout（可選）：
   - 建立一個包含 `sleep 120` 的測試 Makefile
   - 執行後應在 60 秒內終止並顯示 timeout 錯誤

**Linux 環境（如果可用）**：
1. 執行相同的 build 和 flash 測試
2. 驗證跨平台相容性

**錯誤處理測試**：
1. 故意引入編譯錯誤（修改原始碼）
2. 執行 `fwauto build`
3. ✅ 驗證：錯誤訊息正確顯示
4. ✅ 驗證：exit code 正確傳遞

## Performance Considerations

### 效能影響分析

**預期影響**：✅ 無顯著效能影響或輕微改善

**分析**：
1. **函數呼叫開銷**：新增一層函數呼叫，但開銷微乎其微（< 1ms）
2. **Subprocess 執行時間**：不變（仍然是相同的 make 命令）
3. **Timeout 機制**：不影響正常執行路徑
4. **Windows CREATE_NO_WINDOW**：可能輕微加快（減少視窗創建開銷）

**實測基準**（可選）：
- 修改前：記錄 `fwauto build` 的執行時間
- 修改後：比對執行時間
- 預期差異：< 5% 或無差異

### 記憶體使用

**預期影響**：無變化

**分析**：
- 輸出仍然是逐行收集（與原實作相同）
- 無額外的大型資料結構
- Timeout 機制不增加記憶體開銷

## Migration Notes

### 向後相容性

✅ **完全向後相容**

- Node 的輸入輸出介面不變
- State schema 不變
- 配置檔案格式不變
- Makefile 介面不變
- 日誌格式不變（除了可能多出 timeout 錯誤訊息）

### 部署注意事項

1. **無需資料遷移**：此修復不涉及資料格式變更
2. **無需配置更新**：現有專案的 `.fwauto/` 配置無需修改
3. **測試建議**：部署後在開發環境先測試 build 和 flash
4. **回滾計劃**：如有問題，可直接 revert 到修改前的 commit

### 已知限制

1. **Timeout 固定為 60 秒**：不可配置（未來可擴展為配置項）
2. **僅修復 build/flash**：其他可能需要 subprocess 的 nodes 未涵蓋（目前不存在）
3. **依賴 sys.platform**：假設 `win32` 代表 Windows（通常正確）

## References

### 原始研究

- **Research document**: `.ai/rpi/research/2025-10-10-build-make-hang-issue.md`
  - 問題根本原因分析
  - 跨平台 subprocess 行為研究
  - 修復方向評估

### 相關實作

- **Current implementation**:
  - `src/fwauto/nodes/build.py` (lines 83-112): 原始 subprocess 邏輯
  - `src/fwauto/nodes/flash.py` (lines 83-112): 重複的 subprocess 邏輯

### 相關測試

- **Existing tests**:
  - `tests/nodes/test_build_makefile.py`: Build node 測試
  - `tests/nodes/test_flash_makefile.py`: Flash node 測試
  - `tests/integration/test_build_integration.py`: Layer 2 整合測試

### 技術資源

- **Python subprocess documentation**: https://docs.python.org/3/library/subprocess.html
- **subprocess.CREATE_NO_WINDOW**: Windows-specific flag to prevent console window creation
- **Cross-platform subprocess handling**: Best practices from CPython source code

---

## 附錄：遵循規範確認

✅ **KISS 原則**：提取重複邏輯簡化程式碼，而非增加不必要的抽象
✅ **深思熟慮**：經過詳細研究和驗證，確認修復方向正確
✅ **計劃先行**：制定詳細的 4 階段計劃，每階段可獨立驗證
✅ **討論機制**：在計劃制定過程中已確認所有關鍵決策
✅ **僅計劃**：本文件僅包含實施計劃，不包含實際程式碼修改

**下一步**：請審查此計劃，確認無誤後再開始實施。
