Skip to content

Commit 7a9e1f3

Browse files
committed
Handle BrokenPipeError gracefully.
1 parent e6d0127 commit 7a9e1f3

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 a broken stdout pipe more gracefully (e.g. when running ``pip list | head``).

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
)
@@ -191,6 +192,14 @@ def main(self, args):
191192
logger.critical('ERROR: %s', exc)
192193
logger.debug('Exception information:', exc_info=True)
193194

195+
return ERROR
196+
except BrokenStdoutLoggingError:
197+
# Bypass our logger and write any remaining messages to stderr
198+
# because stdout no longer works.
199+
print('ERROR: Pipe to stdout was broken', file=sys.stderr)
200+
if logger.getEffectiveLevel() <= logging.DEBUG:
201+
traceback.print_exc(file=sys.stderr)
202+
194203
return ERROR
195204
except KeyboardInterrupt:
196205
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
"""
@@ -103,6 +124,16 @@ def __init__(self, stream=None, no_color=None):
103124
if WINDOWS and colorama:
104125
self.stream = colorama.AnsiToWin32(self.stream)
105126

127+
def _using_stdout(self):
128+
"""
129+
Return whether the handler is using sys.stdout.
130+
"""
131+
if WINDOWS and colorama:
132+
# Then self.stream is an AnsiToWin32 object.
133+
return self.stream.wrapped is sys.stdout
134+
135+
return self.stream is sys.stdout
136+
106137
def should_color(self):
107138
# Don't colorize things if we do not have colorama or if told not to
108139
if not colorama or self._no_color:
@@ -135,6 +166,19 @@ def format(self, record):
135166

136167
return msg
137168

169+
# The logging module says handleError() can be customized.
170+
def handleError(self, record):
171+
exc_class, exc = sys.exc_info()[:2]
172+
# If a broken pipe occurred while calling write() or flush() on the
173+
# stdout stream in logging's Handler.emit(), then raise our special
174+
# exception so we can handle it in main() instead of logging the
175+
# broken pipe error and continuing.
176+
if (exc_class and self._using_stdout() and
177+
_is_broken_pipe_error(exc_class, exc)):
178+
raise BrokenStdoutLoggingError()
179+
180+
return super(ColorizedStreamHandler, self).handleError(record)
181+
138182

139183
class BetterRotatingFileHandler(logging.handlers.RotatingFileHandler):
140184

0 commit comments

Comments
 (0)