Skip to content

Commit 70b921f

Browse files
committed
Fix handling output from tty-enabled containers.
Treat output from TTY-enabled containers as raw streams, rather than as multiplexed streams. The docker API docs specify that tty-enabled containers don't multiplex. Also update tests to pass with these changes, and changed the code used to read raw streams to not read line-by-line, and to not skip empty lines. Addresses issue #630 Signed-off-by: Dan O'Reilly <[email protected]>
1 parent 7b18543 commit 70b921f

File tree

4 files changed

+87
-49
lines changed

4 files changed

+87
-49
lines changed

docker/client.py

Lines changed: 3 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -40,28 +40,7 @@ def attach(self, container, stdout=True, stderr=True,
4040
u = self._url("/containers/{0}/attach".format(container))
4141
response = self._post(u, params=params, stream=stream)
4242

43-
# Stream multi-plexing was only introduced in API v1.6. Anything before
44-
# that needs old-style streaming.
45-
if utils.compare_version('1.6', self._version) < 0:
46-
def stream_result():
47-
self._raise_for_status(response)
48-
for line in response.iter_lines(chunk_size=1,
49-
decode_unicode=True):
50-
# filter out keep-alive new lines
51-
if line:
52-
yield line
53-
54-
return stream_result() if stream else \
55-
self._result(response, binary=True)
56-
57-
sep = bytes() if six.PY3 else str()
58-
59-
if stream:
60-
return self._multiplexed_response_stream_helper(response)
61-
else:
62-
return sep.join(
63-
[x for x in self._multiplexed_buffer_helper(response)]
64-
)
43+
return self._get_result(container, stream, response)
6544

6645
@check_resource
6746
def attach_socket(self, container, params=None, ws=False):
@@ -363,17 +342,7 @@ def exec_start(self, exec_id, detach=False, tty=False, stream=False):
363342

364343
res = self._post_json(self._url('/exec/{0}/start'.format(exec_id)),
365344
data=data, stream=stream)
366-
self._raise_for_status(res)
367-
if stream:
368-
return self._multiplexed_response_stream_helper(res)
369-
elif six.PY3:
370-
return bytes().join(
371-
[x for x in self._multiplexed_buffer_helper(res)]
372-
)
373-
else:
374-
return str().join(
375-
[x for x in self._multiplexed_buffer_helper(res)]
376-
)
345+
return self._get_result_tty(stream, res, tty)
377346

378347
@check_resource
379348
def export(self, container):
@@ -588,16 +557,7 @@ def logs(self, container, stdout=True, stderr=True, stream=False,
588557
params['tail'] = tail
589558
url = self._url("/containers/{0}/logs".format(container))
590559
res = self._get(url, params=params, stream=stream)
591-
if stream:
592-
return self._multiplexed_response_stream_helper(res)
593-
elif six.PY3:
594-
return bytes().join(
595-
[x for x in self._multiplexed_buffer_helper(res)]
596-
)
597-
else:
598-
return str().join(
599-
[x for x in self._multiplexed_buffer_helper(res)]
600-
)
560+
return self._get_result(container, stream, res)
601561
return self.attach(
602562
container,
603563
stdout=stdout,

docker/clientbase.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,46 @@ def _multiplexed_response_stream_helper(self, response):
221221
break
222222
yield data
223223

224+
def _stream_raw_result_old(self, response):
225+
''' Stream raw output for API versions below 1.6 '''
226+
self._raise_for_status(response)
227+
for line in response.iter_lines(chunk_size=1,
228+
decode_unicode=True):
229+
# filter out keep-alive new lines
230+
if line:
231+
yield line
232+
233+
def _stream_raw_result(self, response):
234+
''' Stream result for TTY-enabled container above API 1.6 '''
235+
self._raise_for_status(response)
236+
for out in response.iter_content(chunk_size=1, decode_unicode=True):
237+
yield out
238+
239+
def _get_result(self, container, stream, res):
240+
cont = self.inspect_container(container)
241+
return self._get_result_tty(stream, res, cont['Config']['Tty'])
242+
243+
def _get_result_tty(self, stream, res, is_tty):
244+
# Stream multi-plexing was only introduced in API v1.6. Anything
245+
# before that needs old-style streaming.
246+
if utils.compare_version('1.6', self._version) < 0:
247+
return self._stream_raw_result_old(res)
248+
249+
# We should also use raw streaming (without keep-alives)
250+
# if we're dealing with a tty-enabled container.
251+
if is_tty:
252+
return self._stream_raw_result(res) if stream else \
253+
self._result(res, binary=True)
254+
255+
self._raise_for_status(res)
256+
sep = six.binary_type()
257+
if stream:
258+
return self._multiplexed_response_stream_helper(res)
259+
else:
260+
return sep.join(
261+
[x for x in self._multiplexed_buffer_helper(res)]
262+
)
263+
224264
def get_adapter(self, url):
225265
try:
226266
return super(ClientBase, self).get_adapter(url)

tests/fake_api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,11 @@ def post_fake_create_container():
129129
return status_code, response
130130

131131

132-
def get_fake_inspect_container():
132+
def get_fake_inspect_container(tty=False):
133133
status_code = 200
134134
response = {
135135
'Id': FAKE_CONTAINER_ID,
136-
'Config': {'Privileged': True},
136+
'Config': {'Privileged': True, 'Tty': tty},
137137
'ID': FAKE_CONTAINER_ID,
138138
'Image': 'busybox:latest',
139139
"State": {

tests/test.py

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,14 @@ def fake_resolve_authconfig(authconfig, registry=None):
6969
return None
7070

7171

72+
def fake_inspect_container(self, container, tty=False):
73+
return fake_api.get_fake_inspect_container(tty=tty)[1]
74+
75+
76+
def fake_inspect_container_tty(self, container):
77+
return fake_inspect_container(self, container, tty=True)
78+
79+
7280
def fake_resp(url, data=None, **kwargs):
7381
status_code, content = fake_api.fake_responses[url]()
7482
return response(status_code=status_code, content=content)
@@ -1546,7 +1554,9 @@ def test_url_compatibility_tcp(self):
15461554

15471555
def test_logs(self):
15481556
try:
1549-
logs = self.client.logs(fake_api.FAKE_CONTAINER_ID)
1557+
with mock.patch('docker.Client.inspect_container',
1558+
fake_inspect_container):
1559+
logs = self.client.logs(fake_api.FAKE_CONTAINER_ID)
15501560
except Exception as e:
15511561
self.fail('Command should not raise exception: {0}'.format(e))
15521562

@@ -1565,7 +1575,9 @@ def test_logs(self):
15651575

15661576
def test_logs_with_dict_instead_of_id(self):
15671577
try:
1568-
logs = self.client.logs({'Id': fake_api.FAKE_CONTAINER_ID})
1578+
with mock.patch('docker.Client.inspect_container',
1579+
fake_inspect_container):
1580+
logs = self.client.logs({'Id': fake_api.FAKE_CONTAINER_ID})
15691581
except Exception as e:
15701582
self.fail('Command should not raise exception: {0}'.format(e))
15711583

@@ -1584,7 +1596,9 @@ def test_logs_with_dict_instead_of_id(self):
15841596

15851597
def test_log_streaming(self):
15861598
try:
1587-
self.client.logs(fake_api.FAKE_CONTAINER_ID, stream=True)
1599+
with mock.patch('docker.Client.inspect_container',
1600+
fake_inspect_container):
1601+
self.client.logs(fake_api.FAKE_CONTAINER_ID, stream=True)
15881602
except Exception as e:
15891603
self.fail('Command should not raise exception: {0}'.format(e))
15901604

@@ -1598,7 +1612,10 @@ def test_log_streaming(self):
15981612

15991613
def test_log_tail(self):
16001614
try:
1601-
self.client.logs(fake_api.FAKE_CONTAINER_ID, stream=False, tail=10)
1615+
with mock.patch('docker.Client.inspect_container',
1616+
fake_inspect_container):
1617+
self.client.logs(fake_api.FAKE_CONTAINER_ID, stream=False,
1618+
tail=10)
16021619
except Exception as e:
16031620
self.fail('Command should not raise exception: {0}'.format(e))
16041621

@@ -1610,6 +1627,27 @@ def test_log_tail(self):
16101627
stream=False
16111628
)
16121629

1630+
def test_log_tty(self):
1631+
try:
1632+
m = mock.Mock()
1633+
with mock.patch('docker.Client.inspect_container',
1634+
fake_inspect_container_tty):
1635+
with mock.patch('docker.Client._stream_raw_result',
1636+
m):
1637+
self.client.logs(fake_api.FAKE_CONTAINER_ID,
1638+
stream=True)
1639+
except Exception as e:
1640+
self.fail('Command should not raise exception: {0}'.format(e))
1641+
1642+
self.assertTrue(m.called)
1643+
fake_request.assert_called_with(
1644+
url_prefix + 'containers/3cc2351ab11b/logs',
1645+
params={'timestamps': 0, 'follow': 1, 'stderr': 1, 'stdout': 1,
1646+
'tail': 'all'},
1647+
timeout=DEFAULT_TIMEOUT_SECONDS,
1648+
stream=True
1649+
)
1650+
16131651
def test_diff(self):
16141652
try:
16151653
self.client.diff(fake_api.FAKE_CONTAINER_ID)

0 commit comments

Comments
 (0)