diff --git a/mercurial/utils/procutil.py b/mercurial/utils/procutil.py --- a/mercurial/utils/procutil.py +++ b/mercurial/utils/procutil.py @@ -119,18 +119,25 @@ if pycompat.ispy3: # a silly wrapper to make a bytes stream backed by a unicode one. stdin = sys.stdin.buffer stdout = _make_write_all(sys.stdout.buffer) + stderr = _make_write_all(sys.stderr.buffer) + if pycompat.iswindows: + # Work around Windows bugs. + stdout = platform.winstdout(stdout) + stderr = platform.winstdout(stderr) if isatty(stdout): # The standard library doesn't offer line-buffered binary streams. stdout = make_line_buffered(stdout) - stderr = _make_write_all(sys.stderr.buffer) else: # Python 2 uses the I/O streams provided by the C library. stdin = sys.stdin stdout = sys.stdout + stderr = sys.stderr + if pycompat.iswindows: + # Work around Windows bugs. + stdout = platform.winstdout(stdout) + stderr = platform.winstdout(stderr) if isatty(stdout): if pycompat.iswindows: - # Work around size limit when writing to console. - stdout = platform.winstdout(stdout) # The Windows C runtime library doesn't support line buffering. stdout = make_line_buffered(stdout) else: @@ -138,7 +145,6 @@ else: # replace a TTY destined stdout with a pipe destined stdout (e.g. # pager), we want line buffering. stdout = os.fdopen(stdout.fileno(), 'wb', 1) - stderr = sys.stderr findexe = platform.findexe diff --git a/mercurial/windows.py b/mercurial/windows.py --- a/mercurial/windows.py +++ b/mercurial/windows.py @@ -197,6 +197,7 @@ class winstdout(object): def __init__(self, fp): self.fp = fp + self.throttle = not pycompat.ispy3 and fp.isatty() def __getattr__(self, key): return getattr(self.fp, key) @@ -208,13 +209,16 @@ class winstdout(object): pass def write(self, s): + if not pycompat.ispy3: + self.softspace = 0 try: + if not self.throttle: + return self.fp.write(s) # This is workaround for "Not enough space" error on # writing large size of data to console. limit = 16000 l = len(s) start = 0 - self.softspace = 0 while start < l: end = start + limit self.fp.write(s[start:end]) diff --git a/tests/test-stdio.py b/tests/test-stdio.py --- a/tests/test-stdio.py +++ b/tests/test-stdio.py @@ -13,7 +13,7 @@ import sys import tempfile import unittest -from mercurial import pycompat +from mercurial import pycompat, util if pycompat.ispy3: @@ -71,6 +71,34 @@ with os.fdopen( ''' +TEST_BROKEN_PIPE_CHILD_SCRIPT = r''' +import os +import pickle + +from mercurial import dispatch +from mercurial.utils import procutil + +dispatch.initstdio() +procutil.stdin.read(1) # wait until parent process closed pipe +try: + procutil.{stream}.write(b'test') + procutil.{stream}.flush() +except EnvironmentError as e: + with os.fdopen( + os.open( + {err_fn!r}, + os.O_WRONLY + | getattr(os, 'O_BINARY', 0) + | getattr(os, 'O_TEMPORARY', 0), + ), + 'wb', + ) as err_f: + pickle.dump(e, err_f) +# Exit early to suppress further broken pipe errors at interpreter shutdown. +os._exit(0) +''' + + @contextlib.contextmanager def _closing(fds): try: @@ -148,11 +176,15 @@ class TestStdio(unittest.TestCase): check_output, python_args=[], post_child_check=None, + stdin_generator=None, ): assert stream in ('stdout', 'stderr') - with rwpair_generator() as (stream_receiver, child_stream), open( - os.devnull, 'rb' - ) as child_stdin: + if stdin_generator is None: + stdin_generator = open(os.devnull, 'rb') + with rwpair_generator() as ( + stream_receiver, + child_stream, + ), stdin_generator as child_stdin: proc = subprocess.Popen( [sys.executable] + python_args + ['-c', child_script], stdin=child_stdin, @@ -295,6 +327,37 @@ class TestStdio(unittest.TestCase): def test_large_write_stderr_ptys_unbuffered(self): self._test_large_write('stderr', _ptys, python_args=['-u']) + def _test_broken_pipe(self, stream): + assert stream in ('stdout', 'stderr') + + def check_output(stream_receiver, proc): + os.close(stream_receiver) + proc.stdin.write(b'x') + proc.stdin.close() + + def post_child_check(): + err = util.pickle.load(err_f) + self.assertEqual(err.errno, errno.EPIPE) + self.assertEqual(err.strerror, "Broken pipe") + + with tempfile.NamedTemporaryFile('rb') as err_f: + self._test( + TEST_BROKEN_PIPE_CHILD_SCRIPT.format( + stream=stream, err_fn=err_f.name + ), + stream, + _pipes, + check_output, + post_child_check=post_child_check, + stdin_generator=util.nullcontextmanager(subprocess.PIPE), + ) + + def test_broken_pipe_stdout(self): + self._test_broken_pipe('stdout') + + def test_broken_pipe_stderr(self): + self._test_broken_pipe('stderr') + if __name__ == '__main__': import silenttestrunner