在 Windows 上打开文件,在 Python 上使用独占锁定

Opening a file on Windows with exclusive locking in Python

我有一个与 this 问题非常相似的问题,我需要满足以下条件:

链接问题中发布的解决方案使用第三方库,该库在与相关文件相同的目录中添加任意 .LOCK 文件。这是一个仅适用于正在使用该库的程序的解决方案,不会阻止任何其他 process/program 使用该文件,因为它们可能无法实现以检查 .LOCK 关联.

本质上,我希望仅使用 Python 的标准库来复制 this 结果。

BLUF: Need a standard library implementation specific to Windows for exclusive file locking

举一个问题集的例子,假设有:

假设用户 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)