blob: ba6dc1b7a95208c59eafe9e98bac759a9f2e6040 [file] [log] [blame]
# Copyright (c) 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import glob
import logging
import os
import pipes
import socket
import time
from infra.services.swarm_docker import containers
_DOCKER_CGROUP = '/sys/fs/cgroup/devices/docker'
class AndroidContainerDescriptor(containers.ContainerDescriptorBase):
def __init__(self, device):
super(AndroidContainerDescriptor, self).__init__()
self._device = device
@property
def name(self):
return 'android_%s' % self._device.serial
@property
def shutdown_file(self):
return '/b/%s.shutdown.stamp' % self._device.serial
@property
def lock_file(self):
return '/var/lock/android_docker.%s.lock' % self._device.serial
@property
def hostname(self):
this_host = socket.gethostname().split('.')[0]
if self._device.physical_port is not None:
return '%s--device%d' % (this_host, self._device.physical_port)
else:
return '%s--%s' % (this_host, self._device.serial)
@property
def device(self):
return self._device
def log_started(self):
logging.debug('Launched new container (%s) for device %s.',
self.name, self.device)
def should_create_container(self):
if self._device.physical_port is None:
logging.warning(
'Unable to assign physical port num to %s. No container will be '
'created.', self._device.serial)
return False
return True
class AndroidDockerClient(containers.DockerClient):
def __init__(self):
super(AndroidDockerClient, self).__init__()
self.cache_size = None
# Needed for access to device watchdog.
self.volumes.setdefault('/opt/infra-android', {
'bind': '/opt/infra-android',
'mode': 'ro'
})
# Needed by wifi_manager to connect to wifi.
self.volumes.setdefault('/creds/passwords', {
'bind': '/creds/passwords',
'mode': 'ro'
})
def _get_env(self, swarming_url):
env = super(AndroidDockerClient, self)._get_env(swarming_url)
env['ADB_LIBUSB'] = '0'
if self.cache_size:
env['ISOLATED_CACHE_SIZE'] = self.cache_size
# Point ADB to all local adb keys under ~/.android/
keys = glob.glob(os.path.expanduser('~/.android/*key'))
if keys:
env['ADB_VENDOR_KEYS'] = ':'.join(keys)
else:
logging.warning(
'No local adb keys found. Device auth will most likely fail.')
return env
@staticmethod
def _make_dev_file_cmd(path, major, minor):
cmd = ('mknod %(path)s c %(major)d %(minor)d && '
'chgrp chrome-bot %(path)s && '
'chmod 664 %(path)s') % {
'major': major,
'minor': minor,
'path': path,
}
return cmd
def create_container(self, container_desc, image_name, swarming_url, labels,
additional_env=None):
assert isinstance(container_desc, AndroidContainerDescriptor)
# Launch containers with a cpu_share value of 1/2 of the default of 1024.
# This will theoretically decrease each container's weight when CPU cycles
# are constrained.
super(AndroidDockerClient, self).create_container(
container_desc, image_name, swarming_url, labels, cpu_shares=512)
self.add_device(container_desc)
def add_device(self, container_desc, sleep_time=1.0):
assert isinstance(container_desc, AndroidContainerDescriptor)
container = self.get_container(container_desc)
device = container_desc.device
if container is None or container.state != 'running':
logging.error('Unable to add device %s: no running container.', device)
return
# Remove the old dev file from the container and wait one second to
# help ensure any wait-for-device threads inside notice its absence.
container.exec_run('rm -rf /dev/bus')
time.sleep(sleep_time)
# Pause the container while modifications to its cgroup are made. This
# isn't strictly necessary, but it helps avoid race-conditions.
container.pause()
try:
# Give the container permission to access the device.
container_id = container.attrs['Id']
path_to_cgroup = os.path.join(
_DOCKER_CGROUP, container_id, 'devices.allow')
if not os.path.exists(path_to_cgroup):
logging.error(
'cgroup file %s does not exist for device %s.',
device, path_to_cgroup)
return
try:
cgroup_fd = os.open(path_to_cgroup, os.O_WRONLY)
except OSError:
logging.exception(
'Unable to open cgroup file %s for device %s.',
path_to_cgroup, device)
return
try:
os.write(cgroup_fd, b'c %d:%d rwm' % (device.major, device.minor))
except OSError:
logging.exception(
'Unable to write to cgroup %s of %s\'s container.',
path_to_cgroup, device)
return
finally:
os.close(cgroup_fd)
# Sleep one more second to ensure the container's cgroup picks up the
# changes that were just made.
time.sleep(sleep_time)
finally:
container.unpause()
# In-line these mutliple commands to help avoid a race condition in adb
# that gets in a stuck state when polling for devices half-way through.
device_mknod_cmd = AndroidDockerClient._make_dev_file_cmd(
device.dev_file_path, device.major, device.minor)
add_device_cmd = ('mkdir -p /dev/bus/usb/%(bus)03d && '
'%(make_dev_file_cmd)s') % {
'bus': device.bus,
'make_dev_file_cmd': device_mknod_cmd,
}
container.exec_run('/bin/bash -c %s' % pipes.quote(add_device_cmd))
logging.debug('Successfully gave container %s access to device %s. '
'(major,minor): (%d,%d) at %s.', container.name, device,
device.major, device.minor, device.dev_file_path)