Close canonical cleanup race windows
This commit is contained in:
parent
1a6f33e2de
commit
d2bb95fe61
4 changed files with 805 additions and 125 deletions
|
|
@ -14,6 +14,7 @@ from typing import Protocol, cast
|
|||
|
||||
from .errors import DocForgeError
|
||||
|
||||
RENAME_NOREPLACE = 1
|
||||
RENAME_EXCHANGE = 2
|
||||
|
||||
|
||||
|
|
@ -32,9 +33,13 @@ class _RenameAt2(Protocol):
|
|||
) -> int: ...
|
||||
|
||||
|
||||
def rename_exchange_at(directory_fd: int, first: str, second: str) -> None:
|
||||
"""Atomically exchange two names inside one already bound directory."""
|
||||
|
||||
def _rename_at2(
|
||||
old_directory_fd: int,
|
||||
old_name: str,
|
||||
new_directory_fd: int,
|
||||
new_name: str,
|
||||
flags: int,
|
||||
) -> int:
|
||||
library = ctypes.CDLL(None, use_errno=True)
|
||||
try:
|
||||
rename_at2 = cast(_RenameAt2, library.renameat2)
|
||||
|
|
@ -53,20 +58,40 @@ def rename_exchange_at(directory_fd: int, first: str, second: str) -> None:
|
|||
rename_at2.restype = ctypes.c_int
|
||||
ctypes.set_errno(0)
|
||||
result = rename_at2(
|
||||
directory_fd,
|
||||
os.fsencode(first),
|
||||
directory_fd,
|
||||
os.fsencode(second),
|
||||
RENAME_EXCHANGE,
|
||||
old_directory_fd,
|
||||
os.fsencode(old_name),
|
||||
new_directory_fd,
|
||||
os.fsencode(new_name),
|
||||
flags,
|
||||
)
|
||||
if result == 0:
|
||||
return
|
||||
return 0
|
||||
error_number = ctypes.get_errno()
|
||||
if error_number in {errno.ENOSYS, errno.EINVAL, errno.EOPNOTSUPP}:
|
||||
raise DocForgeError(
|
||||
"atomic_exchange_unavailable",
|
||||
"Atomic exchange is unavailable on this filesystem",
|
||||
)
|
||||
return error_number
|
||||
|
||||
|
||||
def rename_exchange_between_at(
|
||||
first_directory_fd: int,
|
||||
first: str,
|
||||
second_directory_fd: int,
|
||||
second: str,
|
||||
) -> None:
|
||||
"""Atomically exchange names between two bound directories on one filesystem."""
|
||||
|
||||
error_number = _rename_at2(
|
||||
first_directory_fd,
|
||||
first,
|
||||
second_directory_fd,
|
||||
second,
|
||||
RENAME_EXCHANGE,
|
||||
)
|
||||
if error_number == 0:
|
||||
return
|
||||
raise DocForgeError(
|
||||
"publication_failure",
|
||||
"Could not exchange atomic publication paths",
|
||||
|
|
@ -74,6 +99,38 @@ def rename_exchange_at(directory_fd: int, first: str, second: str) -> None:
|
|||
) from OSError(error_number, os.strerror(error_number))
|
||||
|
||||
|
||||
def rename_exchange_at(directory_fd: int, first: str, second: str) -> None:
|
||||
"""Atomically exchange two names inside one already bound directory."""
|
||||
|
||||
rename_exchange_between_at(directory_fd, first, directory_fd, second)
|
||||
|
||||
|
||||
def rename_noreplace_between_at(
|
||||
source_directory_fd: int,
|
||||
source: str,
|
||||
target_directory_fd: int,
|
||||
target: str,
|
||||
) -> bool:
|
||||
"""Atomically move one name without replacing a target that appeared."""
|
||||
|
||||
error_number = _rename_at2(
|
||||
source_directory_fd,
|
||||
source,
|
||||
target_directory_fd,
|
||||
target,
|
||||
RENAME_NOREPLACE,
|
||||
)
|
||||
if error_number == 0:
|
||||
return True
|
||||
if error_number == errno.EEXIST:
|
||||
return False
|
||||
raise DocForgeError(
|
||||
"publication_failure",
|
||||
"Could not move an atomic publication path without replacement",
|
||||
error_number=error_number,
|
||||
) from OSError(error_number, os.strerror(error_number))
|
||||
|
||||
|
||||
def open_bound_directory(path: Path) -> int:
|
||||
"""Open one real directory and bind its current inode for later operations."""
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue