Convert DockerContainer to use binary PIPEs, to handle arbitrary data

This commit is contained in:
Joe Rickerby
2020-06-26 21:36:52 +01:00
parent 2edb33662b
commit 762dc807cd
+23 -20
View File
@@ -1,3 +1,4 @@
import codecs
import io import io
import json import json
import shlex import shlex
@@ -6,7 +7,7 @@ import sys
import uuid import uuid
from os import PathLike from os import PathLike
from pathlib import Path, PurePath from pathlib import Path, PurePath
from typing import IO, Dict, List, Optional, Sequence, TextIO, Union from typing import IO, Any, Dict, List, Optional, Sequence, TextIO, Union, cast
class DockerContainer: class DockerContainer:
@@ -23,21 +24,21 @@ class DockerContainer:
UTILITY_PYTHON = '/opt/python/cp38-cp38/bin/python' UTILITY_PYTHON = '/opt/python/cp38-cp38/bin/python'
process: subprocess.Popen process: subprocess.Popen
bash_stdin: IO[str] bash_stdin: IO[bytes]
bash_stdout: IO[str] bash_stdout: IO[bytes]
def __init__(self, docker_image: str, simulate_32_bit=False): def __init__(self, docker_image: str, simulate_32_bit=False):
self.docker_image = docker_image self.docker_image = docker_image
self.simulate_32_bit = simulate_32_bit self.simulate_32_bit = simulate_32_bit
def __enter__(self) -> 'DockerContainer': def __enter__(self) -> 'DockerContainer':
self.container_name = f'cibuildwheel-{uuid.uuid4()}' self.name = f'cibuildwheel-{uuid.uuid4()}'
shell_args = ['linux32', '/bin/bash'] if self.simulate_32_bit else ['/bin/bash'] shell_args = ['linux32', '/bin/bash'] if self.simulate_32_bit else ['/bin/bash']
subprocess.run( subprocess.run(
[ [
'docker', 'create', 'docker', 'create',
'--env', 'CIBUILDWHEEL', '--env', 'CIBUILDWHEEL',
'--name', self.container_name, '--name', self.name,
'-i', '-i',
'-v', '/:/host', # ignored on CircleCI '-v', '/:/host', # ignored on CircleCI
self.docker_image, self.docker_image,
@@ -49,13 +50,12 @@ class DockerContainer:
[ [
'docker', 'start', 'docker', 'start',
'--attach', '--interactive', '--attach', '--interactive',
self.container_name, self.name,
], ],
encoding='utf8',
stdin=subprocess.PIPE, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
bufsize=262144,
) )
assert self.process.stdin and self.process.stdout assert self.process.stdin and self.process.stdout
self.bash_stdin = self.process.stdin self.bash_stdin = self.process.stdin
self.bash_stdout = self.process.stdout self.bash_stdout = self.process.stdout
@@ -66,8 +66,8 @@ class DockerContainer:
self.process.terminate() self.process.terminate()
self.process.wait() self.process.wait()
subprocess.run(['docker', 'rm', '--force', '-v', self.container_name]) subprocess.run(['docker', 'rm', '--force', '-v', self.name])
self.container_name = None self.name = None
def copy_into(self, from_path: Path, to_path: PurePath) -> None: def copy_into(self, from_path: Path, to_path: PurePath) -> None:
# `docker cp` causes 'no space left on device' error when # `docker cp` causes 'no space left on device' error when
@@ -77,13 +77,13 @@ class DockerContainer:
if from_path.is_dir(): if from_path.is_dir():
self.call(['mkdir', '-p', to_path]) self.call(['mkdir', '-p', to_path])
subprocess.run( subprocess.run(
f'tar cf - . | docker exec -i {self.container_name} tar -xC {to_path} -f -', f'tar cf - . | docker exec -i {self.name} tar -xC {to_path} -f -',
shell=True, shell=True,
check=True, check=True,
cwd=from_path) cwd=from_path)
else: else:
subprocess.run( subprocess.run(
f'cat {from_path} | docker exec -i {self.container_name} sh -c "cat > {to_path}"', f'cat {from_path} | docker exec -i {self.name} sh -c "cat > {to_path}"',
shell=True, shell=True,
check=True) check=True)
@@ -92,7 +92,7 @@ class DockerContainer:
to_path.mkdir(parents=True, exist_ok=True) to_path.mkdir(parents=True, exist_ok=True)
subprocess.run( subprocess.run(
f'docker exec -i {self.container_name} tar -cC {from_path} -f - . | tar -xf -', f'docker exec -i {self.name} tar -cC {from_path} -f - . | tar -xf -',
shell=True, shell=True,
check=True, check=True,
cwd=to_path cwd=to_path
@@ -125,23 +125,23 @@ class DockerContainer:
# Finally, the remote shell is told to write a footer - this will show # Finally, the remote shell is told to write a footer - this will show
# up in the output so we know when to stop reading, and will include # up in the output so we know when to stop reading, and will include
# the returncode of `command`. # the returncode of `command`.
self.bash_stdin.write(f'''( self.bash_stdin.write(bytes(f'''(
{chdir} {chdir}
env {env_assignments} {command} env {env_assignments} {command}
printf "%04d%s\n" $? {end_of_message} printf "%04d%s\n" $? {end_of_message}
) )
''') ''', encoding='utf8', errors='surrogateescape'))
self.bash_stdin.flush() self.bash_stdin.flush()
if capture_output: if capture_output:
output_io: TextIO = io.StringIO() output_io: IO[bytes] = io.BytesIO()
else: else:
output_io = sys.stdout output_io = sys.stdout.buffer
while True: while True:
line = self.bash_stdout.readline() line = self.bash_stdout.readline()
if line.endswith(end_of_message+'\n'): if line.endswith(b'%s\n' % (bytes(end_of_message, encoding='utf8'))):
footer_offset = ( footer_offset = (
len(line) len(line)
- 1 # newline character - 1 # newline character
@@ -156,12 +156,15 @@ class DockerContainer:
else: else:
output_io.write(line) output_io.write(line)
output = output_io.getvalue() if isinstance(output_io, io.StringIO) else None if isinstance(output_io, io.BytesIO):
output = str(output_io.getvalue(), encoding='utf8', errors='surrogateescape')
else:
output = ''
if returncode != 0: if returncode != 0:
raise subprocess.CalledProcessError(returncode, args, output) raise subprocess.CalledProcessError(returncode, args, output)
return output if output else '' return output
def get_environment(self) -> Dict[str, str]: def get_environment(self) -> Dict[str, str]:
return json.loads(self.call([ return json.loads(self.call([