Repository: docker/docker-py

Issue:
exec_start does not return command output
I encountered this problem in my work and managed to write an isolated exposing test case attached to this issue.

This program works well on Linux Mint 18.3 and Docker version 17.05.0-ce, build 89658be
but fails on Centos 7.3 with Docker version 1.13.1, build b303bf6/1.13.1

I investigated this issue deeper and wrote an equivalent program using docker REST API and requests_unixsocket library. The program + strace shows that the body in the reply for REST endpoint associated to exec_start if interpreted in the wrong way.


Discussion:
Exposing test case
https://gist.github.com/pstrzelczak/d4fe49b42afa887d613ab5230f7ea368#file-docker-py-1732-py

Output on Centos 7.3 with Docker version 1.13.1, build b303bf6/1.13.1:
```
(docker-venv)[strzelczak@xxx ~]$ python docker-py-1732.py 
Traceback (most recent call last):
  File "docker-py-1732.py", line 12, in <module>
    assert output == "hello\n", "Not hello? '%s'" % output
AssertionError: Not hello? ''
(docker-venv)[strzelczak@xxx ~]$
```

Test case rewritten using docker REST API and requests_unixsocket library (no checks though)
https://gist.github.com/pstrzelczak/0fa2b0866a8311bbbba9c44a8054021b#file-docker-py-1732-requests-py

strace of exposing test case on Centos 7.3 with Docker version 1.13.1, build b303bf6/1.13.1:
```
(docker-venv)[strzelczak@xxx ~]$ strace -f -e trace=network -s1024 python docker-py-1732.py
socket(PF_INET6, SOCK_STREAM, IPPROTO_IP) = 12
bind(12, {sa_family=AF_INET6, sin6_port=htons(0), inet_pton(AF_INET6, "::1", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, 28) = 0
Process 21432 attached
Process 21433 attached
[pid 21433] +++ exited with 0 +++
[pid 21432] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=21433, si_status=0, si_utime=0, si_stime=0} ---
[pid 21432] +++ exited with 0 +++
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=21432, si_status=0, si_utime=0, si_stime=0} ---
Process 21434 attached
Process 21435 attached
[pid 21435] +++ exited with 0 +++
[pid 21434] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=21435, si_status=0, si_utime=0, si_stime=0} ---
[pid 21434] +++ exited with 0 +++
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=21434, si_status=0, si_utime=0, si_stime=0} ---
Process 21436 attached
Process 21437 attached
[pid 21437] +++ exited with 0 +++
[pid 21436] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=21437, si_status=0, si_utime=0, si_stime=0} ---
[pid 21436] +++ exited with 0 +++
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=21436, si_status=0, si_utime=0, si_stime=0} ---
socket(PF_LOCAL, SOCK_STREAM, 0)        = 3
connect(3, {sa_family=AF_LOCAL, sun_path="/var/run/docker.sock"}, 22) = 0
sendto(3, "GET /version HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nUser-Agent: docker-sdk-python/2.5.1\r\n\r\n", 148, 0, NULL, 0) = 148
recvfrom(3, "HTTP/1.1 200 OK\r\nApi-Version: 1.26\r\nContent-Type: application/json\r\nDocker-Experimental: false\r\nServer: Docker/1.13.1 (linux)\r\nDate: Wed, 30 Aug 2017 12:25:38 GMT\r\nContent-Length: 263\r\n\r\n{\"Version\":\"1.13.1\",\"ApiVersion\":\"1.26\",\"MinAPIVersion\":\"1.12\",\"GitCommit\":\"b303bf6/1.13.1\",\"GoVersion\":\"go1.7.4\",\"Os\":\"linux\",\"Arch\":\"amd64\",\"KernelVersion\":\"3.10.0-514.26.2.el7.x86_64\",\"BuildTime\":\"2017-07-03T15:57:33.186109768+00:00\",\"PkgVersion\":\"<unknown>\"}\n", 8192, 0, NULL, NULL) = 450
socket(PF_LOCAL, SOCK_STREAM, 0)        = 4
connect(4, {sa_family=AF_LOCAL, sun_path="/var/run/docker.sock"}, 22) = 0
sendto(4, "POST /v1.26/images/create?tag=latest&fromImage=busybox HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nUser-Agent: docker-sdk-python/2.5.1\r\nContent-Length: 0\r\n\r\n", 209, 0, NULL, 0) = 209
recvfrom(4, "HTTP/1.1 200 OK\r\nApi-Version: 1.26\r\nContent-Type: application/json\r\nDocker-Experimental: false\r\nServer: Docker/1.13.1 (linux)\r\nDate: Wed, 30 Aug 2017 12:25:38 GMT\r\nTransfer-Encoding: chunked\r\n\r\n47\r\n{\"status\":\"Trying to pull repository docker.io/library/busybox ... \"}\r\n\r\n", 8192, 0, NULL, NULL) = 271
recvfrom(4, "43\r\n{\"status\":\"Pulling from docker.io/library/busybox\",\"id\":\"latest\"}\r\n\r\n", 8192, 0, NULL, NULL) = 73
recvfrom(4, "5e\r\n{\"status\":\"Digest: sha256:b82b5740006c1ab823596d2c07f081084ecdb32fd258072707b99f52a3cb8692\"}\r\n\r\n47\r\n{\"status\":\"Status: Image is up to date for docker.io/busybox:latest\"}\r\n\r\n", 8192, 0, NULL, NULL) = 177
recvfrom(4, "0\r\n\r\n", 8192, 0, NULL, NULL) = 5
socket(PF_LOCAL, SOCK_STREAM, 0)        = 5
connect(5, {sa_family=AF_LOCAL, sun_path="/var/run/docker.sock"}, 22) = 0
sendto(5, "POST /v1.26/containers/create HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nUser-Agent: docker-sdk-python/2.5.1\r\nContent-Type: application/json\r\nContent-Length: 190\r\n\r\n{\"Tty\": false, \"NetworkDisabled\": false, \"Image\": \"busybox:latest\", \"Cmd\": [\"cat\"], \"StdinOnce\": false, \"AttachStdin\": false, \"AttachStderr\": false, \"AttachStdout\": false, \"OpenStdin\": true}", 408, 0, NULL, 0) = 408
recvfrom(5, "HTTP/1.1 201 Created\r\nApi-Version: 1.26\r\nContent-Type: application/json\r\nDocker-Experimental: false\r\nServer: Docker/1.13.1 (linux)\r\nDate: Wed, 30 Aug 2017 12:25:40 GMT\r\nContent-Length: 90\r\n\r\n{\"Id\":\"e7eec1c49bbbff357f202bf4d80d4205fa8f59fe042baf3faa218067d3bb745f\",\"Warnings\":null}\n", 8192, 0, NULL, NULL) = 281
socket(PF_LOCAL, SOCK_STREAM, 0)        = 6
connect(6, {sa_family=AF_LOCAL, sun_path="/var/run/docker.sock"}, 22) = 0
sendto(6, "POST /v1.26/containers/e7eec1c49bbbff357f202bf4d80d4205fa8f59fe042baf3faa218067d3bb745f/start HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nUser-Agent: docker-sdk-python/2.5.1\r\nContent-Length: 0\r\n\r\n", 248, 0, NULL, 0) = 248
recvfrom(6, "HTTP/1.1 204 No Content\r\nApi-Version: 1.26\r\nDocker-Experimental: false\r\nServer: Docker/1.13.1 (linux)\r\nDate: Wed, 30 Aug 2017 12:25:40 GMT\r\n\r\n", 8192, 0, NULL, NULL) = 142
socket(PF_LOCAL, SOCK_STREAM, 0)        = 7
connect(7, {sa_family=AF_LOCAL, sun_path="/var/run/docker.sock"}, 22) = 0
sendto(7, "POST /v1.26/containers/e7eec1c49bbbff357f202bf4d80d4205fa8f59fe042baf3faa218067d3bb745f/exec HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nUser-Agent: docker-sdk-python/2.5.1\r\nContent-Type: application/json\r\nContent-Length: 220\r\n\r\n{\"Tty\": false, \"Container\": \"e7eec1c49bbbff357f202bf4d80d4205fa8f59fe042baf3faa218067d3bb745f\", \"Cmd\": [\"echo\", \"hello\"], \"AttachStdin\": false, \"User\": \"\", \"AttachStderr\": true, \"Privileged\": false, \"AttachStdout\": true}", 501, 0, NULL, 0) = 501
recvfrom(7, "HTTP/1.1 201 Created\r\nApi-Version: 1.26\r\nContent-Type: application/json\r\nDocker-Experimental: false\r\nServer: Docker/1.13.1 (linux)\r\nDate: Wed, 30 Aug 2017 12:25:40 GMT\r\nContent-Length: 74\r\n\r\n{\"Id\":\"40cfec7ac80a6903c3aee28a3a5a7832f7c71c0717e3f607b1f30813cde5df7f\"}\n", 8192, 0, NULL, NULL) = 265
socket(PF_LOCAL, SOCK_STREAM, 0)        = 8
connect(8, {sa_family=AF_LOCAL, sun_path="/var/run/docker.sock"}, 22) = 0
sendto(8, "POST /v1.26/exec/40cfec7ac80a6903c3aee28a3a5a7832f7c71c0717e3f607b1f30813cde5df7f/start HTTP/1.1\r\nHost: localhost\r\nConnection: Upgrade\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nUser-Agent: docker-sdk-python/2.5.1\r\nUpgrade: tcp\r\nContent-Type: application/json\r\nContent-Length: 31\r\n\r\n{\"Tty\": false, \"Detach\": false}", 317, 0, NULL, 0) = 317
recvfrom(8, "HTTP/1.1 101 UPGRADED\r\nContent-Type: application/vnd.docker.raw-stream\r\nConnection: Upgrade\r\nUpgrade: tcp\r\n", 8192, 0, NULL, NULL) = 107
recvfrom(8, "Api-Version: 1.26\r\nDocker-Experimental: false\r\nServer: Docker/1.13.1 (linux)\r\n\r\n", 8192, 0, NULL, NULL) = 80
recvfrom(8, "\1\0\0\0\0\0\0\0", 8, 0, NULL, NULL) = 8
socket(PF_LOCAL, SOCK_STREAM, 0)        = 8
connect(8, {sa_family=AF_LOCAL, sun_path="/var/run/docker.sock"}, 22) = 0
sendto(8, "DELETE /v1.26/containers/e7eec1c49bbbff357f202bf4d80d4205fa8f59fe042baf3faa218067d3bb745f?force=True&link=False&v=False HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nUser-Agent: docker-sdk-python/2.5.1\r\nContent-Length: 0\r\n\r\n", 274, 0, NULL, 0) = 274
recvfrom(8, "HTTP/1.1 204 No Content\r\nApi-Version: 1.26\r\nDocker-Experimental: false\r\nServer: Docker/1.13.1 (linux)\r\nDate: Wed, 30 Aug 2017 12:25:40 GMT\r\n\r\n", 8192, 0, NULL, NULL) = 142
Traceback (most recent call last):
  File "docker-py-1732.py", line 12, in <module>
    assert output == "hello\n", "Not hello? '%s'" % output
AssertionError: Not hello? ''
+++ exited with 1 +++
```

strace of exposing test case on Linux Mint 18.3 and Docker version 17.05.0-ce, build 89658be:
```
(docker-venv) strzelczak@yyy /home/strzelczak $ strace -f -e trace=network -s1024 python docker-py-1732.py
socket(PF_INET6, SOCK_STREAM, IPPROTO_IP) = 12
bind(12, {sa_family=AF_INET6, sin6_port=htons(0), inet_pton(AF_INET6, "::1", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, 28) = 0
strace: Process 23712 attached
strace: Process 23713 attached
[pid 23713] +++ exited with 0 +++
[pid 23712] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=23713, si_uid=20001, si_status=0, si_utime=0, si_stime=0} ---
[pid 23712] +++ exited with 0 +++
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=23712, si_uid=20001, si_status=0, si_utime=0, si_stime=0} ---
strace: Process 23714 attached
strace: Process 23715 attached
[pid 23715] +++ exited with 0 +++
[pid 23714] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=23715, si_uid=20001, si_status=0, si_utime=0, si_stime=0} ---
[pid 23714] +++ exited with 0 +++
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=23714, si_uid=20001, si_status=0, si_utime=0, si_stime=0} ---
socket(PF_LOCAL, SOCK_STREAM, 0)        = 3
connect(3, {sa_family=AF_LOCAL, sun_path="/var/run/docker.sock"}, 22) = 0
sendto(3, "GET /version HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nUser-Agent: docker-sdk-python/2.5.1\r\n\r\n", 148, 0, NULL, 0) = 148
recvfrom(3, "HTTP/1.1 200 OK\r\nApi-Version: 1.29\r\nContent-Type: application/json\r\nDocker-Experimental: false\r\nOstype: linux\r\nServer: Docker/17.05.0-ce (linux)\r\nDate: Wed, 30 Aug 2017 12:26:38 GMT\r\nContent-Length: 225\r\n\r\n{\"Version\":\"17.05.0-ce\",\"ApiVersion\":\"1.29\",\"MinAPIVersion\":\"1.12\",\"GitCommit\":\"89658be\",\"GoVersion\":\"go1.7.5\",\"Os\":\"linux\",\"Arch\":\"amd64\",\"KernelVersion\":\"4.8.0-53-generic\",\"BuildTime\":\"2017-05-04T22:06:06.693142599+00:00\"}\n", 8192, 0, NULL, NULL) = 431
socket(PF_LOCAL, SOCK_STREAM, 0)        = 4
connect(4, {sa_family=AF_LOCAL, sun_path="/var/run/docker.sock"}, 22) = 0
sendto(4, "POST /v1.29/images/create?tag=latest&fromImage=busybox HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nUser-Agent: docker-sdk-python/2.5.1\r\nContent-Length: 0\r\n\r\n", 209, 0, NULL, 0) = 209
recvfrom(4, "HTTP/1.1 200 OK\r\nApi-Version: 1.29\r\nContent-Type: application/json\r\nDocker-Experimental: false\r\nOstype: linux\r\nServer: Docker/17.05.0-ce (linux)\r\nDate: Wed, 30 Aug 2017 12:26:40 GMT\r\nTransfer-Encoding: chunked\r\n\r\n39\r\n{\"status\":\"Pulling from library/busybox\",\"id\":\"latest\"}\r\n\r\n", 8192, 0, NULL, NULL) = 276
recvfrom(4, "5e\r\n{\"status\":\"Digest: sha256:b82b5740006c1ab823596d2c07f081084ecdb32fd258072707b99f52a3cb8692\"}\r\n\r\n3d\r\n{\"status\":\"Status: Image is up to date for busybox:latest\"}\r\n\r\n0\r\n\r\n", 8192, 0, NULL, NULL) = 172
socket(PF_LOCAL, SOCK_STREAM, 0)        = 5
connect(5, {sa_family=AF_LOCAL, sun_path="/var/run/docker.sock"}, 22) = 0
sendto(5, "POST /v1.29/containers/create HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nUser-Agent: docker-sdk-python/2.5.1\r\nContent-Type: application/json\r\nContent-Length: 190\r\n\r\n{\"Tty\": false, \"NetworkDisabled\": false, \"Image\": \"busybox:latest\", \"Cmd\": [\"cat\"], \"StdinOnce\": false, \"AttachStdin\": false, \"AttachStderr\": false, \"AttachStdout\": false, \"OpenStdin\": true}", 408, 0, NULL, 0) = 408
recvfrom(5, "HTTP/1.1 201 Created\r\nApi-Version: 1.29\r\nContent-Type: application/json\r\nDocker-Experimental: false\r\nOstype: linux\r\nServer: Docker/17.05.0-ce (linux)\r\nDate: Wed, 30 Aug 2017 12:26:40 GMT\r\nContent-Length: 90\r\n\r\n{\"Id\":\"891fcd673641b13a8ef587f9a7bd190f48b91e4173f83378f94efaa7cec7ad5f\",\"Warnings\":null}\n", 8192, 0, NULL, NULL) = 300
socket(PF_LOCAL, SOCK_STREAM, 0)        = 6
connect(6, {sa_family=AF_LOCAL, sun_path="/var/run/docker.sock"}, 22) = 0
sendto(6, "POST /v1.29/containers/891fcd673641b13a8ef587f9a7bd190f48b91e4173f83378f94efaa7cec7ad5f/start HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nUser-Agent: docker-sdk-python/2.5.1\r\nContent-Length: 0\r\n\r\n", 248, 0, NULL, 0) = 248
recvfrom(6, "HTTP/1.1 204 No Content\r\nApi-Version: 1.29\r\nDocker-Experimental: false\r\nOstype: linux\r\nServer: Docker/17.05.0-ce (linux)\r\nDate: Wed, 30 Aug 2017 12:26:41 GMT\r\n\r\n", 8192, 0, NULL, NULL) = 161
socket(PF_LOCAL, SOCK_STREAM, 0)        = 7
connect(7, {sa_family=AF_LOCAL, sun_path="/var/run/docker.sock"}, 22) = 0
sendto(7, "POST /v1.29/containers/891fcd673641b13a8ef587f9a7bd190f48b91e4173f83378f94efaa7cec7ad5f/exec HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nUser-Agent: docker-sdk-python/2.5.1\r\nContent-Type: application/json\r\nContent-Length: 220\r\n\r\n{\"Tty\": false, \"Container\": \"891fcd673641b13a8ef587f9a7bd190f48b91e4173f83378f94efaa7cec7ad5f\", \"Cmd\": [\"echo\", \"hello\"], \"AttachStdin\": false, \"User\": \"\", \"AttachStderr\": true, \"Privileged\": false, \"AttachStdout\": true}", 501, 0, NULL, 0) = 501
recvfrom(7, "HTTP/1.1 201 Created\r\nApi-Version: 1.29\r\nContent-Type: application/json\r\nDocker-Experimental: false\r\nOstype: linux\r\nServer: Docker/17.05.0-ce (linux)\r\nDate: Wed, 30 Aug 2017 12:26:41 GMT\r\nContent-Length: 74\r\n\r\n{\"Id\":\"ae108562938bdd0233a1d4b89b1099740a2aca4c470fb10419e315b07d989755\"}\n", 8192, 0, NULL, NULL) = 284
socket(PF_LOCAL, SOCK_STREAM, 0)        = 8
connect(8, {sa_family=AF_LOCAL, sun_path="/var/run/docker.sock"}, 22) = 0
sendto(8, "POST /v1.29/exec/ae108562938bdd0233a1d4b89b1099740a2aca4c470fb10419e315b07d989755/start HTTP/1.1\r\nHost: localhost\r\nConnection: Upgrade\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nUser-Agent: docker-sdk-python/2.5.1\r\nUpgrade: tcp\r\nContent-Type: application/json\r\nContent-Length: 31\r\n\r\n{\"Tty\": false, \"Detach\": false}", 317, 0, NULL, 0) = 317
recvfrom(8, "HTTP/1.1 101 UPGRADED\r\nContent-Type: application/vnd.docker.raw-stream\r\nConnection: Upgrade\r\nUpgrade: tcp\r\nApi-Version: 1.29\r\nDocker-Experimental: false\r\nOstype: linux\r\nServer: Docker/17.05.0-ce (linux)\r\n\r\n", 8192, 0, NULL, NULL) = 206
recvfrom(8, "\1\0\0\0\0\0\0\6", 8, 0, NULL, NULL) = 8
recvfrom(8, "hello\n", 6, 0, NULL, NULL) = 6
recvfrom(8, "", 8, 0, NULL, NULL)       = 0
socket(PF_LOCAL, SOCK_STREAM, 0)        = 8
connect(8, {sa_family=AF_LOCAL, sun_path="/var/run/docker.sock"}, 22) = 0
sendto(8, "DELETE /v1.29/containers/891fcd673641b13a8ef587f9a7bd190f48b91e4173f83378f94efaa7cec7ad5f?force=True&link=False&v=False HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nUser-Agent: docker-sdk-python/2.5.1\r\nContent-Length: 0\r\n\r\n", 274, 0, NULL, 0) = 274
recvfrom(8, "HTTP/1.1 204 No Content\r\nApi-Version: 1.29\r\nDocker-Experimental: false\r\nOstype: linux\r\nServer: Docker/17.05.0-ce (linux)\r\nDate: Wed, 30 Aug 2017 12:26:41 GMT\r\n\r\n", 8192, 0, NULL, NULL) = 161
+++ exited with 0 +++
```

The key thing seems to be interpretation of the http body in reply to /v1.26/exec/40cfec7ac80a6903c3aee28a3a5a7832f7c71c0717e3f607b1f30813cde5df7f/start :+1: docker-py code is fooled that there is nothing else in the reply because '\1\0\0\0\0\0\0\0' is interpreted as frame size by the following code:
```
def read_exactly(socket, n):
    """
    Reads exactly n bytes from socket
    Raises SocketError if there isn't enough data
    """
    data = six.binary_type()
    while len(data) < n:
        next_data = read(socket, n - len(data))
        if not next_data:
            raise SocketError("Unexpected EOF")
        data += next_data
    return data


def next_frame_size(socket):
    """
    Returns the size of the next frame of data waiting to be read from socket,
    according to the protocol defined here:

    https://docs.docker.com/engine/reference/api/docker_remote_api_v1.24/#/attach-to-a-container
    """
    try:
        data = read_exactly(socket, 8)
    except SocketError:
        return 0

    _, actual = struct.unpack('>BxxxL', data)
    return actual
```
My rewriten test case proves this: there is something more to read after '\1\0\0\0\0\0\0\0':

strace of rewritten test case on Centos 7.3 with Docker version 1.13.1, build b303bf6/1.13.1:

```
(docker-venv)[strzelczak@xxx ~]$ strace -f -e trace=network -s1024 python docker-py-1732-requests.py 
socket(PF_INET6, SOCK_STREAM, IPPROTO_IP) = 10
bind(10, {sa_family=AF_INET6, sin6_port=htons(0), inet_pton(AF_INET6, "::1", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, 28) = 0
Process 1164 attached
Process 1165 attached
[pid  1165] +++ exited with 0 +++
[pid  1164] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=1165, si_status=0, si_utime=0, si_stime=0} ---
[pid  1164] +++ exited with 0 +++
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=1164, si_status=0, si_utime=0, si_stime=0} ---
Process 1166 attached
Process 1167 attached
[pid  1167] +++ exited with 0 +++
[pid  1166] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=1167, si_status=0, si_utime=0, si_stime=0} ---
[pid  1166] +++ exited with 0 +++
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=1166, si_status=0, si_utime=0, si_stime=0} ---
Process 1199 attached
Process 1200 attached
[pid  1200] +++ exited with 0 +++
[pid  1199] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=1200, si_status=0, si_utime=0, si_stime=0} ---
[pid  1199] +++ exited with 0 +++
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=1199, si_status=0, si_utime=0, si_stime=0} ---
socket(PF_LOCAL, SOCK_STREAM, 0)        = 3
connect(3, {sa_family=AF_LOCAL, sun_path="/var/run/docker.sock"}, 22) = 0
sendto(3, "GET /version HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nUser-Agent: python-requests/2.18.4\r\n\r\n", 147, 0, NULL, 0) = 147
recvfrom(3, "HTTP/1.1 200 OK\r\nApi-Version: 1.26\r\nContent-Type: application/json\r\nDocker-Experimental: false\r\nServer: Docker/1.13.1 (linux)\r\nDate: Wed, 30 Aug 2017 12:33:18 GMT\r\nContent-Length: 263\r\n\r\n{\"Version\":\"1.13.1\",\"ApiVersion\":\"1.26\",\"MinAPIVersion\":\"1.12\",\"GitCommit\":\"b303bf6/1.13.1\",\"GoVersion\":\"go1.7.4\",\"Os\":\"linux\",\"Arch\":\"amd64\",\"KernelVersion\":\"3.10.0-514.26.2.el7.x86_64\",\"BuildTime\":\"2017-07-03T15:57:33.186109768+00:00\",\"PkgVersion\":\"<unknown>\"}\n", 8192, 0, NULL, NULL) = 450
socket(PF_LOCAL, SOCK_STREAM, 0)        = 3
connect(3, {sa_family=AF_LOCAL, sun_path="/var/run/docker.sock"}, 22) = 0
sendto(3, "POST /v1.26/images/create?tag=latest&fromImage=busybox HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nUser-Agent: python-requests/2.18.4\r\nContent-Length: 0\r\n\r\n", 208, 0, NULL, 0) = 208
recvfrom(3, "HTTP/1.1 200 OK\r\nApi-Version: 1.26\r\nContent-Type: application/json\r\nDocker-Experimental: false\r\nServer: Docker/1.13.1 (linux)\r\nDate: Wed, 30 Aug 2017 12:33:18 GMT\r\nTransfer-Encoding: chunked\r\n\r\n47\r\n{\"status\":\"Trying to pull repository docker.io/library/busybox ... \"}\r\n\r\n", 8192, 0, NULL, NULL) = 271
recvfrom(3, "43\r\n{\"status\":\"Pulling from docker.io/library/busybox\",\"id\":\"latest\"}\r\n\r\n", 8192, 0, NULL, NULL) = 73
recvfrom(3, "5e\r\n{\"status\":\"Digest: sha256:b82b5740006c1ab823596d2c07f081084ecdb32fd258072707b99f52a3cb8692\"}\r\n\r\n47\r\n{\"status\":\"Status: Image is up to date for docker.io/busybox:latest\"}\r\n\r\n", 8192, 0, NULL, NULL) = 177
recvfrom(3, "0\r\n\r\n", 8192, 0, NULL, NULL) = 5
socket(PF_LOCAL, SOCK_STREAM, 0)        = 3
connect(3, {sa_family=AF_LOCAL, sun_path="/var/run/docker.sock"}, 22) = 0
sendto(3, "POST /v1.26/containers/create HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nUser-Agent: python-requests/2.18.4\r\nContent-Length: 190\r\nContent-Type: application/json\r\n\r\n{\"Tty\": false, \"AttachStderr\": false, \"AttachStdin\": false, \"NetworkDisabled\": false, \"Image\": \"busybox:latest\", \"Cmd\": [\"cat\"], \"StdinOnce\": false, \"AttachStdout\": false, \"OpenStdin\": true}", 407, 0, NULL, 0) = 407
recvfrom(3, "HTTP/1.1 201 Created\r\nApi-Version: 1.26\r\nContent-Type: application/json\r\nDocker-Experimental: false\r\nServer: Docker/1.13.1 (linux)\r\nDate: Wed, 30 Aug 2017 12:33:20 GMT\r\nContent-Length: 90\r\n\r\n{\"Id\":\"087ee71e89ad322294ab8ccf910b0628dc6074491a5f286c73898252b6b6cb55\",\"Warnings\":null}\n", 8192, 0, NULL, NULL) = 281
socket(PF_LOCAL, SOCK_STREAM, 0)        = 3
connect(3, {sa_family=AF_LOCAL, sun_path="/var/run/docker.sock"}, 22) = 0
sendto(3, "POST /v1.26/containers/087ee71e89ad322294ab8ccf910b0628dc6074491a5f286c73898252b6b6cb55/start HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nUser-Agent: python-requests/2.18.4\r\nContent-Length: 0\r\n\r\n", 247, 0, NULL, 0) = 247
recvfrom(3, "HTTP/1.1 204 No Content\r\nApi-Version: 1.26\r\nDocker-Experimental: false\r\nServer: Docker/1.13.1 (linux)\r\nDate: Wed, 30 Aug 2017 12:33:20 GMT\r\n\r\n", 8192, 0, NULL, NULL) = 142
socket(PF_LOCAL, SOCK_STREAM, 0)        = 3
connect(3, {sa_family=AF_LOCAL, sun_path="/var/run/docker.sock"}, 22) = 0
sendto(3, "POST /v1.26/containers/087ee71e89ad322294ab8ccf910b0628dc6074491a5f286c73898252b6b6cb55/exec HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nUser-Agent: python-requests/2.18.4\r\nContent-Length: 220\r\nContent-Type: application/json\r\n\r\n{\"Tty\": false, \"AttachStderr\": true, \"Container\": \"087ee71e89ad322294ab8ccf910b0628dc6074491a5f286c73898252b6b6cb55\", \"User\": \"\", \"AttachStdin\": false, \"Cmd\": [\"echo\", \"hello\"], \"Privileged\": false, \"AttachStdout\": true}", 500, 0, NULL, 0) = 500
recvfrom(3, "HTTP/1.1 201 Created\r\nApi-Version: 1.26\r\nContent-Type: application/json\r\nDocker-Experimental: false\r\nServer: Docker/1.13.1 (linux)\r\nDate: Wed, 30 Aug 2017 12:33:20 GMT\r\nContent-Length: 74\r\n\r\n{\"Id\":\"d83f217ce1a3dc4cc63dd4e68d6b5c0f7e7c8fdd35e5f54d1bda4358181e0c05\"}\n", 8192, 0, NULL, NULL) = 265
socket(PF_LOCAL, SOCK_STREAM, 0)        = 3
connect(3, {sa_family=AF_LOCAL, sun_path="/var/run/docker.sock"}, 22) = 0
sendto(3, "POST /v1.26/exec/d83f217ce1a3dc4cc63dd4e68d6b5c0f7e7c8fdd35e5f54d1bda4358181e0c05/start HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nUser-Agent: python-requests/2.18.4\r\nContent-Length: 31\r\nContent-Type: application/json\r\n\r\n{\"Tty\": false, \"Detach\": false}", 305, 0, NULL, 0) = 305
recvfrom(3, "HTTP/1.1 200 OK\r\nContent-Type: application/vnd.docker.raw-stream\r\n", 8192, 0, NULL, NULL) = 66
recvfrom(3, "Api-Version: 1.26\r\nDocker-Experimental: false\r\nServer: Docker/1.13.1 (linux)\r\n\r\n", 8192, 0, NULL, NULL) = 80
recvfrom(3, "\1\0\0\0\0\0\0\0\2\0\0\0\0\0\0\0", 10240, 0, NULL, NULL) = 16
recvfrom(3, "\1\0\0\0\0\0\0\6hello\n", 10224, 0, NULL, NULL) = 14
recvfrom(3, "", 10210, 0, NULL, NULL)   = 0
recvfrom(3, "", 10240, 0, NULL, NULL)   = 0
socket(PF_LOCAL, SOCK_STREAM, 0)        = 3
connect(3, {sa_family=AF_LOCAL, sun_path="/var/run/docker.sock"}, 22) = 0
sendto(3, "GET /v1.26/exec/d83f217ce1a3dc4cc63dd4e68d6b5c0f7e7c8fdd35e5f54d1bda4358181e0c05/json HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nUser-Agent: python-requests/2.18.4\r\n\r\n", 220, 0, NULL, 0) = 220
recvfrom(3, "HTTP/1.1 200 OK\r\nApi-Version: 1.26\r\nContent-Type: application/json\r\nDocker-Experimental: false\r\nServer: Docker/1.13.1 (linux)\r\nDate: Wed, 30 Aug 2017 12:33:20 GMT\r\nContent-Length: 374\r\n\r\n{\"ID\":\"d83f217ce1a3dc4cc63dd4e68d6b5c0f7e7c8fdd35e5f54d1bda4358181e0c05\",\"Running\":false,\"ExitCode\":0,\"ProcessConfig\":{\"tty\":false,\"entrypoint\":\"echo\",\"arguments\":[\"hello\"],\"privileged\":false},\"OpenStdin\":false,\"OpenStderr\":true,\"OpenStdout\":true,\"CanRemove\":false,\"ContainerID\":\"087ee71e89ad322294ab8ccf910b0628dc6074491a5f286c73898252b6b6cb55\",\"DetachKeys\":\"\",\"Pid\":1334}\n", 8192, 0, NULL, NULL) = 561
{u'OpenStderr': True, u'OpenStdout': True, u'ContainerID': u'087ee71e89ad322294ab8ccf910b0628dc6074491a5f286c73898252b6b6cb55', u'DetachKeys': u'', u'Pid': 1334, u'CanRemove': False, u'Running': False, u'ProcessConfig': {u'tty': False, u'entrypoint': u'echo', u'arguments': [u'hello'], u'privileged': False}, u'ExitCode': 0, u'ID': u'd83f217ce1a3dc4cc63dd4e68d6b5c0f7e7c8fdd35e5f54d1bda4358181e0c05', u'OpenStdin': False}
socket(PF_LOCAL, SOCK_STREAM, 0)        = 3
connect(3, {sa_family=AF_LOCAL, sun_path="/var/run/docker.sock"}, 22) = 0
sendto(3, "DELETE /v1.26/containers/087ee71e89ad322294ab8ccf910b0628dc6074491a5f286c73898252b6b6cb55?force=True&link=False&v=False HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nUser-Agent: python-requests/2.18.4\r\nContent-Length: 0\r\n\r\n", 273, 0, NULL, 0) = 273
recvfrom(3, "HTTP/1.1 204 No Content\r\nApi-Version: 1.26\r\nDocker-Experimental: false\r\nServer: Docker/1.13.1 (linux)\r\nDate: Wed, 30 Aug 2017 12:33:20 GMT\r\n\r\n", 8192, 0, NULL, NULL) = 142
+++ exited with 0 +++
(docker-venv)[strzelczak@xxx ~]$ 
```

A bit related to #1697 . When tty=True is used in exec_start a different protocol is used and the problem does not occur.
Are you testing this on the latest version of docker-py (2.5.1)?
Yes, I am using latest 2.5.1. Sorry not to include this in a original bug report.
Thank you for confirming. I see now that we're making an incorrect interpretation of the protocol where we equate a 0-length chunk to an `EOF` signal, which isn't corroborated by the docs:https://docs.docker.com/engine/api/v1.30/#operation/ContainerAttach

Resolving patch:
diff --git a/docker/utils/socket.py b/docker/utils/socket.py
--- a/docker/utils/socket.py
+++ b/docker/utils/socket.py
@@ -59,7 +59,7 @@ def next_frame_size(socket):
     try:
         data = read_exactly(socket, 8)
     except SocketError:
-        return 0
+        return -1
 
     _, actual = struct.unpack('>BxxxL', data)
     return actual
@@ -71,7 +71,7 @@ def frames_iter(socket):
     """
     while True:
         n = next_frame_size(socket)
-        if n == 0:
+        if n < 0:
             break
         while n > 0:
             result = read(socket, n)


Test patch:
diff --git a/tests/unit/fake_api.py b/tests/unit/fake_api.py
--- a/tests/unit/fake_api.py
+++ b/tests/unit/fake_api.py
@@ -205,7 +205,9 @@ def get_fake_wait():
 
 def get_fake_logs():
     status_code = 200
-    response = (b'\x01\x00\x00\x00\x00\x00\x00\x11Flowering Nights\n'
+    response = (b'\x01\x00\x00\x00\x00\x00\x00\x00'
+                b'\x02\x00\x00\x00\x00\x00\x00\x00'
+                b'\x01\x00\x00\x00\x00\x00\x00\x11Flowering Nights\n'
                 b'\x01\x00\x00\x00\x00\x00\x00\x10(Sakuya Iyazoi)\n')
     return status_code, response