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] = {}
|
last_sizes: Dict[Path, int] = {}
|
||||||
stable: Dict[Path, int] = {}
|
stable: Dict[Path, int] = {}
|
||||||
while time.monotonic() < deadline:
|
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:
|
for root in search_roots:
|
||||||
if not root.exists():
|
if not root.exists():
|
||||||
continue
|
continue
|
||||||
candidates.extend(path for path in root.rglob("*") if path.is_file())
|
for path in root.rglob("*"):
|
||||||
for path in sorted(candidates, key=lambda item: item.stat().st_mtime, reverse=True):
|
if not path.is_file():
|
||||||
try:
|
|
||||||
stat = path.stat()
|
|
||||||
size = stat.st_size
|
|
||||||
if since is not None and stat.st_mtime < since:
|
|
||||||
continue
|
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
|
continue
|
||||||
if size == last_sizes.get(path) and size > 0:
|
if size == last_sizes.get(path) and size > 0:
|
||||||
stable[path] = stable.get(path, 0) + 1
|
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)
|
found = MODULE.find_download([root], timeout=2.0, since=since)
|
||||||
self.assertEqual(found.resolve(), new.resolve())
|
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):
|
def test_browser_open_does_not_pass_download_path(self):
|
||||||
session = MODULE.BrowserSession(
|
session = MODULE.BrowserSession(
|
||||||
"/bin/agent-browser",
|
"/bin/agent-browser",
|
||||||
|
|||||||
Reference in New Issue
Block a user