Skip to content

Commit 9b83654

Browse files
authored
Merge pull request #9331 from sbidoul/7969-revert-sbi
Revert #7969 and fix VCS stdout/stderr capture
2 parents e157cf5 + b3d348d commit 9b83654

File tree

10 files changed

+174
-167
lines changed

10 files changed

+174
-167
lines changed

news/8876.bugfix.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fixed hanging VCS subprocess calls when the VCS outputs a large amount of data
2+
on stderr. Restored logging of VCS errors that was inadvertently removed in pip
3+
20.2.

src/pip/_internal/cli/base_command.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
InstallationError,
2323
NetworkConnectionError,
2424
PreviousBuildDirError,
25-
SubProcessError,
2625
UninstallationError,
2726
)
2827
from pip._internal.utils.deprecation import deprecated
@@ -196,7 +195,7 @@ def _main(self, args):
196195

197196
return PREVIOUS_BUILD_DIR_ERROR
198197
except (InstallationError, UninstallationError, BadCommand,
199-
SubProcessError, NetworkConnectionError) as exc:
198+
NetworkConnectionError) as exc:
200199
logger.critical(str(exc))
201200
logger.debug('Exception information:', exc_info=True)
202201

src/pip/_internal/exceptions.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,6 @@ class CommandError(PipError):
8282
"""Raised when there is an error in command-line arguments"""
8383

8484

85-
class SubProcessError(PipError):
86-
"""Raised when there is an error raised while executing a
87-
command in subprocess"""
88-
89-
9085
class PreviousBuildDirError(PipError):
9186
"""Raised when there's a previous conflicting build directory"""
9287

src/pip/_internal/utils/subprocess.py

Lines changed: 50 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,8 @@ def call_subprocess(
115115
extra_environ=None, # type: Optional[Mapping[str, Any]]
116116
unset_environ=None, # type: Optional[Iterable[str]]
117117
spinner=None, # type: Optional[SpinnerInterface]
118-
log_failed_cmd=True # type: Optional[bool]
118+
log_failed_cmd=True, # type: Optional[bool]
119+
stdout_only=False, # type: Optional[bool]
119120
):
120121
# type: (...) -> str
121122
"""
@@ -127,6 +128,9 @@ def call_subprocess(
127128
unset_environ: an iterable of environment variable names to unset
128129
prior to calling subprocess.Popen().
129130
log_failed_cmd: if false, failed commands are not logged, only raised.
131+
stdout_only: if true, return only stdout, else return both. When true,
132+
logging of both stdout and stderr occurs when the subprocess has
133+
terminated, else logging occurs as subprocess output is produced.
130134
"""
131135
if extra_ok_returncodes is None:
132136
extra_ok_returncodes = []
@@ -177,38 +181,59 @@ def call_subprocess(
177181
proc = subprocess.Popen(
178182
# Convert HiddenText objects to the underlying str.
179183
reveal_command_args(cmd),
180-
stderr=subprocess.STDOUT, stdin=subprocess.PIPE,
181-
stdout=subprocess.PIPE, cwd=cwd, env=env,
184+
stdin=subprocess.PIPE,
185+
stdout=subprocess.PIPE,
186+
stderr=subprocess.STDOUT if not stdout_only else subprocess.PIPE,
187+
cwd=cwd,
188+
env=env,
182189
)
183-
assert proc.stdin
184-
assert proc.stdout
185-
proc.stdin.close()
186190
except Exception as exc:
187191
if log_failed_cmd:
188192
subprocess_logger.critical(
189193
"Error %s while executing command %s", exc, command_desc,
190194
)
191195
raise
192196
all_output = []
193-
while True:
194-
# The "line" value is a unicode string in Python 2.
195-
line = console_to_str(proc.stdout.readline())
196-
if not line:
197-
break
198-
line = line.rstrip()
199-
all_output.append(line + '\n')
197+
if not stdout_only:
198+
assert proc.stdout
199+
assert proc.stdin
200+
proc.stdin.close()
201+
# In this mode, stdout and stderr are in the same pipe.
202+
while True:
203+
# The "line" value is a unicode string in Python 2.
204+
line = console_to_str(proc.stdout.readline())
205+
if not line:
206+
break
207+
line = line.rstrip()
208+
all_output.append(line + '\n')
209+
210+
# Show the line immediately.
211+
log_subprocess(line)
212+
# Update the spinner.
213+
if use_spinner:
214+
assert spinner
215+
spinner.spin()
216+
try:
217+
proc.wait()
218+
finally:
219+
if proc.stdout:
220+
proc.stdout.close()
221+
output = ''.join(all_output)
222+
else:
223+
# In this mode, stdout and stderr are in different pipes.
224+
# We must use communicate() which is the only safe way to read both.
225+
out_bytes, err_bytes = proc.communicate()
226+
# log line by line to preserve pip log indenting
227+
out = console_to_str(out_bytes)
228+
for out_line in out.splitlines():
229+
log_subprocess(out_line)
230+
all_output.append(out)
231+
err = console_to_str(err_bytes)
232+
for err_line in err.splitlines():
233+
log_subprocess(err_line)
234+
all_output.append(err)
235+
output = out
200236

201-
# Show the line immediately.
202-
log_subprocess(line)
203-
# Update the spinner.
204-
if use_spinner:
205-
assert spinner
206-
spinner.spin()
207-
try:
208-
proc.wait()
209-
finally:
210-
if proc.stdout:
211-
proc.stdout.close()
212237
proc_had_error = (
213238
proc.returncode and proc.returncode not in extra_ok_returncodes
214239
)
@@ -243,7 +268,7 @@ def call_subprocess(
243268
else:
244269
raise ValueError('Invalid value: on_returncode={!r}'.format(
245270
on_returncode))
246-
return ''.join(all_output)
271+
return output
247272

248273

249274
def runner_with_spinner_message(message):

src/pip/_internal/vcs/bazaar.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ def export(self, location, url):
4444

4545
url, rev_options = self.get_url_rev_options(url)
4646
self.run_command(
47-
make_command('export', location, url, rev_options.to_args())
47+
make_command('export', location, url, rev_options.to_args()),
48+
show_stdout=False,
4849
)
4950

5051
def fetch_new(self, dest, url, rev_options):
@@ -82,7 +83,9 @@ def get_url_rev_and_auth(cls, url):
8283
@classmethod
8384
def get_remote_url(cls, location):
8485
# type: (str) -> str
85-
urls = cls.run_command(['info'], cwd=location)
86+
urls = cls.run_command(
87+
['info'], show_stdout=False, stdout_only=True, cwd=location
88+
)
8689
for line in urls.splitlines():
8790
line = line.strip()
8891
for x in ('checkout of branch: ',
@@ -98,7 +101,7 @@ def get_remote_url(cls, location):
98101
def get_revision(cls, location):
99102
# type: (str) -> str
100103
revision = cls.run_command(
101-
['revno'], cwd=location,
104+
['revno'], show_stdout=False, stdout_only=True, cwd=location,
102105
)
103106
return revision.splitlines()[-1]
104107

src/pip/_internal/vcs/git.py

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from pip._vendor.packaging.version import parse as parse_version
1111

12-
from pip._internal.exceptions import BadCommand, SubProcessError
12+
from pip._internal.exceptions import BadCommand, InstallationError
1313
from pip._internal.utils.misc import display_path, hide_url
1414
from pip._internal.utils.subprocess import make_command
1515
from pip._internal.utils.temp_dir import TempDirectory
@@ -77,7 +77,9 @@ def is_immutable_rev_checkout(self, url, dest):
7777

7878
def get_git_version(self):
7979
VERSION_PFX = 'git version '
80-
version = self.run_command(['version'])
80+
version = self.run_command(
81+
['version'], show_stdout=False, stdout_only=True
82+
)
8183
if version.startswith(VERSION_PFX):
8284
version = version[len(VERSION_PFX):].split()[0]
8385
else:
@@ -100,7 +102,11 @@ def get_current_branch(cls, location):
100102
# and to suppress the message to stderr.
101103
args = ['symbolic-ref', '-q', 'HEAD']
102104
output = cls.run_command(
103-
args, extra_ok_returncodes=(1, ), cwd=location,
105+
args,
106+
extra_ok_returncodes=(1, ),
107+
show_stdout=False,
108+
stdout_only=True,
109+
cwd=location,
104110
)
105111
ref = output.strip()
106112

@@ -119,7 +125,7 @@ def export(self, location, url):
119125
self.unpack(temp_dir.path, url=url)
120126
self.run_command(
121127
['checkout-index', '-a', '-f', '--prefix', location],
122-
cwd=temp_dir.path
128+
show_stdout=False, cwd=temp_dir.path
123129
)
124130

125131
@classmethod
@@ -133,13 +139,13 @@ def get_revision_sha(cls, dest, rev):
133139
rev: the revision name.
134140
"""
135141
# Pass rev to pre-filter the list.
136-
137-
output = ''
138-
try:
139-
output = cls.run_command(['show-ref', rev], cwd=dest)
140-
except SubProcessError:
141-
pass
142-
142+
output = cls.run_command(
143+
['show-ref', rev],
144+
cwd=dest,
145+
show_stdout=False,
146+
stdout_only=True,
147+
on_returncode='ignore',
148+
)
143149
refs = {}
144150
for line in output.strip().splitlines():
145151
try:
@@ -314,7 +320,10 @@ def get_remote_url(cls, location):
314320
# exits with return code 1 if there are no matching lines.
315321
stdout = cls.run_command(
316322
['config', '--get-regexp', r'remote\..*\.url'],
317-
extra_ok_returncodes=(1, ), cwd=location,
323+
extra_ok_returncodes=(1, ),
324+
show_stdout=False,
325+
stdout_only=True,
326+
cwd=location,
318327
)
319328
remotes = stdout.splitlines()
320329
try:
@@ -336,9 +345,11 @@ def has_commit(cls, location, rev):
336345
"""
337346
try:
338347
cls.run_command(
339-
['rev-parse', '-q', '--verify', "sha^" + rev], cwd=location
348+
['rev-parse', '-q', '--verify', "sha^" + rev],
349+
cwd=location,
350+
log_failed_cmd=False,
340351
)
341-
except SubProcessError:
352+
except InstallationError:
342353
return False
343354
else:
344355
return True
@@ -349,7 +360,10 @@ def get_revision(cls, location, rev=None):
349360
if rev is None:
350361
rev = 'HEAD'
351362
current_rev = cls.run_command(
352-
['rev-parse', rev], cwd=location,
363+
['rev-parse', rev],
364+
show_stdout=False,
365+
stdout_only=True,
366+
cwd=location,
353367
)
354368
return current_rev.strip()
355369

@@ -362,7 +376,10 @@ def get_subdirectory(cls, location):
362376
# find the repo root
363377
git_dir = cls.run_command(
364378
['rev-parse', '--git-dir'],
365-
cwd=location).strip()
379+
show_stdout=False,
380+
stdout_only=True,
381+
cwd=location,
382+
).strip()
366383
if not os.path.isabs(git_dir):
367384
git_dir = os.path.join(location, git_dir)
368385
repo_root = os.path.abspath(os.path.join(git_dir, '..'))
@@ -420,13 +437,16 @@ def get_repository_root(cls, location):
420437
r = cls.run_command(
421438
['rev-parse', '--show-toplevel'],
422439
cwd=location,
440+
show_stdout=False,
441+
stdout_only=True,
442+
on_returncode='raise',
423443
log_failed_cmd=False,
424444
)
425445
except BadCommand:
426446
logger.debug("could not determine if %s is under git control "
427447
"because git is not available", location)
428448
return None
429-
except SubProcessError:
449+
except InstallationError:
430450
return None
431451
return os.path.normpath(r.rstrip('\r\n'))
432452

src/pip/_internal/vcs/mercurial.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import logging
66
import os
77

8-
from pip._internal.exceptions import BadCommand, SubProcessError
8+
from pip._internal.exceptions import BadCommand, InstallationError
99
from pip._internal.utils.misc import display_path
1010
from pip._internal.utils.subprocess import make_command
1111
from pip._internal.utils.temp_dir import TempDirectory
@@ -44,7 +44,7 @@ def export(self, location, url):
4444
self.unpack(temp_dir.path, url=url)
4545

4646
self.run_command(
47-
['archive', location], cwd=temp_dir.path
47+
['archive', location], show_stdout=False, cwd=temp_dir.path
4848
)
4949

5050
def fetch_new(self, dest, url, rev_options):
@@ -90,7 +90,10 @@ def get_remote_url(cls, location):
9090
# type: (str) -> str
9191
url = cls.run_command(
9292
['showconfig', 'paths.default'],
93-
cwd=location).strip()
93+
show_stdout=False,
94+
stdout_only=True,
95+
cwd=location,
96+
).strip()
9497
if cls._is_local_repository(url):
9598
url = path_to_url(url)
9699
return url.strip()
@@ -102,7 +105,11 @@ def get_revision(cls, location):
102105
Return the repository-local changeset revision number, as an integer.
103106
"""
104107
current_revision = cls.run_command(
105-
['parents', '--template={rev}'], cwd=location).strip()
108+
['parents', '--template={rev}'],
109+
show_stdout=False,
110+
stdout_only=True,
111+
cwd=location,
112+
).strip()
106113
return current_revision
107114

108115
@classmethod
@@ -113,7 +120,10 @@ def get_requirement_revision(cls, location):
113120
"""
114121
current_rev_hash = cls.run_command(
115122
['parents', '--template={node}'],
116-
cwd=location).strip()
123+
show_stdout=False,
124+
stdout_only=True,
125+
cwd=location,
126+
).strip()
117127
return current_rev_hash
118128

119129
@classmethod
@@ -129,7 +139,8 @@ def get_subdirectory(cls, location):
129139
"""
130140
# find the repo root
131141
repo_root = cls.run_command(
132-
['root'], cwd=location).strip()
142+
['root'], show_stdout=False, stdout_only=True, cwd=location
143+
).strip()
133144
if not os.path.isabs(repo_root):
134145
repo_root = os.path.abspath(os.path.join(location, repo_root))
135146
return find_path_to_setup_from_repo_root(location, repo_root)
@@ -143,13 +154,16 @@ def get_repository_root(cls, location):
143154
r = cls.run_command(
144155
['root'],
145156
cwd=location,
157+
show_stdout=False,
158+
stdout_only=True,
159+
on_returncode='raise',
146160
log_failed_cmd=False,
147161
)
148162
except BadCommand:
149163
logger.debug("could not determine if %s is under hg control "
150164
"because hg is not available", location)
151165
return None
152-
except SubProcessError:
166+
except InstallationError:
153167
return None
154168
return os.path.normpath(r.rstrip('\r\n'))
155169

0 commit comments

Comments
 (0)