Skip to content

Commit c9b6d90

Browse files
authored
Raise when trying to acquire in R/O or missing folder (#96)
1 parent 684d69d commit c9b6d90

File tree

7 files changed

+122
-16
lines changed

7 files changed

+122
-16
lines changed

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ testing =
5050
coverage>=4
5151
pytest>=4
5252
pytest-cov
53+
pytest-timeout>=1.4.2
5354

5455
[bdist_wheel]
5556
universal = true

src/filelock/_soft.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,32 @@
11
import os
2+
import sys
3+
from errno import EACCES, EEXIST, ENOENT
24

35
from ._api import BaseFileLock
6+
from ._util import raise_on_exist_ro_file
47

58

69
class SoftFileLock(BaseFileLock):
710
"""Simply watches the existence of the lock file."""
811

912
def _acquire(self):
10-
open_mode = os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_TRUNC
13+
raise_on_exist_ro_file(self._lock_file)
14+
# first check for exists and read-only mode as the open will mask this case as EEXIST
15+
mode = (
16+
os.O_WRONLY # open for writing only
17+
| os.O_CREAT
18+
| os.O_EXCL # together with above raise EEXIST if the file specified by filename exists
19+
| os.O_TRUNC # truncate the file to zero byte
20+
)
1121
try:
12-
fd = os.open(self._lock_file, open_mode)
13-
except OSError:
14-
pass
22+
fd = os.open(self._lock_file, mode)
23+
except OSError as exception:
24+
if exception.errno == EEXIST: # expected if cannot lock
25+
pass
26+
elif exception.errno == ENOENT: # No such file or directory - parent directory is missing
27+
raise
28+
elif exception.errno == EACCES and sys.platform != "win32": # Permission denied - parent dir is R/O
29+
raise # note windows does not allow you to make a folder r/o only files
1530
else:
1631
self._lock_file_fd = fd
1732

@@ -20,8 +35,7 @@ def _release(self):
2035
self._lock_file_fd = None
2136
try:
2237
os.remove(self._lock_file)
23-
# The file is already deleted and that's what we want.
24-
except OSError:
38+
except OSError: # the file is already deleted and that's what we want
2539
pass
2640

2741

src/filelock/_util.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import os
2+
import stat
3+
import sys
4+
5+
PermissionError = PermissionError if sys.version_info[0] == 3 else OSError
6+
7+
8+
def raise_on_exist_ro_file(filename):
9+
try:
10+
file_stat = os.stat(filename) # use stat to do exists + can write to check without race condition
11+
except OSError:
12+
return None # swallow does not exist or other errors
13+
14+
if file_stat.st_mtime != 0: # if os.stat returns but modification is zero that's an invalid os.stat - ignore it
15+
if not (file_stat.st_mode & stat.S_IWUSR):
16+
raise PermissionError("Permission denied: {!r}".format(filename))
17+
18+
19+
__all__ = [
20+
"raise_on_exist_ro_file",
21+
"PermissionError",
22+
]

src/filelock/_windows.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import os
2+
from errno import ENOENT
23

34
from ._api import BaseFileLock
5+
from ._util import raise_on_exist_ro_file
46

57
try:
68
import msvcrt
@@ -12,11 +14,17 @@ class WindowsFileLock(BaseFileLock):
1214
"""Uses the :func:`msvcrt.locking` function to hard lock the lock file on windows systems."""
1315

1416
def _acquire(self):
15-
open_mode = os.O_RDWR | os.O_CREAT | os.O_TRUNC
17+
raise_on_exist_ro_file(self._lock_file)
18+
mode = (
19+
os.O_RDWR # open for read and write
20+
| os.O_CREAT # create file if not exists
21+
| os.O_TRUNC # truncate file if not empty
22+
)
1623
try:
17-
fd = os.open(self._lock_file, open_mode)
18-
except OSError:
19-
pass
24+
fd = os.open(self._lock_file, mode)
25+
except OSError as exception:
26+
if exception.errno == ENOENT: # No such file or directory
27+
raise
2028
else:
2129
try:
2230
msvcrt.locking(fd, msvcrt.LK_NBLCK, 1)

tests/test_filelock.py

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@
33
import logging
44
import sys
55
import threading
6+
from contextlib import contextmanager
7+
from stat import S_IWGRP, S_IWOTH, S_IWUSR
68

79
import pytest
810

911
from filelock import FileLock, SoftFileLock, Timeout
12+
from filelock._util import PermissionError
1013

1114

1215
@pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock])
@@ -29,6 +32,52 @@ def test_simple(lock_type, tmp_path, caplog):
2932
assert [r.levelno for r in caplog.records] == [logging.DEBUG, logging.DEBUG, logging.DEBUG, logging.DEBUG]
3033

3134

35+
@contextmanager
36+
def make_ro(path):
37+
write = S_IWUSR | S_IWGRP | S_IWOTH
38+
path.chmod(path.stat().st_mode & ~write)
39+
yield
40+
path.chmod(path.stat().st_mode | write)
41+
42+
43+
@pytest.fixture()
44+
def tmp_path_ro(tmp_path):
45+
with make_ro(tmp_path):
46+
yield tmp_path
47+
48+
49+
@pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock])
50+
@pytest.mark.skipif(sys.platform == "win32", reason="Windows does not have read only folders")
51+
def test_ro_folder(lock_type, tmp_path_ro):
52+
lock = lock_type(str(tmp_path_ro / "a"))
53+
with pytest.raises(PermissionError, match="Permission denied"):
54+
lock.acquire()
55+
56+
57+
@pytest.fixture()
58+
def tmp_file_ro(tmp_path):
59+
filename = tmp_path / "a"
60+
filename.write_text("")
61+
with make_ro(filename):
62+
yield filename
63+
64+
65+
@pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock])
66+
def test_ro_file(lock_type, tmp_file_ro):
67+
lock = lock_type(str(tmp_file_ro))
68+
with pytest.raises(PermissionError, match="Permission denied"):
69+
lock.acquire()
70+
71+
72+
@pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock])
73+
def test_missing_directory(lock_type, tmp_path_ro):
74+
lock_path = tmp_path_ro / "a" / "b"
75+
lock = lock_type(str(lock_path))
76+
77+
with pytest.raises(OSError, match="No such file or directory:"):
78+
lock.acquire()
79+
80+
3281
@pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock])
3382
def test_nested_context_manager(lock_type, tmp_path):
3483
# lock is not released before the most outer with statement that locked the lock, is left
@@ -93,8 +142,8 @@ def test_nested_forced_release(lock_type, tmp_path):
93142

94143

95144
class ExThread(threading.Thread):
96-
def __init__(self, target):
97-
super(ExThread, self).__init__(target=target)
145+
def __init__(self, target, name):
146+
super(ExThread, self).__init__(target=target, name=name)
98147
self.ex = None
99148

100149
def run(self):
@@ -106,6 +155,7 @@ def run(self):
106155
def join(self, timeout=None):
107156
super(ExThread, self).join(timeout=timeout)
108157
if self.ex is not None:
158+
print("fail from thread {}".format(self.name))
109159
if sys.version_info[0] == 2:
110160
wrapper_ex = self.ex[1]
111161
raise (wrapper_ex.__class__, wrapper_ex, self.ex[2])
@@ -124,7 +174,7 @@ def thread_work():
124174
with lock:
125175
assert lock.is_locked
126176

127-
threads = [ExThread(target=thread_work) for _ in range(100)]
177+
threads = [ExThread(target=thread_work, name="t{}".format(i)) for i in range(100)]
128178
for thread in threads:
129179
thread.start()
130180
for thread in threads:
@@ -138,21 +188,21 @@ def test_threaded_lock_different_lock_obj(lock_type, tmp_path):
138188
# Runs multiple threads, which acquire the same lock file with a different FileLock object. When thread group 1
139189
# acquired the lock, thread group 2 must not hold their lock.
140190

141-
def thread_work_one():
191+
def t_1():
142192
for _ in range(1000):
143193
with lock_1:
144194
assert lock_1.is_locked
145195
assert not lock_2.is_locked
146196

147-
def thread_work_two():
197+
def t_2():
148198
for _ in range(1000):
149199
with lock_2:
150200
assert not lock_1.is_locked
151201
assert lock_2.is_locked
152202

153203
lock_path = tmp_path / "a"
154204
lock_1, lock_2 = lock_type(str(lock_path)), lock_type(str(lock_path))
155-
threads = [(ExThread(target=thread_work_one), ExThread(target=thread_work_two)) for i in range(10)]
205+
threads = [(ExThread(t_1, "t1_{}".format(i)), ExThread(t_2, "t2_{}".format(i))) for i in range(10)]
156206

157207
for thread_1, thread_2 in threads:
158208
thread_1.start()

tox.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,6 @@ max-line-length = 120
113113

114114
[pep8]
115115
max-line-length = 120
116+
117+
[pytest]
118+
timeout = 120

whitelist.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,18 @@ autodoc
33
autosectionlabel
44
caplog
55
creat
6+
eacces
7+
eexist
8+
enoent
69
exc
710
fcntl
811
filelock
912
fmt
1013
intersphinx
1114
intervall
15+
iwgrp
16+
iwoth
17+
iwusr
1218
levelno
1319
lk
1420
lockfile
@@ -18,10 +24,12 @@ nitpicky
1824
param
1925
pygments
2026
rdwr
27+
ro
2128
skipif
2229
src
2330
tmp
2431
trunc
2532
typehints
2633
unlck
34+
util
2735
wronly

0 commit comments

Comments
 (0)