Skip to content

fix(oknte): 修复脚本配置目录含只读文件时备份与复原失败 - #456

Open
qiyinxi wants to merge 1 commit into
devfrom
fix/oknte-config-restore-readonly
Open

fix(oknte): 修复脚本配置目录含只读文件时备份与复原失败#456
qiyinxi wants to merge 1 commit into
devfrom
fix/oknte-config-restore-readonly

Conversation

@qiyinxi

@qiyinxi qiyinxi commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

摘要

  • 用户的 ok-nte 配置目录里带 .git,而 git 的 pack 对象在 Windows 上是只读的。shutil.rmtree(..., ignore_errors=True) 删不掉只读文件且静默跳过,残留文件让随后的 copytree(..., dirs_exist_ok=True) 覆盖时抛 PermissionError
  • 失败沿 preparefinal_taskon_crash 级联:后两者的收尾清理走同一个 _restore_script_config_from_temp,同一个错误连抛三次,最终裹进 ExceptionGroup 以「Task exception was never retrieved」落地——用户的脚本配置既没复原,错误也没抛到界面上。
  • 新增 app/utils/io.force_rmtree(清除只读位后重试,仍失败的条目按原 ignore_errors 语义忽略);_restore_script_config_from_temp 自身兜住异常并降级为告警,临时目录清理移入 finally
  • 线上表现:Sentry AUTO-MAS-BACKEND-2H,7 天 69 次,release auto-mas@v5.4.0-beta.8

检查

  • 新增 tests/tools/test_io_force_rmtree.py:2 个用例通过(含只读文件树的删除)
  • python -m pytest tests --collect-only -q:收集 166 个,退出码 0
  • ruff format 无残留改动
  • 未做人工验证:需要一个含 .git 的 OK-NTE 配置目录跑一次 AutoProxy,确认备份、复原与异常路径都不再报错

未一并处理

app/task/general/manager.py:149app/task/general/AutoProxy.py:547 是同一个 rmtree(ignore_errors=True) + copytree 组合,可能存在同类问题。本 PR 只修 OK-NTE 这条线上报的路径,通用脚本那条线是否要一并换成 force_rmtree 请维护者定夺。

Sourcery 摘要

使 OK-NTE 配置备份和恢复能够适应只读文件,同时保留任务错误处理和清理逻辑。

错误修复:

  • 防止 OK-NTE 脚本配置备份和恢复在配置目录包含只读文件(例如 Git pack 对象)时失败。
  • 确保恢复失败会作为警告记录,而不会掩盖任务错误,也不会中断清理和状态更新。

增强功能:

  • 添加可复用的、支持只读文件的目录树删除功能,并将其用于 OK-NTE 临时文件和脚本配置的清理。

测试:

  • 添加对删除包含只读文件的目录树以及处理缺失路径的测试覆盖。
Original summary in English

Summary by Sourcery

Make OK-NTE configuration backup and restoration resilient to read-only files while preserving task error handling and cleanup.

Bug Fixes:

  • Prevent OK-NTE script configuration backup and restoration from failing when configuration directories contain read-only files, such as Git pack objects.
  • Ensure restoration failures are logged as warnings without masking task errors or interrupting cleanup and state updates.

Enhancements:

  • Add reusable read-only-aware directory-tree removal support and use it for OK-NTE temporary and script configuration cleanup.

Tests:

  • Add coverage for deleting directory trees containing read-only files and for handling missing paths.

用户的 ok-nte 配置目录里带 .git,而 git 的 pack 对象在 Windows 上是只读的。
`shutil.rmtree(..., ignore_errors=True)` 删不掉只读文件且静默跳过,残留文件
让随后的 `copytree(..., dirs_exist_ok=True)` 覆盖时抛 PermissionError。

失败沿 prepare -> final_task -> on_crash 级联:后两者的收尾清理走同一个
`_restore_script_config_from_temp`,于是同一个错误连抛三次,最终裹进
ExceptionGroup 以「Task exception was never retrieved」落地,用户的脚本配置
既没复原,错误也没抛到界面上。

- 新增 `app/utils/io.force_rmtree`:遇到只读文件先清除只读位再重试,仍失败
  的条目按原 ignore_errors 语义忽略
- OK-NTE 的备份与复原路径改用该函数
- `_restore_script_config_from_temp` 自身兜住异常并降级为告警,收尾清理失败
  不再掩盖任务本身的异常,临时目录清理移入 finally

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Aug 29, 2026

Copy link
Copy Markdown

审查者指南

本 PR 针对 Windows 上 OK-NTE 配置目录含只读 .git 对象时备份复原失败的问题,引入可强制删除只读文件的工具,并将复原过程改为异常隔离且始终清理临时目录,同时补充相关回归测试。

具备容错能力的 OK-NTE 配置备份与复原时序图

sequenceDiagram
    participant Task as OKNTE_Task
    participant Temp as Temp_Config
    participant IO as force_rmtree
    participant Config as Script_Config

    Task->>IO: force_rmtree(Temp)
    IO->>IO: shutil.rmtree(path, onexc=_retry_without_readonly)
    IO-->>Task: Temp directory removed
    Task->>Temp: copytree(Script_Config, Temp)
    Task->>Config: Run task
    Task->>Config: _restore_script_config_from_temp()
    alt Folder with original config
        Task->>IO: force_rmtree(tmp_dst)
        Task->>Temp: copytree(Temp, tmp_dst, dirs_exist_ok=True)
        Task->>IO: force_rmtree(Script_Config)
        Task->>Config: tmp_dst.rename(Script_Config)
    else No original folder
        Task->>IO: force_rmtree(Script_Config)
    end
    opt Restore fails
        Task->>Task: logger.warning(...)
    end
    Task->>IO: force_rmtree(Temp)
Loading

OK-NTE 清理失败隔离流程图

flowchart TD
    A[prepare] --> B["force_rmtree(temp_path)"]
    B --> C[Backup script configuration]
    C --> D[Run OK-NTE task]
    D --> E["_restore_script_config_from_temp"]
    E --> F{Restore succeeds?}
    F -->|Yes| G["force_rmtree(temp_path) in finally"]
    F -->|No| H["logger.warning(...)"]
    H --> G
    G --> I[Continue task cleanup and state updates]
Loading

文件级变更

变更 详情 文件
使用可处理只读文件的目录树删除逻辑,避免 Windows 配置备份目录残留导致复原失败。
  • 新增清除只读位并重试删除的 force_rmtree,保留删除失败时的忽略语义。
  • 将 OK-NTE 备份临时目录、原配置目录和临时目标目录的删除切换为 force_rmtree
app/utils/io.py
app/task/OkNte/manager.py
提高 OK-NTE 配置复原流程的故障隔离能力,确保清理失败不影响任务收尾。
  • 捕获目录或文件复原异常并记录告警,避免覆盖任务原始异常及后续解锁、状态回写流程。
  • 将临时目录删除移入 finally,保证复原成功或失败后都尝试清理。
app/task/OkNte/manager.py
为只读目录树删除行为增加回归测试。
  • 覆盖包含只读文件的嵌套目录删除。
  • 覆盖重复删除不存在路径的幂等行为。
tests/tools/test_io_force_rmtree.py
同步格式化相关代码并更新版本资源。
  • 应用 Ruff 格式化调整。
  • 更新版本元数据。
app/task/OkNte/manager.py
app/utils/io.py
res/version.json

提示与命令

使用 Sourcery

  • 触发新的审查: 在拉取请求中评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的审查评论。
  • 根据审查评论生成 GitHub issue: 回复审查评论,请 Sourcery 根据该评论创建 issue。你也可以回复审查评论 @sourcery-ai issue,以根据该评论创建 issue。
  • 生成拉取请求标题: 在拉取请求标题的任意位置写入 @sourcery-ai,即可随时生成标题。你也可以在拉取请求中评论 @sourcery-ai title,以随时(重新)生成标题。
  • 生成拉取请求摘要: 在拉取请求正文中任意位置写入 @sourcery-ai summary,即可在指定位置随时生成 PR 摘要。你也可以在拉取请求中评论 @sourcery-ai summary,以随时(重新)生成摘要。
  • 生成审查者指南: 在拉取请求中评论 @sourcery-ai guide,即可随时(重新)生成审查者指南。
  • 解决所有 Sourcery 评论: 在拉取请求中评论 @sourcery-ai resolve,以解决所有 Sourcery 评论。如果你已经处理完所有评论且不想再看到它们,这个功能会很有用。
  • 忽略所有 Sourcery 审查: 在拉取请求中评论 @sourcery-ai dismiss,以忽略所有现有的 Sourcery 审查。如果你想从头开始一次新的审查,这个功能尤其有用——别忘了评论 @sourcery-ai review 来触发新的审查!

自定义使用体验

访问你的控制面板以:

  • 启用或禁用审查功能,例如 Sourcery 生成的拉取请求摘要、审查者指南等。
  • 更改审查语言。
  • 添加、移除或编辑自定义审查说明。
  • 调整其他审查设置。

获取帮助

Original review guide in English

Reviewer's Guide

本 PR 针对 Windows 上 OK-NTE 配置目录含只读 .git 对象时备份复原失败的问题,引入可强制删除只读文件的工具,并将复原过程改为异常隔离且始终清理临时目录,同时补充相关回归测试。

Sequence diagram for resilient OK-NTE configuration backup and restore

sequenceDiagram
    participant Task as OKNTE_Task
    participant Temp as Temp_Config
    participant IO as force_rmtree
    participant Config as Script_Config

    Task->>IO: force_rmtree(Temp)
    IO->>IO: shutil.rmtree(path, onexc=_retry_without_readonly)
    IO-->>Task: Temp directory removed
    Task->>Temp: copytree(Script_Config, Temp)
    Task->>Config: Run task
    Task->>Config: _restore_script_config_from_temp()
    alt Folder with original config
        Task->>IO: force_rmtree(tmp_dst)
        Task->>Temp: copytree(Temp, tmp_dst, dirs_exist_ok=True)
        Task->>IO: force_rmtree(Script_Config)
        Task->>Config: tmp_dst.rename(Script_Config)
    else No original folder
        Task->>IO: force_rmtree(Script_Config)
    end
    opt Restore fails
        Task->>Task: logger.warning(...)
    end
    Task->>IO: force_rmtree(Temp)
Loading

Flow diagram for isolated OK-NTE cleanup failures

flowchart TD
    A[prepare] --> B["force_rmtree(temp_path)"]
    B --> C[Backup script configuration]
    C --> D[Run OK-NTE task]
    D --> E["_restore_script_config_from_temp"]
    E --> F{Restore succeeds?}
    F -->|Yes| G["force_rmtree(temp_path) in finally"]
    F -->|No| H["logger.warning(...)"]
    H --> G
    G --> I[Continue task cleanup and state updates]
Loading

File-Level Changes

Change Details Files
使用可处理只读文件的目录树删除逻辑,避免 Windows 配置备份目录残留导致复原失败。
  • 新增清除只读位并重试删除的 force_rmtree,保留删除失败时的忽略语义。
  • 将 OK-NTE 备份临时目录、原配置目录和临时目标目录的删除切换为 force_rmtree
app/utils/io.py
app/task/OkNte/manager.py
提高 OK-NTE 配置复原流程的故障隔离能力,确保清理失败不影响任务收尾。
  • 捕获目录或文件复原异常并记录告警,避免覆盖任务原始异常及后续解锁、状态回写流程。
  • 将临时目录删除移入 finally,保证复原成功或失败后都尝试清理。
app/task/OkNte/manager.py
为只读目录树删除行为增加回归测试。
  • 覆盖包含只读文件的嵌套目录删除。
  • 覆盖重复删除不存在路径的幂等行为。
tests/tools/test_io_force_rmtree.py
同步格式化相关代码并更新版本资源。
  • 应用 Ruff 格式化调整。
  • 更新版本元数据。
app/task/OkNte/manager.py
app/utils/io.py
res/version.json

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

嘿——我发现了 2 个问题

面向 AI Agent 的提示
请处理这次代码审查中的评论:

## 单独评论

### 评论 1
<location path="app/utils/io.py" line_range="104" />
<code_context>
+            func(target)
+
+    with suppress(FileNotFoundError):
+        shutil.rmtree(path, onexc=_retry_without_readonly)
+
+
</code_context>
<issue_to_address>
**issue (bug_risk):** 在 Python 3.11 上,`shutil.rmtree(..., onexc=...)` 会引发 `TypeError`,因为 `onexc` 参数是在 Python 3.12 中才引入的。仓库的 CI 明确运行 Python 3.11,因此在该环境中,OK-NTE 准备工作会在创建临时目录之前失败。

**触发条件:** 应用或测试套件运行在 Python 3.11 或更早版本上时。

**建议修复:** 对 Python 3.11 使用版本兼容的 `onerror` 回调,或者仅在支持时有条件地选择 `onexc````suggestion
        shutil.rmtree(path, onerror=_retry_without_readonly)
```
</issue_to_address>

### 评论 2
<location path="tests/tools/test_io_force_rmtree.py" line_range="17-24" />
<code_context>
+        nested.mkdir(parents=True)
+        readonly = nested / "pack-0001.idx"
+        readonly.write_bytes(b"payload")
+        os.chmod(readonly, stat.S_IREAD)
+        return root, readonly
+
+    def test_removes_tree_containing_readonly_file(self):
+        root, readonly = self._make_tree_with_readonly_file()
+        self.addCleanup(self._cleanup, root, readonly)
+
+        force_rmtree(root)
+
+        self.assertFalse(root.exists())
</code_context>
<issue_to_address>
**issue (testing):** 在 POSIX 系统上,只读文件测试并未触发只读重试路径:当父目录仍可写时,将文件设为只读并不会阻止其被删除。因此,即使 `_retry_without_readonly` 已损坏,该测试仍会通过,导致 Windows 特有的行为未得到验证。

**触发条件:** 测试套件在 Linux 或其他 POSIX 平台上运行时。

**建议修复:** 模拟失败的 `rmtree` 回调,或在 Windows 上运行该场景,并断言是否调用了 chmod-and-retry 路径。
</issue_to_address>

Sourcery 评估

需要人工审查。 需要先处理 2 个发现;此外,新的清理路径会强制删除脚本配置目录,而恢复路径在复制或重命名失败且异常被吞掉时,可能导致线上配置被删除或仅部分恢复。恢复此 PR 可以阻止未来再次发生,但无法恢复已经被删除或覆盖的配置文件。

阻塞性发现:app/utils/io.py:104tests/tools/test_io_force_rmtree.py:24


Sourcery 对开源项目免费——如果您喜欢我们的审查,请考虑分享 ✨
帮助我变得更有用!请在每条评论上点击 👍 或 👎,我会利用反馈来改进审查结果。
Original comment in English

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="app/utils/io.py" line_range="104" />
<code_context>
+            func(target)
+
+    with suppress(FileNotFoundError):
+        shutil.rmtree(path, onexc=_retry_without_readonly)
+
+
</code_context>
<issue_to_address>
**issue (bug_risk):** `shutil.rmtree(..., onexc=...)` raises `TypeError` on Python 3.11 because the `onexc` parameter was introduced in Python 3.12. The repository's CI explicitly runs Python 3.11, so OK-NTE preparation fails before the temporary directory is created in that environment.

**Triggers:** When the application or test suite runs under Python 3.11 or earlier.

**Suggested fix:** Use the version-compatible `onerror` callback for Python 3.11, or conditionally select `onexc` only when supported.

```suggestion
        shutil.rmtree(path, onerror=_retry_without_readonly)
```
</issue_to_address>

### Comment 2
<location path="tests/tools/test_io_force_rmtree.py" line_range="17-24" />
<code_context>
+        nested.mkdir(parents=True)
+        readonly = nested / "pack-0001.idx"
+        readonly.write_bytes(b"payload")
+        os.chmod(readonly, stat.S_IREAD)
+        return root, readonly
+
+    def test_removes_tree_containing_readonly_file(self):
+        root, readonly = self._make_tree_with_readonly_file()
+        self.addCleanup(self._cleanup, root, readonly)
+
+        force_rmtree(root)
+
+        self.assertFalse(root.exists())
</code_context>
<issue_to_address>
**issue (testing):** The read-only-file test does not exercise the read-only retry path on POSIX systems: making a file read-only does not prevent its removal when its parent directory remains writable. The test therefore passes even if `_retry_without_readonly` is broken, leaving the Windows-specific behavior unverified.

**Triggers:** When the test suite runs on Linux or another POSIX platform.

**Suggested fix:** Mock the failing `rmtree` callback or run the scenario on Windows, and assert that the chmod-and-retry path is invoked.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 2 findings to address first, and the new cleanup path force-deletes script configuration trees and the restore path can leave the live configuration removed or partially restored if copying or renaming fails, while the exception is swallowed. Reverting the PR would stop future occurrences but would not recover configuration files already deleted or overwritten.

Blocking findings: app/utils/io.py:104, tests/tools/test_io_force_rmtree.py:24


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread app/utils/io.py
func(target)

with suppress(FileNotFoundError):
shutil.rmtree(path, onexc=_retry_without_readonly)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): 在 Python 3.11 上,shutil.rmtree(..., onexc=...) 会引发 TypeError,因为 onexc 参数是在 Python 3.12 中才引入的。仓库的 CI 明确运行 Python 3.11,因此在该环境中,OK-NTE 准备工作会在创建临时目录之前失败。

触发条件: 应用或测试套件运行在 Python 3.11 或更早版本上时。

建议修复: 对 Python 3.11 使用版本兼容的 onerror 回调,或者仅在支持时有条件地选择 onexc

Suggested change
shutil.rmtree(path, onexc=_retry_without_readonly)
shutil.rmtree(path, onerror=_retry_without_readonly)
Original comment in English

issue (bug_risk): shutil.rmtree(..., onexc=...) raises TypeError on Python 3.11 because the onexc parameter was introduced in Python 3.12. The repository's CI explicitly runs Python 3.11, so OK-NTE preparation fails before the temporary directory is created in that environment.

Triggers: When the application or test suite runs under Python 3.11 or earlier.

Suggested fix: Use the version-compatible onerror callback for Python 3.11, or conditionally select onexc only when supported.

Suggested change
shutil.rmtree(path, onexc=_retry_without_readonly)
shutil.rmtree(path, onerror=_retry_without_readonly)

Comment on lines +17 to +24
os.chmod(readonly, stat.S_IREAD)
return root, readonly

def test_removes_tree_containing_readonly_file(self):
root, readonly = self._make_tree_with_readonly_file()
self.addCleanup(self._cleanup, root, readonly)

force_rmtree(root)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (testing): 在 POSIX 系统上,只读文件测试并未触发只读重试路径:当父目录仍可写时,将文件设为只读并不会阻止其被删除。因此,即使 _retry_without_readonly 已损坏,该测试仍会通过,导致 Windows 特有的行为未得到验证。

触发条件: 测试套件在 Linux 或其他 POSIX 平台上运行时。

建议修复: 模拟失败的 rmtree 回调,或在 Windows 上运行该场景,并断言是否调用了 chmod-and-retry 路径。

Original comment in English

issue (testing): The read-only-file test does not exercise the read-only retry path on POSIX systems: making a file read-only does not prevent its removal when its parent directory remains writable. The test therefore passes even if _retry_without_readonly is broken, leaving the Windows-specific behavior unverified.

Triggers: When the test suite runs on Linux or another POSIX platform.

Suggested fix: Mock the failing rmtree callback or run the scenario on Windows, and assert that the chmod-and-retry path is invoked.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI AI generated content enhancement New feature or request Sentry daily Sentry daily issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants