"""Ermittlung von Prozessen, die Dateien unter Windows sperren.""" from __future__ import annotations import ctypes import os from ctypes import wintypes from dataclasses import dataclass from pathlib import Path __all__ = ["LockingProcess", "find_locking_processes", "is_file_locked"] @dataclass(frozen=True, slots=True) class LockingProcess: """Ein vom Windows Restart Manager gemeldeter Prozess.""" pid: int name: str def is_file_locked(path: Path) -> bool: """Prüft eine vorhandene Datei durch exklusives Öffnen ohne Schreibzugriff.""" if not path.exists() or os.name != "nt": return False kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) create_file = kernel32.CreateFileW create_file.argtypes = [ wintypes.LPCWSTR, wintypes.DWORD, wintypes.DWORD, ctypes.c_void_p, wintypes.DWORD, wintypes.DWORD, wintypes.HANDLE, ] create_file.restype = wintypes.HANDLE close_handle = kernel32.CloseHandle close_handle.argtypes = [wintypes.HANDLE] close_handle.restype = wintypes.BOOL handle = create_file( str(path), 0x80000000, 0, None, 3, 0x80, None, ) invalid_handle = ctypes.c_void_p(-1).value if handle == invalid_handle: return ctypes.get_last_error() in {32, 33} close_handle(handle) return False def find_locking_processes(path: Path) -> list[LockingProcess]: """Liefert sperrende Prozesse über die Windows-Restart-Manager-API.""" if os.name != "nt" or not path.exists(): return [] try: return _restart_manager_processes(path) except (OSError, AttributeError, ctypes.ArgumentError): return [] def _restart_manager_processes(path: Path) -> list[LockingProcess]: rstrtmgr = ctypes.WinDLL("Rstrtmgr") session_handle = wintypes.DWORD() session_key = ctypes.create_unicode_buffer(33) if rstrtmgr.RmStartSession(ctypes.byref(session_handle), 0, session_key) != 0: return [] try: resources = (wintypes.LPCWSTR * 1)(str(path)) if rstrtmgr.RmRegisterResources(session_handle, 1, resources, 0, None, 0, None) != 0: return [] needed = wintypes.UINT(0) count = wintypes.UINT(0) reboot_reasons = wintypes.DWORD(0) result = rstrtmgr.RmGetList( session_handle, ctypes.byref(needed), ctypes.byref(count), None, ctypes.byref(reboot_reasons), ) if result not in {0, 234} or needed.value == 0: return [] process_info_type = _rm_process_info_type() process_array = (process_info_type * needed.value)() count.value = needed.value result = rstrtmgr.RmGetList( session_handle, ctypes.byref(needed), ctypes.byref(count), process_array, ctypes.byref(reboot_reasons), ) if result != 0: return [] return [ LockingProcess( pid=int(item.Process.dwProcessId), name=item.strAppName or f"Prozess {item.Process.dwProcessId}" ) for item in process_array[: count.value] ] finally: rstrtmgr.RmEndSession(session_handle) def _rm_process_info_type() -> type[ctypes.Structure]: class FileTime(ctypes.Structure): _fields_ = [("dwLowDateTime", wintypes.DWORD), ("dwHighDateTime", wintypes.DWORD)] class UniqueProcess(ctypes.Structure): _fields_ = [("dwProcessId", wintypes.DWORD), ("ProcessStartTime", FileTime)] class ProcessInfo(ctypes.Structure): _fields_ = [ ("Process", UniqueProcess), ("strAppName", wintypes.WCHAR * 256), ("strServiceShortName", wintypes.WCHAR * 64), ("ApplicationType", wintypes.UINT), ("AppStatus", wintypes.ULONG), ("TSSessionId", wintypes.DWORD), ("bRestartable", wintypes.BOOL), ] return ProcessInfo