Read about our upcoming Code of Conduct on this issue

This instance will be upgraded to Heptapod 0.28.0 on 2022-01-18 at 14:00 UTC+1 (a few minutes of down time)

Commit d04c79d2 authored by Manuel Jacob's avatar Manuel Jacob
Browse files

windows: always work around EINVAL in case of broken pipe for stdout / stderr

In 29a905fe23ae, I missed the fact that the `winstdout` class works around two
unrelated bugs (size limit when writing to consoles and EINVAL in case of
broken pipe) and that the latter bug happens even when no console is involved.
When writing a test for this, I realized that the same problem applies to
stderr, so I applied the workaround for EINVAL to both stdout and stderr.

The size limit is worked around in the same case as before (consoles on Windows
on Python 2). For that, I changed the `winstdout` class.
parent c2c862b9b544
Pipeline #8025 failed with stage
in 25 minutes and 45 seconds
......@@ -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
......
......@@ -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])
......
......@@ -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
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment