Files
SyncVito/src/syncvito/file_locks.py
T
2026-09-24 16:04:51 +02:00

130 lines
4.0 KiB
Python

"""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