From ac21052298a843f7e13e0b6baf1af1f617b3ae49 Mon Sep 17 00:00:00 2001 From: mcroomp Date: Wed, 27 May 2026 10:43:19 +0200 Subject: [PATCH] indexer: open files with FILE_SHARE_DELETE on Windows Use CreateFileW with FILE_SHARE_DELETE so that files being read during indexing do not block deletion or renaming on Windows. Adds _open_shared() helper that calls CreateFileW with FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE on Windows and falls back to plain open() on other platforms. Both callsites in build_document (source file reads) and the .gitignore reader in walk_source_files now use _open_shared(). --- indexserver/indexer.py | 45 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/indexserver/indexer.py b/indexserver/indexer.py index 8302e52..85ba60e 100644 --- a/indexserver/indexer.py +++ b/indexserver/indexer.py @@ -28,6 +28,47 @@ _LOG = logging.getLogger("tscodesearch.indexer") +# --------------------------------------------------------------------------- +# Shared-delete file open (Windows) +# --------------------------------------------------------------------------- + +if sys.platform == "win32": + import ctypes as _ctypes + import msvcrt as _msvcrt + _kernel32 = _ctypes.WinDLL("kernel32", use_last_error=True) + _kernel32.CreateFileW.restype = _ctypes.c_void_p + _kernel32.CreateFileW.argtypes = [ + _ctypes.c_wchar_p, # lpFileName + _ctypes.c_uint32, # dwDesiredAccess + _ctypes.c_uint32, # dwShareMode + _ctypes.c_void_p, # lpSecurityAttributes + _ctypes.c_uint32, # dwCreationDisposition + _ctypes.c_uint32, # dwFlagsAndAttributes + _ctypes.c_void_p, # hTemplateFile + ] + _GENERIC_READ = 0x80000000 + _FILE_SHARE_ALL = 0x00000007 # READ | WRITE | DELETE + _OPEN_EXISTING = 3 + _FILE_ATTR_NORMAL = 0x00000080 + _INVALID_HANDLE = _ctypes.c_void_p(-1).value + + def _open_shared(path: str, mode: str = "rb", **kwargs): + """Open a file allowing concurrent deletion on Windows (FILE_SHARE_DELETE).""" + h = _kernel32.CreateFileW( + path, _GENERIC_READ, _FILE_SHARE_ALL, + None, _OPEN_EXISTING, _FILE_ATTR_NORMAL, None, + ) + if h == _INVALID_HANDLE: + err = _ctypes.get_last_error() + raise OSError(err, _ctypes.FormatError(err), path) + fd = _msvcrt.open_osfhandle(h, os.O_RDONLY) + return open(fd, mode, closefd=True, **kwargs) +else: + def _open_shared(path: str, mode: str = "rb", **kwargs): # type: ignore[misc] + """Open a file (non-Windows: plain open).""" + return open(path, mode, **kwargs) + + # --------------------------------------------------------------------------- # Walk result # --------------------------------------------------------------------------- @@ -323,7 +364,7 @@ def build_document(full_path: str, relative_path: str) -> dict | None: Callers must check for None.""" try: stat = os.stat(full_path) - with open(full_path, "rb") as _f: + with _open_shared(full_path, "rb") as _f: src_bytes = _f.read() except OSError: return None @@ -413,7 +454,7 @@ def _get_specs(dirpath: str, entries: list) -> list: if e.name == ".gitignore": try: if e.is_file(follow_symlinks=False): - with open(e.path, "r", encoding="utf-8", errors="replace") as f: + with _open_shared(e.path, "r", encoding="utf-8", errors="replace") as f: spec = pathspec.PathSpec.from_lines("gitwildmatch", f) result = inherited + [(dirpath, spec)] _dir_specs[dirpath] = result