diff --git a/chalice/cli/filewatch/stat.py b/chalice/cli/filewatch/stat.py index f4a987621..180fe977a 100644 --- a/chalice/cli/filewatch/stat.py +++ b/chalice/cli/filewatch/stat.py @@ -56,7 +56,12 @@ def _seed_mtime_cache(self, root_dir): for rootdir, _, filenames in self._osutils.walk(root_dir): for filename in filenames: path = self._osutils.joinpath(rootdir, filename) - self._mtime_cache[path] = self._osutils.mtime(path) + try: + self._mtime_cache[path] = self._osutils.mtime(path) + except (OSError, IOError): + # This can happen with broken symlinks or files that + # disappear between walk() and mtime(). + LOGGER.debug("Unable to stat file: %s, skipping.", path) def _single_pass_poll(self, root_dir, callback): # type: (str, Callable[[], None]) -> None diff --git a/tests/unit/cli/filewatch/test_stat.py b/tests/unit/cli/filewatch/test_stat.py index 28015cfec..fb9a4e660 100644 --- a/tests/unit/cli/filewatch/test_stat.py +++ b/tests/unit/cli/filewatch/test_stat.py @@ -38,3 +38,42 @@ def callback(*args, **kwargs): time.sleep(0.2) else: raise AssertionError("Expected callback to be invoked but was not.") + + +class FakeOSUtilsWithBrokenSymlink(object): + """Simulates a broken symlink that fails os.stat during initial cache seed. + + This can happen with symlinks pointing to non-existent files, such as + Emacs lock files (.#app.py) or other temporary symlinks. + """ + def __init__(self): + self.scan_count = 0 + + def walk(self, rootdir): + self.scan_count += 1 + yield 'rootdir', [], ['broken-symlink', 'good-file'] + + def joinpath(self, *parts): + return os.path.join(*parts) + + def mtime(self, path): + if path.endswith('broken-symlink'): + raise FileNotFoundError("No such file or directory") + return 1 + + +def test_can_handle_broken_symlink_during_initial_scan(): + """Test that broken symlinks during initial cache seeding are handled. + + This is a regression test for issue #997 where symlinks to non-existent + files (like Emacs .#app.py files) would crash the file watcher during + the initial _seed_mtime_cache call. + """ + osutils = FakeOSUtilsWithBrokenSymlink() + watcher = stat.StatFileWatcher(osutils) + # This should not raise an exception + watcher._seed_mtime_cache('rootdir') + # The good file should be in the cache, the broken symlink should not + assert len(watcher._mtime_cache) == 1 + assert 'rootdir/good-file' in watcher._mtime_cache or \ + os.path.join('rootdir', 'good-file') in watcher._mtime_cache