file_util.Move() deletes the destination before renaming, leaving a window where the config file does not exist

Open Beginner friendly
#800 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
78/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
python

Research direction

Start in glazier/lib/file_util.py:96-112 and inspect the callers at config/files.py:82 and :112. Check whether Remove() provides any permissions side effect before evaluating the replacement behavior on Windows and other supported platforms. Done means both config replacement paths avoid a window where the destination is absent while preserving the intended move semantics.

Written by the indexing model from the issue text.

Description

file_util.Move() deletes the destination before renaming onto it, which turns the config writer's replace step into two operations with a window between them:

# glazier/lib/file_util.py:96-112
"""Python's os.rename doesn't support overwrite on Windows."""
try:
  Remove(dst)
  os.rename(src, dst)
except OSError as e:
  raise FileMoveError(src, dst) from e

The docstring's reasoning is correct. Measured on CPython 3.13.13, Windows 11:

os.rename(src, dst)   # dst exists
# FileExistsError: [WinError 183] Cannot create a file when that file already exists

But os.replace(), added in 3.3, overwrites atomically on both platforms:

os.replace(src, dst)  # same setup
# succeeds; dst content is now src's, src is gone

The window matters most at config/files.py:112, the "Replace the original with the tmp" step of Dump(). That is the write-to-temp-then-swap pattern, whose whole purpose is that the live file is never absent. As written, Remove(dst) deletes the live config first, so an interruption between the two calls leaves no config at all rather than the previous version. config/files.py:82 has the same shape when rotating a .bak.

Interruption is not hypothetical here, since Glazier restarts machines as part of a build.

os.replace(src, dst) is a drop-in for both lines and removes the window. Worth checking whether Remove() was also carrying a permissions side effect before swapping it.

I have no Linux or macOS machine, so the cross-platform half is read from the os.replace contract rather than measured.

Dominant language
Python
Stars
1.3k
Forks
96
Avg merge
3d 15h
Merged PRs (30d)
4

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from google/glazier

All issues in google/glazier

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.