Skip to content

fix(utils): handle EACCES in atomic_replace on Windows - #73807

Closed
guanla-zz wants to merge 3 commits into
NousResearch:mainfrom
guanla-zz:fix/windows-atomic-replace-eacces
Closed

fix(utils): handle EACCES in atomic_replace on Windows#73807
guanla-zz wants to merge 3 commits into
NousResearch:mainfrom
guanla-zz:fix/windows-atomic-replace-eacces

Conversation

@guanla-zz

@guanla-zz guanla-zz commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

os.replace() fails with PermissionError (EACCES) on Windows when the target config file is open in another handle (e.g. during config migration after update). Only EXDEV and EBUSY were handled, causing 'hermes config migrate' to crash with 'PermissionError: [WinError 5]'.

Add EACCES to the fallback set so atomic_replace falls through to the copy+fsync+unlink path on Windows instead of raising.

What does this PR do?

Related Issue

Fixes #

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

How to Test

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform:

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — or N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — or N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — or N/A
  • I've updated tool descriptions/schemas if I changed tool behavior — or N/A

For New Skills

  • This skill is broadly useful to most users (if bundled) — see Contributing Guide
  • SKILL.md follows the standard format (frontmatter, trigger conditions, steps, pitfalls)
  • No external dependencies that aren't already available (prefer stdlib, curl, existing Hermes tools)
  • I've tested the skill end-to-end: hermes --toolsets skills -q "Use the X skill to do Y"

Screenshots / Logs

@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint platform/windows Native Windows-specific behavior or breakage sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows labels Jul 29, 2026
@monerostar

Copy link
Copy Markdown
Contributor

Native Win11 verification (monerostar)

Host: Windows 11 build 26200, CPython 3.11.15, current main (015718066) — utils.atomic_replace still only accepts EXDEV/EBUSY.

Bug is real

Holding the target open the way a running Hermes process often does makes os.replace fail with PermissionError errno=13 (EACCES) / WinError 5, and main re-raises:

Hold os.replace main atomic_replace
open(target, "r") (this process) EACCES / WinError 5 raises
open(target, "r+") same raises
Other process open(..., "r") for ~10s same raises
CreateFile share R+W or R+W+D same raises

This PR’s +EACCES fallback helps the realistic cases

Same holds, but treat EACCES like EXDEV/EBUSY and fall through to shutil.copyfile + unlink:

Hold copyfile fallback
Python open("r") / open("r+") SUCCESS — target content becomes the new bytes
Other-process read hold SUCCESS
CreateFile share R+W / R+W+D SUCCESS

Snippet shape used for the patched path (mirrors the PR):

if exc.errno not in (errno.EXDEV, errno.EBUSY, errno.EACCES):
    raise
shutil.copyfile(tmp_str, real_path)
# …copystat/fsync/unlink as today…

Honest limit (not a reason to reject)

If the target is opened with no write share (share_mode=0 or read-only share without FILE_SHARE_WRITE), both os.replace and copyfile fail with EACCES — the fallback cannot write the destination either. That is expected Windows sharing behavior; the PR still fixes the common “another handle has the file open with normal share flags” case described in the PR body (config migrate / update).

Review take

  • Direction: approve-quality for a 4-line errno widen.
  • Live evidence matches the claimed failure mode (PermissionError: [WinError 5] during replace while target is open).
  • Optional follow-up (not blocking): a tiny unit test that monkeypatches os.replace to raise OSError(errno.EACCES, ...) and asserts the copy fallback runs — keeps this regression covered on Linux CI too.

No code changes from me — verification only.

@monerostar

Copy link
Copy Markdown
Contributor

Sibling signal (cross-link)

After the live Win11 verify above: #57777 (fix(utils): handle Windows sharing violations in atomic_replace) targets the same failure class with a broader approach — Windows-gated EACCES handling, short retries for sharing violations, copy fallback, and extended tests (including “EACCES must still propagate on POSIX”).

Not a reject of this PR — just routing note so the two don’t thrash each other.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for targeting the shared helper; the current-main premise remains live: utils.py:115-125 re-raises EACCES, and config persistence reaches this helper (for example hermes_cli/config.py:3757).

Problems

  • utils.py:120 adds EACCES to the fallback set on every platform, while the PR rationale is specific to Windows sharing violations. Current helper documentation limits fallback semantics to EXDEV/EBUSY at utils.py:104-106; preserve POSIX EACCES propagation rather than broadening it.
  • The PR changes no tests. Existing copy-fallback coverage at tests/test_atomic_replace_symlinks.py:198-215 covers EXDEV, not EACCES.

Suggested changes

  • Scope the new eligibility to Windows sharing violations.
  • Add a simulated Windows-eligible EACCES fallback test and a POSIX EACCES propagation test.

The linked open PR #57777 demonstrates this distinction and related coverage. This is an automated hermes-sweeper review.

Comment thread utils.py Outdated
# Windows: os.replace fails with EACCES when the target file is open
# in another handle (e.g. the running process has config.yaml
# mmap'd or loaded). Fall back to copy+unlink like EXDEV/EBUSY.
if exc.errno not in (errno.EXDEV, errno.EBUSY, errno.EACCES):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This admits EACCES on POSIX too, although the reported condition is Windows-specific. Please gate this fallback to the Windows sharing-violation case so ordinary POSIX permission denials continue to propagate directly.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit labels Jul 30, 2026
Hermes Installer added 2 commits August 4, 2026 16:49
os.replace() fails with PermissionError (EACCES) on Windows when the
target config file is open in another handle (e.g. during config
migration after update). Only EXDEV and EBUSY were handled, causing
'hermes config migrate' to crash with 'PermissionError: [WinError 5]'.

Add EACCES to the fallback set so atomic_replace falls through to
the copy+fsync+unlink path on Windows instead of raising.
Monkeypatches os.replace to raise OSError(errno.EACCES) and asserts the
copy+unlink fallback path runs, keeping the Windows open-handle case
covered on Linux CI too.
@guanla-zz
guanla-zz force-pushed the fix/windows-atomic-replace-eacces branch from 76a0793 to d3939a8 Compare August 4, 2026 08:57
@guanla-zz

guanla-zz commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed Win11 verification, @monerostar — really appreciate the live evidence!

@teknium1: I've addressed both review points:

Windows-gated fallback — EACCES now falls back to copy+unlink only on Windows (os.name == "nt"); on POSIX it keeps propagating as a genuine permission denial.
Test coverage — added test_atomic_replace_eacces_falls_back_to_copy (Windows path) and test_atomic_replace_eacces_propagates_on_posix (POSIX path), both platform-simulated via os.name patch so they run on Linux CI.
Regarding #57777: happy to defer to maintainers on whether this minimal fix or the fuller sharing-violation treatment should land — this PR stays focused on the errno widen + tests.

EACCES on POSIX is a genuine permission denial and must keep
propagating. Gate the new fallback to os.name == 'nt' (Windows
sharing violations) per review feedback, and add a POSIX propagation
test so both branches stay covered on Linux CI.
@OutThisLife

Copy link
Copy Markdown
Collaborator

Superseded by #84852.

You identified the right helper and the right fallback strategy. #84852 credits you as a co-author.

The blanket errno == EACCES and os.name == "nt" predicate was the blocker: on Windows a genuine ACL denial and a contended rename both raise EACCES, so this classifies real permission failures as recoverable and converts them into silent successes.

Worth noting the narrower fix isn't os.access either — os.replace needs delete-child rights on the parent directory, so probing the target reports a directory-level denial as writable. #84852 handles it by not classifying up front: both cases take the same bounded path, and a genuine denial is re-raised unchanged.

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

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P3 Low — cosmetic, nice to have platform/windows Native Windows-specific behavior or breakage sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants