在 Windows 上打开文件,在 Python 上使用独占锁定
Opening a file on Windows with exclusive locking in Python
我有一个与 this 问题非常相似的问题,我需要满足以下条件:
- 如果打开一个文件进行阅读,则该文件只能由任何其他人打开进行阅读process/program
- 如果一个文件是为写入而打开的,则该文件只能被任何其他人打开以供读取process/program
链接问题中发布的解决方案使用第三方库,该库在与相关文件相同的目录中添加任意 .LOCK
文件。这是一个仅适用于正在使用该库的程序的解决方案,不会阻止任何其他 process/program 使用该文件,因为它们可能无法实现以检查 .LOCK
关联.
本质上,我希望仅使用 Python 的标准库来复制 this 结果。
BLUF: Need a standard library implementation specific to Windows for exclusive file locking
举一个问题集的例子,假设有:
- 1 个共享文件 network/drive
- 2 个用户在单独的 processes/programs
假设用户 1 是文件中的 运行 程序 A,并且在某个时刻执行了以下命令:
with open(fp, 'rb') as f:
while True:
chunk = f.read(10)
if chunk:
# do something with chunk
else:
break
因此他们一次遍历文件 10 个字节。
现在用户 2 稍后在同一个文件上运行程序 B:
with open(fp, 'wb') as f:
for b in data: # some byte array
f.write(b)
在 Windows,有问题的文件立即被截断,程序 A 停止迭代(即使它没有完成),程序 B 开始写入文件。因此,我需要一种方法来确保文件不会以不同的模式打开,如果以前打开过会改变其内容。
我正在查看 msvcrt 库,即 msvcrt.locking()
接口。我成功地做到了确保打开以供阅读的文件可以锁定以供阅读,但没有其他人可以读取该文件(因为我锁定了整个文件):
>>> f1 = open(fp, 'rb')
>>> f2 = open(fp, 'rb')
>>> msvcrt.locking(f1.fileno(), msvcrt.LK_LOCK, os.stat(fp).st_size)
>>> next(f1)
b"\x00\x05'\n"
>>> next(f2)
PermissionError: [Errno 13] Permission denied
这是一个可以接受的结果,只是不是最理想的结果。
在同一场景中,用户 1 运行程序 A,其中包括:
with open(fp, 'rb') as f
msvcrt.locking(f.fileno(), msvcrt.LK_LOCK, os.stat(fp).st_size)
# repeat while block
msvcrt.locking(f.fileno(), msvcrt.LK_UNLCK, os.stat(fp).st_size)
稍后用户 2 运行程序 B,结果相同,文件被截断。
在这一点上,我希望有一种方法可以向用户 2 抛出一个错误,说明该文件已打开以供在其他地方读取,此时无法写入。但是如果用户3过来打开文件阅读,就没有问题了。
更新:
一个可能的解决方案是更改文件的权限(如果文件已在使用则捕获异常):
>>> os.chmod(fp, stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH)
>>> with open(fp, 'wb') as f:
# do something
PermissionError: [Errno 13] Permission denied <fp>
感觉这不是最佳解决方案(特别是如果用户甚至没有更改权限的权限)。仍在寻找合适的锁定解决方案,但如果文件被锁定以供读取,msvcrt
不会阻止截断和写入。似乎仍然没有办法用 Python 的标准库生成排他锁。
对于Windows特定解决方案感兴趣的人:
import os
import ctypes
import msvcrt
import pathlib
# Windows constants for file operations
NULL = 0x00000000
CREATE_ALWAYS = 0x00000002
OPEN_EXISTING = 0x00000003
FILE_SHARE_READ = 0x00000001
FILE_ATTRIBUTE_READONLY = 0x00000001 # strictly for file reading
FILE_ATTRIBUTE_NORMAL = 0x00000080 # strictly for file writing
FILE_FLAG_SEQUENTIAL_SCAN = 0x08000000
GENERIC_READ = 0x80000000
GENERIC_WRITE = 0x40000000
_ACCESS_MASK = os.O_RDONLY | os.O_WRONLY
_ACCESS_MAP = {os.O_RDONLY: GENERIC_READ,
os.O_WRONLY: GENERIC_WRITE
}
_CREATE_MASK = os.O_CREAT | os.O_TRUNC
_CREATE_MAP = {NULL: OPEN_EXISTING,
os.O_CREAT | os.O_TRUNC: CREATE_ALWAYS
}
win32 = ctypes.WinDLL('kernel32.dll', use_last_error=True)
win32.CreateFileW.restype = ctypes.c_void_p
INVALID_FILE_HANDLE = ctypes.c_void_p(-1).value
def _opener(path: pathlib.Path, flags: int) -> int:
access_flags = _ACCESS_MAP[flags & _ACCESS_MASK]
create_flags = _CREATE_MAP[flags & _CREATE_MASK]
if flags & os.O_WRONLY:
share_flags = NULL
attr_flags = FILE_ATTRIBUTE_NORMAL
else:
share_flags = FILE_SHARE_READ
attr_flags = FILE_ATTRIBUTE_READONLY
attr_flags |= FILE_FLAG_SEQUENTIAL_SCAN
h = win32.CreateFileW(path, access_flags, share_flags, NULL, create_flags, attr_flags, NULL)
if h == INVALID_FILE_HANDLE:
raise ctypes.WinError(ctypes.get_last_error())
return msvcrt.open_osfhandle(h, flags)
class _FileControlAccessor(pathlib._NormalAccessor):
open = staticmethod(_opener)
_control_accessor = _FileControlAccessor()
class Path(pathlib.WindowsPath):
def _init(self) -> None:
self._closed = False
self._accessor = _control_accessor
def _opener(self, name, flags) -> int:
return self._accessor.open(name, flags)
我有一个与 this 问题非常相似的问题,我需要满足以下条件:
- 如果打开一个文件进行阅读,则该文件只能由任何其他人打开进行阅读process/program
- 如果一个文件是为写入而打开的,则该文件只能被任何其他人打开以供读取process/program
链接问题中发布的解决方案使用第三方库,该库在与相关文件相同的目录中添加任意 .LOCK
文件。这是一个仅适用于正在使用该库的程序的解决方案,不会阻止任何其他 process/program 使用该文件,因为它们可能无法实现以检查 .LOCK
关联.
本质上,我希望仅使用 Python 的标准库来复制 this 结果。
BLUF: Need a standard library implementation specific to Windows for exclusive file locking
举一个问题集的例子,假设有:
- 1 个共享文件 network/drive
- 2 个用户在单独的 processes/programs
假设用户 1 是文件中的 运行 程序 A,并且在某个时刻执行了以下命令:
with open(fp, 'rb') as f:
while True:
chunk = f.read(10)
if chunk:
# do something with chunk
else:
break
因此他们一次遍历文件 10 个字节。
现在用户 2 稍后在同一个文件上运行程序 B:
with open(fp, 'wb') as f:
for b in data: # some byte array
f.write(b)
在 Windows,有问题的文件立即被截断,程序 A 停止迭代(即使它没有完成),程序 B 开始写入文件。因此,我需要一种方法来确保文件不会以不同的模式打开,如果以前打开过会改变其内容。
我正在查看 msvcrt 库,即 msvcrt.locking()
接口。我成功地做到了确保打开以供阅读的文件可以锁定以供阅读,但没有其他人可以读取该文件(因为我锁定了整个文件):
>>> f1 = open(fp, 'rb')
>>> f2 = open(fp, 'rb')
>>> msvcrt.locking(f1.fileno(), msvcrt.LK_LOCK, os.stat(fp).st_size)
>>> next(f1)
b"\x00\x05'\n"
>>> next(f2)
PermissionError: [Errno 13] Permission denied
这是一个可以接受的结果,只是不是最理想的结果。
在同一场景中,用户 1 运行程序 A,其中包括:
with open(fp, 'rb') as f
msvcrt.locking(f.fileno(), msvcrt.LK_LOCK, os.stat(fp).st_size)
# repeat while block
msvcrt.locking(f.fileno(), msvcrt.LK_UNLCK, os.stat(fp).st_size)
稍后用户 2 运行程序 B,结果相同,文件被截断。
在这一点上,我希望有一种方法可以向用户 2 抛出一个错误,说明该文件已打开以供在其他地方读取,此时无法写入。但是如果用户3过来打开文件阅读,就没有问题了。
更新:
一个可能的解决方案是更改文件的权限(如果文件已在使用则捕获异常):
>>> os.chmod(fp, stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH)
>>> with open(fp, 'wb') as f:
# do something
PermissionError: [Errno 13] Permission denied <fp>
感觉这不是最佳解决方案(特别是如果用户甚至没有更改权限的权限)。仍在寻找合适的锁定解决方案,但如果文件被锁定以供读取,msvcrt
不会阻止截断和写入。似乎仍然没有办法用 Python 的标准库生成排他锁。
对于Windows特定解决方案感兴趣的人:
import os
import ctypes
import msvcrt
import pathlib
# Windows constants for file operations
NULL = 0x00000000
CREATE_ALWAYS = 0x00000002
OPEN_EXISTING = 0x00000003
FILE_SHARE_READ = 0x00000001
FILE_ATTRIBUTE_READONLY = 0x00000001 # strictly for file reading
FILE_ATTRIBUTE_NORMAL = 0x00000080 # strictly for file writing
FILE_FLAG_SEQUENTIAL_SCAN = 0x08000000
GENERIC_READ = 0x80000000
GENERIC_WRITE = 0x40000000
_ACCESS_MASK = os.O_RDONLY | os.O_WRONLY
_ACCESS_MAP = {os.O_RDONLY: GENERIC_READ,
os.O_WRONLY: GENERIC_WRITE
}
_CREATE_MASK = os.O_CREAT | os.O_TRUNC
_CREATE_MAP = {NULL: OPEN_EXISTING,
os.O_CREAT | os.O_TRUNC: CREATE_ALWAYS
}
win32 = ctypes.WinDLL('kernel32.dll', use_last_error=True)
win32.CreateFileW.restype = ctypes.c_void_p
INVALID_FILE_HANDLE = ctypes.c_void_p(-1).value
def _opener(path: pathlib.Path, flags: int) -> int:
access_flags = _ACCESS_MAP[flags & _ACCESS_MASK]
create_flags = _CREATE_MAP[flags & _CREATE_MASK]
if flags & os.O_WRONLY:
share_flags = NULL
attr_flags = FILE_ATTRIBUTE_NORMAL
else:
share_flags = FILE_SHARE_READ
attr_flags = FILE_ATTRIBUTE_READONLY
attr_flags |= FILE_FLAG_SEQUENTIAL_SCAN
h = win32.CreateFileW(path, access_flags, share_flags, NULL, create_flags, attr_flags, NULL)
if h == INVALID_FILE_HANDLE:
raise ctypes.WinError(ctypes.get_last_error())
return msvcrt.open_osfhandle(h, flags)
class _FileControlAccessor(pathlib._NormalAccessor):
open = staticmethod(_opener)
_control_accessor = _FileControlAccessor()
class Path(pathlib.WindowsPath):
def _init(self) -> None:
self._closed = False
self._accessor = _control_accessor
def _opener(self, name, flags) -> int:
return self._accessor.open(name, flags)