Merge pull request #5 from yoruuuchan/fix/find-download-race

fix: tolerate files vanishing mid-scan in find_download
This commit is contained in:
binaryify
2026-08-06 21:00:10 +08:00
committed by GitHub
2 changed files with 44 additions and 8 deletions
+13 -8
View File
@@ -461,18 +461,23 @@ def find_download(
last_sizes: Dict[Path, int] = {}
stable: Dict[Path, int] = {}
while time.monotonic() < deadline:
candidates: List[Path] = []
# Snapshot stats while collecting and tolerate races everywhere: the
# search roots include the live Downloads folder, where Chrome renames
# .crdownload files away between directory listing and stat().
entries: List[Tuple[Path, float, int]] = []
for root in search_roots:
if not root.exists():
continue
candidates.extend(path for path in root.rglob("*") if path.is_file())
for path in sorted(candidates, key=lambda item: item.stat().st_mtime, reverse=True):
try:
stat = path.stat()
size = stat.st_size
if since is not None and stat.st_mtime < since:
for path in root.rglob("*"):
if not path.is_file():
continue
except OSError:
try:
info = path.stat()
except OSError:
continue
entries.append((path, info.st_mtime, info.st_size))
for path, mtime, size in sorted(entries, key=lambda entry: entry[1], reverse=True):
if since is not None and mtime < since:
continue
if size == last_sizes.get(path) and size > 0:
stable[path] = stable.get(path, 0) + 1
@@ -163,6 +163,37 @@ class ExportPptxTests(unittest.TestCase):
found = MODULE.find_download([root], timeout=2.0, since=since)
self.assertEqual(found.resolve(), new.resolve())
def test_find_download_survives_files_vanishing_mid_scan(self):
# Chrome renames "*.crdownload" files away between directory listing
# and stat(); a vanished file must be skipped, not crash the export.
with tempfile.TemporaryDirectory() as name:
root = Path(name)
deck = root / "deck.pptx"
with zipfile.ZipFile(deck, "w") as archive:
archive.writestr(
"[Content_Types].xml",
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
'<Override PartName="/ppt/presentation.xml" '
f'ContentType="{MODULE.PPTX_CONTENT_TYPE}"/></Types>',
)
archive.writestr("ppt/presentation.xml", "<p:presentation/>")
ghost = root / "ghost.crdownload"
ghost.write_bytes(b"partial download")
real_stat = Path.stat
seen = {"count": 0}
def racy_stat(self, **kwargs):
if self.name == "ghost.crdownload":
seen["count"] += 1
if seen["count"] > 1:
raise FileNotFoundError(2, "vanished mid-scan", str(self))
return real_stat(self, **kwargs)
with patch.object(Path, "stat", racy_stat):
found = MODULE.find_download([root], timeout=2.0)
self.assertEqual(found.resolve(), deck.resolve())
def test_browser_open_does_not_pass_download_path(self):
session = MODULE.BrowserSession(
"/bin/agent-browser",