資料夾被占用 #2

Open
opened 2026-04-16 15:04:58 +08:00 by MarioYang · 1 comment
Owner

即便關閉程式,repo 資料夾仍有被占用的跡象,導致無法更名、刪除

即便關閉程式,repo 資料夾仍有被占用的跡象,導致無法更名、刪除
Author
Owner

[!NOTE]
Automated reply generated by code_work_spawner via gitea as @MarioYang.
Review it as bot output, not as a direct human response.

分析:資料夾被占用的根本原因

問題來源(已驗證)

檢查程式碼後,找到兩個主要原因:


1. disposeEphemeralWorkspace 先開啟 GitDir 再刪除(scm_client.dart:69

Future<void> disposeEphemeralWorkspace(String workspacePath) async {
  await GitDir.fromExisting(workspacePath); // ← 開啟 git handles 做驗證
  final result = await _runGit([
    '-C', workspacePath,                    // ← git process CWD = workspacePath
    'worktree', 'remove', '--force', workspacePath,
  ]);
  ...
  await tempRoot.delete(recursive: true);   // ← 此時 handle 可能尚未完全釋放
}

Windows 上 git worktree remove 執行過程中,workspacePath 是該 git process 的 CWD,加上 GitDir.fromExisting 開啟的 handle,兩者在 Windows 上都會短暫佔用資料夾。


2. Responder 子程序(及其孫程序)在 Windows 不會隨父程序一起結束(cli_responder.dart:93-96

final process = await startExternalCommand(
  responder.command,
  arguments,
  workingDirectory: workspacePath,  // ← 子程序 CWD = workspace
  ...
);

Windows 上,子程序不像 Unix 會繼承 process group;Dart runtime 也沒有建立 Windows Job Object 來綁定孫程序的生命週期。

流程:

  1. Responder 以 workspacePath 為 CWD 啟動(例如呼叫 gh copilot / node 等)
  2. Responder 本身再派生孫程序,孫程序繼承 CWD = workspacePath
  3. 主程式結束或呼叫 kill() ──孫程序仍存活,繼續佔用該資料夾

建議修法

短期(scm_client.dart)──移除多餘的 GitDir.fromExisting,並在 Windows 加入 retry

@override
Future<void> disposeEphemeralWorkspace(String workspacePath) async {
  // 移除 GitDir.fromExisting 驗證,避免在 Windows 上額外持有 handle
  final result = await _runGit([
    '-C', workspacePath,
    'worktree', 'remove', '--force', workspacePath,
  ]);

  final tempRoot = Directory(p.dirname(workspacePath));
  if (!await tempRoot.exists()) return;

  // Windows 上 git/AV 軟體可能短暫持有 handle,加入 retry
  for (var attempt = 0; attempt < 5; attempt++) {
    try {
      await tempRoot.delete(recursive: true);
      return;
    } catch (_) {
      if (attempt == 4) rethrow;
      await Future.delayed(Duration(milliseconds: 300 * (attempt + 1)));
    }
  }
}

長期(cli_responder.dart)──不使用 workspace 作為 CWD

Responder 已透過 CWS_WORKSPACE_PATH 環境變數拿到路徑,因此可改用 project path(或系統暫存目錄)作為 workingDirectory,避免孫程序繼承並持有 workspace CWD:

// 改用 project path 而非 workspacePath 作為 CWD
final process = await startExternalCommand(
  responder.command,
  arguments,
  workingDirectory: project.path,  // 或 Directory.systemTemp.path
  environment: {
    ...responder.env,
    'CWS_WORKSPACE_PATH': workspacePath,
    // ...
  },
);

⚠️ 此為 breaking change,會影響依賴相對路徑的 responder script,需確認現有腳本均已改用 CWS_WORKSPACE_PATH


摘要

原因 位置 嚴重度
GitDir.fromExisting 在刪除前開啟 handle scm_client.dart:69
git -C workspacePath 的 CWD handle 釋放時序問題 scm_client.dart:70-75
Responder 孫程序在 Windows 不隨主程式結束,繼承 CWD = workspace cli_responder.dart:93-96

Automation note: generated by code_work_spawner via gitea as @MarioYang.

> [!NOTE] > Automated reply generated by `code_work_spawner` via `gitea` as `@MarioYang`. > Review it as bot output, not as a direct human response. ## 分析:資料夾被占用的根本原因 ### 問題來源(已驗證) 檢查程式碼後,找到兩個主要原因: --- #### 1. `disposeEphemeralWorkspace` 先開啟 GitDir 再刪除(`scm_client.dart:69`) ```dart Future<void> disposeEphemeralWorkspace(String workspacePath) async { await GitDir.fromExisting(workspacePath); // ← 開啟 git handles 做驗證 final result = await _runGit([ '-C', workspacePath, // ← git process CWD = workspacePath 'worktree', 'remove', '--force', workspacePath, ]); ... await tempRoot.delete(recursive: true); // ← 此時 handle 可能尚未完全釋放 } ``` Windows 上 `git worktree remove` 執行過程中,`workspacePath` 是該 git process 的 CWD,加上 `GitDir.fromExisting` 開啟的 handle,兩者在 Windows 上都會短暫佔用資料夾。 --- #### 2. Responder 子程序(及其孫程序)在 Windows 不會隨父程序一起結束(`cli_responder.dart:93-96`) ```dart final process = await startExternalCommand( responder.command, arguments, workingDirectory: workspacePath, // ← 子程序 CWD = workspace ... ); ``` Windows 上,子程序不像 Unix 會繼承 process group;Dart runtime 也**沒有建立 Windows Job Object** 來綁定孫程序的生命週期。 流程: 1. Responder 以 `workspacePath` 為 CWD 啟動(例如呼叫 `gh copilot` / `node` 等) 2. Responder 本身再派生孫程序,孫程序繼承 CWD = `workspacePath` 3. 主程式結束或呼叫 `kill()` ──孫程序仍存活,繼續佔用該資料夾 --- ### 建議修法 #### 短期(`scm_client.dart`)──移除多餘的 `GitDir.fromExisting`,並在 Windows 加入 retry ```dart @override Future<void> disposeEphemeralWorkspace(String workspacePath) async { // 移除 GitDir.fromExisting 驗證,避免在 Windows 上額外持有 handle final result = await _runGit([ '-C', workspacePath, 'worktree', 'remove', '--force', workspacePath, ]); final tempRoot = Directory(p.dirname(workspacePath)); if (!await tempRoot.exists()) return; // Windows 上 git/AV 軟體可能短暫持有 handle,加入 retry for (var attempt = 0; attempt < 5; attempt++) { try { await tempRoot.delete(recursive: true); return; } catch (_) { if (attempt == 4) rethrow; await Future.delayed(Duration(milliseconds: 300 * (attempt + 1))); } } } ``` #### 長期(`cli_responder.dart`)──不使用 workspace 作為 CWD Responder 已透過 `CWS_WORKSPACE_PATH` 環境變數拿到路徑,因此可改用 project path(或系統暫存目錄)作為 `workingDirectory`,避免孫程序繼承並持有 workspace CWD: ```dart // 改用 project path 而非 workspacePath 作為 CWD final process = await startExternalCommand( responder.command, arguments, workingDirectory: project.path, // 或 Directory.systemTemp.path environment: { ...responder.env, 'CWS_WORKSPACE_PATH': workspacePath, // ... }, ); ``` > ⚠️ 此為 **breaking change**,會影響依賴相對路徑的 responder script,需確認現有腳本均已改用 `CWS_WORKSPACE_PATH`。 --- ### 摘要 | 原因 | 位置 | 嚴重度 | |------|------|--------| | `GitDir.fromExisting` 在刪除前開啟 handle | `scm_client.dart:69` | 中 | | `git -C workspacePath` 的 CWD handle 釋放時序問題 | `scm_client.dart:70-75` | 中 | | Responder 孫程序在 Windows 不隨主程式結束,繼承 CWD = workspace | `cli_responder.dart:93-96` | 高 | _Automation note: generated by `code_work_spawner` via `gitea` as `@MarioYang`._ <!-- code-work-spawner:8885171efb7d267523b95438105bfb15def000c335ef4fbda710b0105f9c1296 -->
Sign in to join this conversation.
No Label
No Milestone
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: MarioYang/code_work_spawner#2
No description provided.