Skip to content
This repository was archived by the owner on Aug 26, 2020. It is now read-only.

Commit f4d3456

Browse files
authored
change: stream stderr even when capture_error is True (#233)
1 parent e82e5e3 commit f4d3456

File tree

3 files changed

+76
-13
lines changed

3 files changed

+76
-13
lines changed

src/sagemaker_containers/_process.py

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"""Placeholder docstring"""
1414
from __future__ import absolute_import
1515

16+
import io
1617
import os
1718
import subprocess
1819
import sys
@@ -24,30 +25,82 @@
2425

2526

2627
def create(cmd, error_class, cwd=None, capture_error=False, **kwargs):
27-
"""Placeholder docstring"""
28+
"""Create subprocess.Popen object for the given command.
29+
30+
Args:
31+
cmd (list): The command to be run.
32+
error_class (cls): The class to use when raising an exception.
33+
cwd (str): The location from which to run the command (default: None).
34+
If None, this defaults to the ``code_dir`` of the environment.
35+
capture_error (bool): whether or not to direct stderr to a stream
36+
that can later be read (default: False).
37+
**kwargs: Extra arguments that are passed to the subprocess.Popen constructor.
38+
39+
Returns:
40+
subprocess.Popen: the process for the given command
41+
42+
Raises:
43+
error_class: if there is an exception raised when creating the process
44+
"""
2845
try:
46+
# Capture both so that we can control the order of when stdout and stderr are streamed
47+
stdout = subprocess.PIPE if capture_error else None
2948
stderr = subprocess.PIPE if capture_error else None
49+
3050
return subprocess.Popen(
31-
cmd, env=os.environ, cwd=cwd or _env.code_dir, stderr=stderr, **kwargs
51+
cmd, env=os.environ, cwd=cwd or _env.code_dir, stdout=stdout, stderr=stderr, **kwargs
3252
)
3353
except Exception as e: # pylint: disable=broad-except
3454
six.reraise(error_class, error_class(e), sys.exc_info()[2])
3555

3656

3757
def check_error(cmd, error_class, capture_error=False, **kwargs):
3858
# type: (List[str], type, bool, Mapping[str, object]) -> subprocess.Popen
39-
"""Placeholder docstring"""
59+
"""Run a commmand, raising an exception if there is an error.
60+
61+
Args:
62+
cmd (list): The command to be run.
63+
error_class (cls): The class to use when raising an exception.
64+
capture_error (bool): whether or not to include stderr in
65+
the exception message (default: False). In either case,
66+
stderr is streamed to the process's output.
67+
**kwargs: Extra arguments that are passed to the subprocess.Popen constructor.
68+
69+
Returns:
70+
subprocess.Popen: the process for the given command
71+
72+
Raises:
73+
error_class: if there is an exception raised when creating the process
74+
"""
4075
process = create(cmd, error_class, capture_error=capture_error, **kwargs)
4176

4277
if capture_error:
43-
_, stderr = process.communicate()
44-
return_code = process.poll()
78+
# Create a copy of stderr so that it can be read after being streamed
79+
with io.BytesIO() as stderr_copy:
80+
return_code = process.poll()
81+
while return_code is None:
82+
stdout = process.stdout.readline()
83+
sys.stdout.write(stdout.decode("utf-8"))
84+
stderr = process.stderr.readline()
85+
sys.stdout.write(stderr.decode("utf-8"))
86+
87+
stderr_copy.write(stderr)
88+
return_code = process.poll()
89+
90+
# Read the rest of stdout/stdin because readline() reads only one line at a time
91+
stdout = process.stdout.read()
92+
sys.stdout.write(stdout.decode("utf-8"))
93+
stderr = process.stderr.read()
94+
sys.stdout.write(stderr.decode("utf-8"))
95+
96+
stderr_copy.write(stderr)
97+
full_stderr = stderr_copy.getvalue()
4598
else:
46-
stderr = None
99+
full_stderr = None
47100
return_code = process.wait()
48101

49102
if return_code:
50-
raise error_class(return_code=return_code, cmd=" ".join(cmd), output=stderr)
103+
raise error_class(return_code=return_code, cmd=" ".join(cmd), output=full_stderr)
51104
return process
52105

53106

test/unit/test_mpi.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ def test_mpi_master_run(training_env, popen, policy, ssh_client, path_exists):
166166
],
167167
cwd=_env.code_dir,
168168
env=ANY,
169+
stdout=None,
169170
stderr=None,
170171
)
171172

@@ -261,6 +262,7 @@ def test_mpi_master_run_python(
261262
],
262263
cwd=_env.code_dir,
263264
env=ANY,
265+
stdout=None,
264266
stderr=None,
265267
)
266268

test/unit/test_process.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
1+
# Copyright 2018-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
#
33
# Licensed under the Apache License, Version 2.0 (the 'License'). You
44
# may not use this file except in compliance with the License. A copy of
@@ -67,20 +67,28 @@ def test_run_bash(log, popen, entry_point_type_script):
6767
_process.ProcessRunner("launcher.sh", ["--lr", "13"], {}).run()
6868

6969
cmd = ["/bin/sh", "-c", "./launcher.sh --lr 13"]
70-
popen.assert_called_with(cmd, cwd=_env.code_dir, env=os.environ, stderr=None)
70+
popen.assert_called_with(cmd, cwd=_env.code_dir, env=os.environ, stdout=None, stderr=None)
7171
log.assert_called_with(cmd, {})
7272

7373

7474
@patch("subprocess.Popen")
7575
@patch("sagemaker_containers._logging.log_script_invocation")
76-
def test_run_python(log, popen, entry_point_type_script):
77-
popen().communicate.return_value = (None, 0)
76+
def test_run_python_capture_error(log, popen, entry_point_type_script):
77+
mock_process = MagicMock()
78+
mock_process.stdout.readline.return_value = b"stdout"
79+
mock_process.stderr.readline.return_value = b"stderr"
80+
mock_process.stdout.read.return_value = b"stdout"
81+
mock_process.stderr.read.return_value = b"stderr"
82+
mock_process.poll.return_value = 1
83+
popen.return_value = mock_process
7884

7985
with pytest.raises(_errors.ExecuteUserScriptError):
8086
_process.ProcessRunner("launcher.py", ["--lr", "13"], {}).run(capture_error=True)
8187

8288
cmd = [sys.executable, "launcher.py", "--lr", "13"]
83-
popen.assert_called_with(cmd, cwd=_env.code_dir, env=os.environ, stderr=subprocess.PIPE)
89+
popen.assert_called_with(
90+
cmd, cwd=_env.code_dir, env=os.environ, stdout=subprocess.PIPE, stderr=subprocess.PIPE
91+
)
8492
log.assert_called_with(cmd, {})
8593

8694

@@ -91,7 +99,7 @@ def test_run_module(log, popen, entry_point_type_module):
9199
_process.ProcessRunner("module.py", ["--lr", "13"], {}).run()
92100

93101
cmd = [sys.executable, "-m", "module", "--lr", "13"]
94-
popen.assert_called_with(cmd, cwd=_env.code_dir, env=os.environ, stderr=None)
102+
popen.assert_called_with(cmd, cwd=_env.code_dir, env=os.environ, stdout=None, stderr=None)
95103
log.assert_called_with(cmd, {})
96104

97105

0 commit comments

Comments
 (0)