Skip to content

Commit cce077f

Browse files
author
Dan Davis
committed
Fix issue pypa#7280 - capture error and warn, but retry as normal
- TempDirectory() tries to delete the directory as normal - if cleanup fails on Windows due to EACCES, warn about virus scanner - This is a more specific error than previous error, but does not change the behavior when a user is attempting to pip uninstall in a system directory - This changes the messaging, but risks leaving the directory behind. - Leaving the directory behind, however, is what is already happening - The fix for pypa#7479 gives the Virus scanner more time to finish its work, but there is still a possibility for a race condition that leaves the impression that there is an error in pip itself.
1 parent 942ab14 commit cce077f

File tree

5 files changed

+94
-7
lines changed

5 files changed

+94
-7
lines changed

src/pip/_internal/utils/temp_dir.py

+1-6
Original file line numberDiff line numberDiff line change
@@ -128,12 +128,7 @@ def cleanup(self):
128128
try:
129129
rmtree(self._path)
130130
except OSError as e:
131-
skip_error = (
132-
sys.platform == 'win32' and
133-
e.errno == errno.EACCES and
134-
getattr(e, 'winerror', 0) in {5, 32}
135-
)
136-
if skip_error:
131+
if sys.platform == 'win32' and e.errno == errno.EACCES:
137132
logger.warning(
138133
"%s (virus scanner may be holding it)."
139134
"cannot remove '%s'",

tests/lib/filesystem.py

+1
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ def external_file_opener(conn):
6464
# Open the file
6565
try:
6666
f = open(path, 'r')
67+
# NOTE: action is for future use and may be unused
6768
if action == 'lock':
6869
lock_action(f)
6970
elif action == 'noread':

tests/unit/test_utils_filesystem.py

+55-1
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
import os
22
import shutil
33

4+
import psutil
45
import pytest
56

67
from pip._internal.utils.filesystem import copy2_fixed, is_socket
7-
from tests.lib.filesystem import make_socket_file, make_unreadable_file
8+
from tests.lib.filesystem import make_socket_file, make_unreadable_file, FileOpener
89
from tests.lib.path import Path
910

1011

12+
@pytest.fixture()
13+
def process():
14+
return psutil.Process()
15+
16+
1117
def make_file(path):
1218
Path(path).touch()
1319

@@ -27,6 +33,7 @@ def make_dir(path):
2733

2834

2935
skip_on_windows = pytest.mark.skipif("sys.platform == 'win32'")
36+
skip_unless_windows = pytest.mark.skipif("sys.platform != 'win32'")
3037

3138

3239
@skip_on_windows
@@ -59,3 +66,50 @@ def test_copy2_fixed_raises_appropriate_errors(create, error_type, tmpdir):
5966
copy2_fixed(src, dest)
6067

6168
assert not dest.exists()
69+
70+
71+
def test_file_opener_no_file(process):
72+
# The FileOpener cleans up the subprocess even if the parent never sends the path
73+
with FileOpener():
74+
pass
75+
assert len(process.children()) == 0
76+
77+
78+
def test_file_opener_not_found(tmpdir, process):
79+
# The FileOpener cleans up the subprocess even if the file cannot be opened
80+
path = tmpdir.joinpath('foo.txt')
81+
with FileOpener(path):
82+
pass
83+
assert len(process.children()) == 0
84+
85+
86+
def test_file_opener_normal(tmpdir, process):
87+
# The FileOpener cleans up the subprocess when the file exists; also tests that path may be deferred
88+
path = tmpdir.joinpath('foo.txt')
89+
with open(path, 'w') as f:
90+
f.write('Hello\n')
91+
with FileOpener() as opener:
92+
opener.send(path)
93+
assert len(process.children()) == 0
94+
95+
96+
@skip_unless_windows
97+
def test_file_opener_produces_unlink_error(tmpdir, process):
98+
path = tmpdir.joinpath('foo.txt')
99+
with open(path, 'w') as f:
100+
f.write('Hello\n')
101+
with FileOpener(path):
102+
with pytest.raises(OSError):
103+
os.unlink(path)
104+
105+
106+
@skip_unless_windows
107+
def test_file_opener_produces_rmtree_error(tmpdir, process):
108+
subdir = tmpdir.joinpath('foo')
109+
os.mkdir(subdir)
110+
path = subdir.joinpath('bar.txt')
111+
with open(path, 'w') as f:
112+
f.write('Hello\n')
113+
with FileOpener(path):
114+
with pytest.raises(OSError):
115+
shutil.rmtree(subdir)

tests/unit/test_utils_temp_dir.py

+35
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import itertools
2+
import logging
23
import os
4+
import shutil
35
import stat
46
import tempfile
7+
import time
58

69
import pytest
710

@@ -13,6 +16,8 @@
1316
global_tempdir_manager,
1417
)
1518

19+
from tests.lib.filesystem import FileOpener
20+
1621

1722
# No need to test symlinked directories on Windows
1823
@pytest.mark.skipif("sys.platform == 'win32'")
@@ -207,3 +212,33 @@ def test_tempdirectory_asserts_global_tempdir(monkeypatch):
207212
monkeypatch.setattr(temp_dir, "_tempdir_manager", None)
208213
with pytest.raises(AssertionError):
209214
TempDirectory(globally_managed=True)
215+
216+
217+
@pytest.mark.skipif("sys.platform != 'win32'")
218+
def test_temp_dir_warns_if_cannot_clean(caplog):
219+
temp_dir = TempDirectory()
220+
temp_dir_path = temp_dir.path
221+
222+
stime = time.time()
223+
224+
# Capture only at WARNING level and up
225+
with caplog.at_level(logging.WARNING, 'pip._internal.utils.temp_dir'):
226+
# open a file within the temporary directory in a sub-process, so it cannot be cleaned up
227+
with FileOpener() as opener:
228+
subpath = os.path.join(temp_dir_path, 'foo.txt')
229+
with open(subpath, 'w') as f:
230+
f.write('Cannot be deleted')
231+
opener.send(subpath)
232+
# with the file open, attempt to remove the log directory
233+
temp_dir.cleanup()
234+
235+
# assert that a WARNING was logged and that it contains a note about a potential virus scanner
236+
assert 'WARNING' in caplog.text
237+
assert 'virus scanner' in caplog.text
238+
239+
# Assure that the cleanup was properly retried; this fix did not change retries
240+
duration = time.time() - stime
241+
assert duration >= 2.0
242+
243+
# Clean-up for failed TempDirectory cleanup
244+
shutil.rmtree(temp_dir_path, ignore_errors=True)

tools/requirements/tests.txt

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ cryptography==2.8
22
freezegun
33
mock
44
pretend
5+
# Below is to count sub-processes and assure child was cleaned up properly
6+
psutil
57
pytest==3.8.2
68
pytest-cov
79
# Prevent installing 7.0 which has install_requires "pytest >= 3.10".

0 commit comments

Comments
 (0)