Merge pull request #5 from yoruuuchan/fix/find-download-race
fix: tolerate files vanishing mid-scan in find_download
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user