Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/filelock/_util.py: 35%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from __future__ import annotations
3import os
4import secrets
5import stat
6import sys
7from errno import EACCES, EIO, EISDIR
8from pathlib import Path
9from typing import Final
12def write_all(fd: int, data: bytes) -> None:
13 """
14 Write the whole buffer to *fd*, looping over the short writes ``os.write`` is allowed to make.
16 A marker written with a bare ``os.write`` can land partially: a peer reading it mid-write parses a truncated record
17 as malformed or as a foreign holder. Looping until the buffer drains keeps the record atomic in the process and
18 kernel view. No ``fsync``: filelock needs a complete record, not crash-durable storage.
20 :param fd: file descriptor open for writing.
21 :param data: bytes to write in full.
23 :raises OSError: if a write reports zero progress before the record is complete.
25 """
26 remaining = memoryview(data)
27 while remaining:
28 if (written := os.write(fd, remaining)) == 0:
29 raise OSError(EIO, "os.write wrote 0 bytes before the record was complete")
30 remaining = remaining[written:]
33def raise_on_not_writable_file(filename: str) -> None:
34 """
35 Raise an exception if attempting to open the file for writing would fail.
37 Separates files that can never be written from files that are writable but currently locked.
39 :param filename: file to check
41 :raises OSError: as if the file was opened for writing.
43 """
44 try:
45 # lstat, not stat: settles exists-and-writable in one syscall, and a hostile symlink at the lock path would
46 # make stat inspect the link target, letting an attacker turn a contended acquire into a misleading
47 # PermissionError / IsADirectoryError and probe that target's attributes. The real open passes O_NOFOLLOW and
48 # refuses the symlink anyway.
49 file_stat = os.lstat(filename)
50 except OSError:
51 return # does not exist, or an error the caller cannot act on
53 # No mtime guard: the old `if st_mtime != 0` skip covered NFS/Linux quirks where os.lstat returned an all-zero
54 # struct, which it no longer does. Skipping on mtime 0 let a read-only file or a directory at the lock path pass
55 # as missing, so acquire() blocked forever on an open that cannot succeed.
56 # Match open() credentials where supported; group permissions and ACLs can grant access without S_IWUSR.
57 if not (file_stat.st_mode & stat.S_IWUSR) and not os.access(
58 filename, os.W_OK, effective_ids=os.access in os.supports_effective_ids
59 ):
60 try:
61 os.lstat(filename)
62 except FileNotFoundError:
63 # A holder may unlink its marker between lstat() and access(); let the caller attempt creation.
64 return
65 raise PermissionError(EACCES, "Permission denied", filename)
67 if stat.S_ISDIR(file_stat.st_mode):
68 if sys.platform == "win32": # pragma: win32 cover
69 raise PermissionError(EACCES, "Permission denied", filename)
70 raise IsADirectoryError(EISDIR, "Is a directory", filename) # pragma: win32 no cover
73def ensure_directory_exists(filename: Path | str) -> None:
74 """
75 Ensure the directory containing the file exists (create it if necessary).
77 :param filename: file.
79 """
80 Path(filename).parent.mkdir(parents=True, exist_ok=True)
83def break_lock_file(lock_file: str, mtime_before: float, ino_before: int) -> None:
84 """
85 Atomically break a stale lock file judged stale at modification time *mtime_before*.
87 Rename the file to a process-private name before unlinking it, so two processes breaking the same lock cannot
88 delete each other's work: only one rename of a given inode wins, the loser gets ``OSError``. After the rename,
89 re-check the file. A newer modification time, or a different inode than *ino_before*, means a peer recreated the
90 lock between the stale decision and the rename, so we grabbed a live file and abort, leaving the renamed file in
91 place. A rollback rename is itself racy, the same trade-off as the soft read/write marker break. The inode check
92 matters because filesystems with coarse modification-time granularity (NFS, FAT) can give a same-second recreation
93 the old mtime, so mtime alone would miss it and unlink a live lock; the inode is the reliable identity, mirroring
94 the token re-check in the soft read/write marker break. ``lstat`` avoids following a hostile symlink swapped in
95 after the decision.
97 The break name carries a random token so it is unguessable and unique per attempt. Without it two breakers in the
98 same process share ``<lock>.break.<pid>``, and a second break can rename a recreated live lock onto that path in
99 the window between the re-verify ``lstat`` above and the ``unlink`` below, deleting a live lock the inode check
100 just approved. A private name keeps anyone else from targeting our break path, matching the soft read/write marker
101 break.
103 :param lock_file: path to the lock file to break.
104 :param mtime_before: modification time observed when the lock was judged stale.
105 :param ino_before: inode number observed when the lock was judged stale.
107 :raises OSError: if the rename fails (e.g. the file vanished or is not owned in a sticky directory).
109 """
110 break_path = f"{lock_file}.break.{os.getpid()}.{secrets.token_hex(16)}"
111 Path(lock_file).rename(break_path)
112 try:
113 st_after = os.lstat(break_path)
114 except OSError:
115 return
116 if st_after.st_mtime > mtime_before or st_after.st_ino != ino_before:
117 return
118 Path(break_path).unlink()
121def touch(name: str, *, fd: int | None = None) -> None:
122 # Prefer the already-open, already-verified fd so a peer that swaps a symlink or a different file in at the
123 # path after our O_NOFOLLOW read cannot redirect the touch: utime then targets the inode behind the fd.
124 # Where the platform cannot utime an fd, fall back to a path-based touch that still refuses to follow a
125 # symlink where supported, matching the O_NOFOLLOW reads used elsewhere here.
126 if fd is not None and _SUPPORTS_UTIME_FD: # pragma: needs utime-fd
127 os.utime(fd, None)
128 return
129 os.utime(name, None, follow_symlinks=not _SUPPORTS_UTIME_NOFOLLOW)
132# Retargeting os.utime to an open fd lets a heartbeat refresh the exact inode it verified instead of whatever the
133# pathname now names.
134_SUPPORTS_UTIME_FD: Final[bool] = sys.platform != "win32" and os.utime in os.supports_fd
135# os.utime follows symlinks unless told not to; not every platform can refuse the follow, so probe support.
136_SUPPORTS_UTIME_NOFOLLOW: Final[bool] = os.utime in os.supports_follow_symlinks
139__all__ = [
140 "break_lock_file",
141 "ensure_directory_exists",
142 "raise_on_not_writable_file",
143 "touch",
144 "write_all",
145]