diff --git a/skills/open-kimi-ppt/scripts/export_pptx.py b/skills/open-kimi-ppt/scripts/export_pptx.py index 92bd9ae..1bf8c1c 100755 --- a/skills/open-kimi-ppt/scripts/export_pptx.py +++ b/skills/open-kimi-ppt/scripts/export_pptx.py @@ -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 diff --git a/skills/open-kimi-ppt/tests/test_export_pptx.py b/skills/open-kimi-ppt/tests/test_export_pptx.py index 24d5009..29640bb 100755 --- a/skills/open-kimi-ppt/tests/test_export_pptx.py +++ b/skills/open-kimi-ppt/tests/test_export_pptx.py @@ -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", + '' + '', + ) + archive.writestr("ppt/presentation.xml", "") + 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",