Skip to content

Commit 6343c92

Browse files
committed
Handle BrokenPipeError gracefully.
1 parent 7bf91e6 commit 6343c92

File tree

3 files changed

+56
-2
lines changed

3 files changed

+56
-2
lines changed

news/4170.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Handle ``BrokenPipeError`` gracefully by logging a message to ``stderr``.

src/pip/_internal/cli/base_command.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
"""Base Command class, and related routines"""
2-
from __future__ import absolute_import
2+
from __future__ import absolute_import, print_function
33

44
import logging
55
import logging.config
66
import optparse
77
import os
88
import sys
9+
import traceback
910

1011
from pip._internal.cli import cmdoptions
1112
from pip._internal.cli.parser import (
@@ -27,7 +28,7 @@
2728
)
2829
from pip._internal.req.req_file import parse_requirements
2930
from pip._internal.utils.deprecation import deprecated
30-
from pip._internal.utils.logging import setup_logging
31+
from pip._internal.utils.logging import BrokenStdoutLoggingError, setup_logging
3132
from pip._internal.utils.misc import (
3233
get_prog, normalize_path, redact_password_from_url,
3334
)
@@ -182,6 +183,14 @@ def main(self, args):
182183
logger.critical('ERROR: %s', exc)
183184
logger.debug('Exception information:', exc_info=True)
184185

186+
return ERROR
187+
except BrokenStdoutLoggingError:
188+
# Bypass our logger and write any remaining messages to stderr
189+
# because stdout no longer works.
190+
print('ERROR: Pipe to stdout was broken', file=sys.stderr)
191+
if logger.getEffectiveLevel() <= logging.DEBUG:
192+
traceback.print_exc(file=sys.stderr)
193+
185194
return ERROR
186195
except KeyboardInterrupt:
187196
logger.critical('Operation cancelled by user')

src/pip/_internal/utils/logging.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
from __future__ import absolute_import
22

33
import contextlib
4+
import errno
45
import logging
56
import logging.handlers
67
import os
8+
import sys
9+
10+
from pip._vendor.six import PY2
711

812
from pip._internal.utils.compat import WINDOWS
913
from pip._internal.utils.misc import ensure_dir
@@ -33,6 +37,23 @@ class BrokenStdoutLoggingError(Exception):
3337
pass
3438

3539

40+
if PY2:
41+
# BrokenPipeError does not exist in Python 2.
42+
def _is_broken_pipe_error(exc_class, exc):
43+
"""
44+
Return whether an exception is a broken pipe error.
45+
46+
Args:
47+
exc_class: an exception class.
48+
exc: an exception instance.
49+
"""
50+
return (exc_class is IOError and exc.errno == errno.EPIPE)
51+
52+
else:
53+
def _is_broken_pipe_error(exc_class, exc):
54+
return (exc_class is BrokenPipeError) # noqa: F821
55+
56+
3657
@contextlib.contextmanager
3758
def indent_log(num=2):
3859
"""
@@ -90,6 +111,16 @@ def __init__(self, stream=None, no_color=None):
90111
if WINDOWS and colorama:
91112
self.stream = colorama.AnsiToWin32(self.stream)
92113

114+
def _using_stdout(self):
115+
"""
116+
Return whether the handler is using sys.stdout.
117+
"""
118+
if WINDOWS and colorama:
119+
# Then self.stream is an AnsiToWin32 object.
120+
return self.stream.wrapped is sys.stdout
121+
122+
return self.stream is sys.stdout
123+
93124
def should_color(self):
94125
# Don't colorize things if we do not have colorama or if told not to
95126
if not colorama or self._no_color:
@@ -122,6 +153,19 @@ def format(self, record):
122153

123154
return msg
124155

156+
# The logging module says handleError() can be customized.
157+
def handleError(self, record):
158+
exc_class, exc = sys.exc_info()[:2]
159+
# If a broken pipe occurred while calling write() or flush() on the
160+
# stdout stream in logging's Handler.emit(), then raise our special
161+
# exception so we can handle it in main() instead of logging the
162+
# broken pipe error and continuing.
163+
if (exc_class and self._using_stdout() and
164+
_is_broken_pipe_error(exc_class, exc)):
165+
raise BrokenStdoutLoggingError()
166+
167+
return super(ColorizedStreamHandler, self).handleError(record)
168+
125169

126170
class BetterRotatingFileHandler(logging.handlers.RotatingFileHandler):
127171

0 commit comments

Comments
 (0)