blob: f1f37d49a9dac61304a38aeef2366dffb9bf17ba [file] [log] [blame]
# Copyright 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 atexit
import datetime
import os
import logging
import platform
import random
import subprocess
import sys
import tempfile
import time
from battor import battor_error
from py_utils import cloud_storage
import dependency_manager
from devil.utils import battor_device_mapping
from devil.utils import find_usb_devices
import serial
from serial.tools import list_ports
def IsBattOrConnected(test_platform, android_device=None,
android_device_map=None, android_device_file=None):
"""Returns True if BattOr is detected."""
if test_platform == 'android':
if not android_device:
raise ValueError('Must pass android device serial when determining '
'support on android platform')
if not android_device_map:
device_tree = find_usb_devices.GetBusNumberToDeviceTreeMap()
if len(battor_device_mapping.GetBattorList(device_tree)) == 1:
return True
if android_device_file:
android_device_map = battor_device_mapping.ReadSerialMapFile(
android_device_file)
else:
try:
android_device_map = battor_device_mapping.GenerateSerialMap()
except battor_error.BattorError:
return False
# If neither if statement above is triggered, it means that an
# android_device_map was passed in and will be used.
return str(android_device) in android_device_map
elif test_platform == 'win':
for (_1, desc, _2) in serial.tools.list_ports.comports():
if 'USB Serial Port' in desc:
return True
logging.info('No usb serial port discovered. Available ones are: %s' %
list(serial.tools.list_ports.comports()))
return False
elif test_platform == 'mac':
for (_1, desc, _2) in serial.tools.list_ports.comports():
if 'BattOr' in desc:
return True
return False
elif test_platform == 'linux':
device_tree = find_usb_devices.GetBusNumberToDeviceTreeMap(fast=True)
return bool(battor_device_mapping.GetBattorList(device_tree))
return False
class BattorWrapper(object):
"""A class for communicating with a BattOr in python."""
_START_TRACING_CMD = 'StartTracing'
_STOP_TRACING_CMD = 'StopTracing'
_SUPPORTS_CLOCKSYNC_CMD = 'SupportsExplicitClockSync'
_RECORD_CLOCKSYNC_CMD = 'RecordClockSyncMarker'
_SUPPORTED_PLATFORMS = ['android', 'chromeos', 'linux', 'mac', 'win']
def __init__(self, target_platform, android_device=None, battor_path=None,
battor_map_file=None, battor_map=None, serial_log_bucket=None):
"""Constructor.
Args:
target_platform: Platform BattOr is attached to.
android_device: Serial number of Android device.
battor_path: Path to BattOr device.
battor_map_file: File giving map of [device serial: BattOr path]
battor_map: Map of [device serial: BattOr path]
serial_log_bucket: The cloud storage bucket to which BattOr agent serial
logs are uploaded on failure.
Attributes:
_battor_path: Path to BattOr. Typically similar to /tty/USB0.
_battor_agent_binary: Path to the BattOr agent binary used to communicate
with the BattOr.
_tracing: A bool saying if tracing has been started.
_battor_shell: A subprocess running the bator_agent_binary
_trace_results_path: Path to BattOr trace results file.
_serial_log_bucket: Cloud storage bucket to which BattOr agent serial logs
are uploaded on failure.
_serial_log_file: Temp file for the BattOr agent serial log.
"""
self._battor_path = self._GetBattorPath(target_platform, android_device,
battor_path, battor_map_file, battor_map)
config = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
'battor_binary_dependencies.json')
dm = dependency_manager.DependencyManager(
[dependency_manager.BaseConfig(config)])
self._battor_agent_binary = dm.FetchPath(
'battor_agent_binary', '%s_%s' % (sys.platform, platform.machine()))
self._serial_log_bucket = serial_log_bucket
self._tracing = False
self._battor_shell = None
self._trace_results_path = None
self._start_tracing_time = None
self._stop_tracing_time = None
self._trace_results = None
self._serial_log_file = None
atexit.register(self.KillBattOrShell)
def KillBattOrShell(self):
if self._battor_shell:
logging.critical('BattOr shell was not properly closed. Killing now.')
self._battor_shell.kill()
def GetShellReturnCode(self):
"""Gets the return code of the BattOr agent shell."""
# TODO(rnephew): Get rid of logging after crbug.com/645106 is fixed.
logging.critical('Finding return code for BattOr shell.')
rc = self._battor_shell.poll()
logging.critical('Found return code: %s' % rc)
return rc
def StartShell(self):
"""Start BattOr binary shell."""
assert not self._battor_shell, 'Attempting to start running BattOr shell.'
battor_cmd = [self._battor_agent_binary]
if self._serial_log_bucket:
self._serial_log_file = tempfile.NamedTemporaryFile()
battor_cmd.append('--battor-serial-log=%s' % self._serial_log_file.name)
if self._battor_path:
battor_cmd.append('--battor-path=%s' % self._battor_path)
self._battor_shell = self._StartShellImpl(battor_cmd)
assert self.GetShellReturnCode() is None, 'Shell failed to start.'
def StartTracing(self):
"""Start tracing on the BattOr."""
assert self._battor_shell, 'Must start shell before tracing'
assert not self._tracing, 'Tracing already started.'
self._SendBattorCommand(self._START_TRACING_CMD)
self._tracing = True
self._start_tracing_time = int(time.time())
def StopTracing(self):
"""Stop tracing on the BattOr."""
assert self._tracing, 'Must run StartTracing before StopTracing'
# Create temp file to reserve location for saving results.
temp_file = tempfile.NamedTemporaryFile(delete=False)
self._trace_results_path = temp_file.name
temp_file.close()
self._SendBattorCommand(
'%s %s' % (self._STOP_TRACING_CMD, self._trace_results_path),
check_return=False)
self._tracing = False
self._stop_tracing_time = int(time.time())
def CollectTraceData(self, timeout=None):
"""Collect trace data from battor.
Args:
timeout: timeout for waiting on the BattOr process to terminate in
seconds.
Returns: Trace data in form of a list.
"""
# The BattOr shell terminates after returning the results.
if timeout is None:
timeout = self._stop_tracing_time - self._start_tracing_time
if self.GetShellReturnCode() == 1:
self._UploadSerialLogToCloudStorage()
with open(self._trace_results_path) as results:
self._trace_results = results.read()
self._battor_shell = None
self._serial_log_file = None
return self._trace_results
def SupportsExplicitClockSync(self):
"""Returns if BattOr supports Clock Sync events."""
return bool(int(self._SendBattorCommand(self._SUPPORTS_CLOCKSYNC_CMD,
check_return=False)))
def RecordClockSyncMarker(self, sync_id):
"""Record clock sync event on BattOr."""
if not isinstance(sync_id, basestring):
raise TypeError('sync_id must be a string.')
self._SendBattorCommand('%s %s' % (self._RECORD_CLOCKSYNC_CMD, sync_id))
def _GetBattorPath(self, target_platform, android_device=None,
battor_path=None, battor_map_file=None, battor_map=None):
"""Determines most likely path to the correct BattOr."""
if target_platform not in self._SUPPORTED_PLATFORMS:
raise battor_error.BattorError(
'%s is an unsupported platform.' % target_platform)
if target_platform in ['win']:
# Right now, the BattOr agent binary isn't able to automatically detect
# the BattOr port on Windows. To get around this, we know that the BattOr
# shows up with a name of 'USB Serial Port', so use the COM port that
# corresponds to a device with that name.
for (port, desc, _) in serial.tools.list_ports.comports():
if 'USB Serial Port' in desc:
return port
raise battor_error.BattorError(
'Could not find BattOr attached to machine.')
if target_platform in ['mac']:
for (port, desc, _) in serial.tools.list_ports.comports():
if 'BattOr' in desc:
return port
if target_platform in ['android', 'linux']:
device_tree = find_usb_devices.GetBusNumberToDeviceTreeMap(fast=True)
if battor_path:
if not isinstance(battor_path, basestring):
raise battor_error.BattorError(
'An invalid BattOr path was specified.')
return battor_path
if target_platform == 'android':
if not android_device:
raise battor_error.BattorError(
'Must specify device for Android platform.')
if not battor_map_file and not battor_map:
# No map was passed, so must create one.
battor_map = battor_device_mapping.GenerateSerialMap()
return battor_device_mapping.GetBattorPathFromPhoneSerial(
str(android_device), serial_map_file=battor_map_file,
serial_map=battor_map)
# Not Android and no explicitly passed BattOr.
battors = battor_device_mapping.GetBattorList(device_tree)
if len(battors) != 1:
raise battor_error.BattorError(
'For non-Android platforms, exactly one BattOr must be '
'attached unless address is explicitly given.')
return '/dev/%s' % battors.pop()
raise NotImplementedError(
'BattOr Wrapper not implemented for given platform')
def _SendBattorCommandImpl(self, cmd):
"""Sends command to the BattOr."""
self._battor_shell.stdin.write('%s\n' % cmd)
self._battor_shell.stdin.flush()
return self._battor_shell.stdout.readline()
def _SendBattorCommand(self, cmd, check_return=True):
status = self._SendBattorCommandImpl(cmd)
if check_return and not 'Done.' in status:
self._UploadSerialLogToCloudStorage()
self._serial_log_file = None
raise battor_error.BattorError(
'BattOr did not complete command \'%s\' correctly.\n'
'Outputted: %s' % (cmd, status))
return status
def _StartShellImpl(self, battor_cmd):
return subprocess.Popen(
battor_cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, shell=False)
def _UploadSerialLogToCloudStorage(self):
"""Uploads the BattOr serial log to cloud storage."""
if not self._serial_log_file or not cloud_storage.IsNetworkIOEnabled():
return
remote_path = ('battor-serial-log-%s-%d.txt' % (
datetime.datetime.now().strftime('%Y-%m-%d_%H-%M.txt'),
random.randint(1, 100000)))
try:
cloud_url = cloud_storage.Insert(
self._serial_log_bucket, remote_path, self._serial_log_file.name)
sys.stderr.write('View BattOr serial log at %s\n' % cloud_url)
except cloud_storage.PermissionError as e:
logging.error('Cannot upload BattOr serial log file to cloud storage due '
'to permission error: %s' % e.message)