Skip to content

Commit ab250e3

Browse files
authored
Merge pull request #6879 from chrahunt/bugfix/dont-lock-selfcheck
Don't use lockfile to protect updates to selfcheck file.
2 parents 5fa906b + 68e6feb commit ab250e3

File tree

4 files changed

+70
-51
lines changed

4 files changed

+70
-51
lines changed

news/6954.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Don't use hardlinks for locking selfcheck state file.

src/pip/_internal/utils/filesystem.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,26 @@
22
import os.path
33
import shutil
44
import stat
5+
from contextlib import contextmanager
6+
from tempfile import NamedTemporaryFile
7+
8+
# NOTE: retrying is not annotated in typeshed as on 2017-07-17, which is
9+
# why we ignore the type on this import.
10+
from pip._vendor.retrying import retry # type: ignore
11+
from pip._vendor.six import PY2
512

613
from pip._internal.utils.compat import get_path_uid
14+
from pip._internal.utils.misc import cast
15+
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
16+
17+
if MYPY_CHECK_RUNNING:
18+
from typing import BinaryIO, Iterator
19+
20+
class NamedTemporaryFileResult(BinaryIO):
21+
@property
22+
def file(self):
23+
# type: () -> BinaryIO
24+
pass
725

826

927
def check_path_owner(path):
@@ -59,3 +77,39 @@ def copy2_fixed(src, dest):
5977
def is_socket(path):
6078
# type: (str) -> bool
6179
return stat.S_ISSOCK(os.lstat(path).st_mode)
80+
81+
82+
@contextmanager
83+
def adjacent_tmp_file(path):
84+
# type: (str) -> Iterator[NamedTemporaryFileResult]
85+
"""Given a path to a file, open a temp file next to it securely and ensure
86+
it is written to disk after the context reaches its end.
87+
"""
88+
with NamedTemporaryFile(
89+
delete=False,
90+
dir=os.path.dirname(path),
91+
prefix=os.path.basename(path),
92+
suffix='.tmp',
93+
) as f:
94+
result = cast('NamedTemporaryFileResult', f)
95+
try:
96+
yield result
97+
finally:
98+
result.file.flush()
99+
os.fsync(result.file.fileno())
100+
101+
102+
_replace_retry = retry(stop_max_delay=1000, wait_fixed=250)
103+
104+
if PY2:
105+
@_replace_retry
106+
def replace(src, dest):
107+
# type: (str, str) -> None
108+
try:
109+
os.rename(src, dest)
110+
except OSError:
111+
os.remove(dest)
112+
os.rename(src, dest)
113+
114+
else:
115+
replace = _replace_retry(os.replace)

src/pip/_internal/utils/outdated.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,19 @@
77
import os.path
88
import sys
99

10-
from pip._vendor import lockfile, pkg_resources
10+
from pip._vendor import pkg_resources
1111
from pip._vendor.packaging import version as packaging_version
1212
from pip._vendor.six import ensure_binary
1313

1414
from pip._internal.cli.cmdoptions import make_search_scope
1515
from pip._internal.index import PackageFinder
1616
from pip._internal.models.selection_prefs import SelectionPreferences
1717
from pip._internal.utils.compat import WINDOWS
18-
from pip._internal.utils.filesystem import check_path_owner
18+
from pip._internal.utils.filesystem import (
19+
adjacent_tmp_file,
20+
check_path_owner,
21+
replace,
22+
)
1923
from pip._internal.utils.misc import ensure_dir, get_installed_version
2024
from pip._internal.utils.packaging import get_installer
2125
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
@@ -86,12 +90,16 @@ def save(self, pypi_version, current_time):
8690

8791
text = json.dumps(state, sort_keys=True, separators=(",", ":"))
8892

89-
# Attempt to write out our version check file
90-
with lockfile.LockFile(self.statefile_path):
93+
with adjacent_tmp_file(self.statefile_path) as f:
94+
f.write(ensure_binary(text))
95+
96+
try:
9197
# Since we have a prefix-specific state file, we can just
9298
# overwrite whatever is there, no need to check.
93-
with open(self.statefile_path, "w") as statefile:
94-
statefile.write(text)
99+
replace(f.name, self.statefile_path)
100+
except OSError:
101+
# Best effort.
102+
pass
95103

96104

97105
def was_installed_by_pip(pkg):

tests/unit/test_unit_outdated.py

Lines changed: 1 addition & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@
22
import json
33
import os
44
import sys
5-
from contextlib import contextmanager
65

76
import freezegun
87
import pretend
98
import pytest
10-
from pip._vendor import lockfile, pkg_resources
9+
from pip._vendor import pkg_resources
1110

1211
from pip._internal.index import InstallationCandidate
1312
from pip._internal.utils import outdated
@@ -162,49 +161,6 @@ def _get_statefile_path(cache_dir, key):
162161
)
163162

164163

165-
def test_self_check_state(monkeypatch, tmpdir):
166-
CONTENT = '''{"key": "pip_prefix", "last_check": "1970-01-02T11:00:00Z",
167-
"pypi_version": "1.0"}'''
168-
fake_file = pretend.stub(
169-
read=pretend.call_recorder(lambda: CONTENT),
170-
write=pretend.call_recorder(lambda s: None),
171-
)
172-
173-
@pretend.call_recorder
174-
@contextmanager
175-
def fake_open(filename, mode='r'):
176-
yield fake_file
177-
178-
monkeypatch.setattr(outdated, 'open', fake_open, raising=False)
179-
180-
@pretend.call_recorder
181-
@contextmanager
182-
def fake_lock(filename):
183-
yield
184-
185-
monkeypatch.setattr(outdated, "check_path_owner", lambda p: True)
186-
187-
monkeypatch.setattr(lockfile, 'LockFile', fake_lock)
188-
189-
cache_dir = tmpdir / 'cache_dir'
190-
key = 'pip_prefix'
191-
monkeypatch.setattr(sys, 'prefix', key)
192-
193-
state = SelfCheckState(cache_dir=cache_dir)
194-
state.save('2.0', datetime.datetime.utcnow())
195-
196-
expected_path = _get_statefile_path(str(cache_dir), key)
197-
assert fake_lock.calls == [pretend.call(expected_path)]
198-
199-
assert fake_open.calls == [
200-
pretend.call(expected_path),
201-
pretend.call(expected_path, 'w'),
202-
]
203-
204-
# json.dumps will call this a number of times
205-
assert len(fake_file.write.calls)
206-
207-
208164
def test_self_check_state_no_cache_dir():
209165
state = SelfCheckState(cache_dir=False)
210166
assert state.state == {}

0 commit comments

Comments
 (0)