diff options
author | ilevy@chromium.org <ilevy@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-06-21 01:57:32 +0000 |
---|---|---|
committer | ilevy@chromium.org <ilevy@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-06-21 01:57:32 +0000 |
commit | 6cad138f674ab8c0d8a6b203e42788e747119452 (patch) | |
tree | 095308ecaabebb32a40d0e0bb9343fa8af013f55 /build | |
parent | 30974f245cc049f24895fe444aed20a0ab061711 (diff) | |
download | chromium_src-6cad138f674ab8c0d8a6b203e42788e747119452.zip chromium_src-6cad138f674ab8c0d8a6b203e42788e747119452.tar.gz chromium_src-6cad138f674ab8c0d8a6b203e42788e747119452.tar.bz2 |
Revert "Revert 143261 - Upstreaming android tools"
- fixing permissions and relanding. CQ does not run
check_perms apparently.
- original review:
https://chromiumcodereview.appspot.com/10578032/
- failing bot log:
http://build.chromium.org/p/chromium/builders/Linux/builds/26592/steps/check_perms/logs/stdio
This reverts commit 1ad94575470ccb61f9a64915638aafac3269a28f.
BUG=
TEST=
Review URL: https://chromiumcodereview.appspot.com/10598004
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@143329 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'build')
-rwxr-xr-x | build/android/adb_logcat_monitor.py | 155 | ||||
-rwxr-xr-x | build/android/adb_logcat_printer.py | 202 | ||||
-rwxr-xr-x[-rw-r--r--] | build/android/cmd_helper.py | 18 | ||||
-rwxr-xr-x | build/android/device_status_check.py | 120 |
4 files changed, 492 insertions, 3 deletions
diff --git a/build/android/adb_logcat_monitor.py b/build/android/adb_logcat_monitor.py new file mode 100755 index 0000000..7c5f369 --- /dev/null +++ b/build/android/adb_logcat_monitor.py @@ -0,0 +1,155 @@ +#!/usr/bin/python +# +# Copyright (c) 2012 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. + +"""Saves logcats from all connected devices. + +Usage: adb_logcat_monitor.py <base_dir> [<adb_binary_path>] + +This script will repeatedly poll adb for new devices and save logcats +inside the <base_dir> directory, which it attempts to create. The +script will run until killed by an external signal. To test, run the +script in a shell and <Ctrl>-C it after a while. It should be +resilient across phone disconnects and reconnects and start the logcat +early enough to not miss anything. +""" + +import logging +import os +import re +import shutil +import signal +import subprocess +import sys +import time + +# Map from device_id -> (process, logcat_num) +devices = {} + + +class TimeoutException(Exception): + """Exception used to signal a timeout.""" + pass + + +class SigtermError(Exception): + """Exception used to catch a sigterm.""" + pass + + +def StartLogcatIfNecessary(device_id, adb_cmd, base_dir): + """Spawns a adb logcat process if one is not currently running.""" + process, logcat_num = devices[device_id] + if process: + if process.poll() is None: + # Logcat process is still happily running + return + else: + logging.info('Logcat for device %s has died', device_id) + error_filter = re.compile('- waiting for device -') + for line in process.stderr: + if not error_filter.match(line): + logging.error(device_id + ': ' + line) + + logging.info('Starting logcat %d for device %s', logcat_num, + device_id) + logcat_filename = 'logcat_%s_%03d' % (device_id, logcat_num) + logcat_file = open(os.path.join(base_dir, logcat_filename), 'w') + process = subprocess.Popen([adb_cmd, '-s', device_id, + 'logcat', '-v', 'threadtime'], + stdout=logcat_file, + stderr=subprocess.PIPE) + devices[device_id] = (process, logcat_num + 1) + + +def GetAttachedDevices(adb_cmd): + """Gets the device list from adb. + + We use an alarm in this function to avoid deadlocking from an external + dependency. + + Args: + adb_cmd: binary to run adb + + Returns: + list of devices or an empty list on timeout + """ + signal.alarm(2) + try: + out, err = subprocess.Popen([adb_cmd, 'devices'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE).communicate() + if err: + logging.warning('adb device error %s', err.strip()) + return re.findall('^(\w+)\tdevice$', out, re.MULTILINE) + except TimeoutException: + logging.warning('"adb devices" command timed out') + return [] + except (IOError, OSError): + logging.exception('Exception from "adb devices"') + return [] + finally: + signal.alarm(0) + + +def main(base_dir, adb_cmd='adb'): + """Monitor adb forever. Expects a SIGINT (Ctrl-C) to kill.""" + # We create the directory to ensure 'run once' semantics + if os.path.exists(base_dir): + print 'adb_logcat_monitor: %s already exists? Cleaning' % base_dir + shutil.rmtree(base_dir, ignore_errors=True) + + os.makedirs(base_dir) + logging.basicConfig(filename=os.path.join(base_dir, 'eventlog'), + level=logging.INFO, + format='%(asctime)-2s %(levelname)-8s %(message)s') + + # Set up the alarm for calling 'adb devices'. This is to ensure + # our script doesn't get stuck waiting for a process response + def TimeoutHandler(_, unused_frame): + raise TimeoutException() + signal.signal(signal.SIGALRM, TimeoutHandler) + + # Handle SIGTERMs to ensure clean shutdown + def SigtermHandler(_, unused_frame): + raise SigtermError() + signal.signal(signal.SIGTERM, SigtermHandler) + + logging.info('Started with pid %d', os.getpid()) + pid_file_path = os.path.join(base_dir, 'LOGCAT_MONITOR_PID') + + try: + with open(pid_file_path, 'w') as f: + f.write(str(os.getpid())) + while True: + for device_id in GetAttachedDevices(adb_cmd): + if not device_id in devices: + devices[device_id] = (None, 0) + + for device in devices: + # This will spawn logcat watchers for any device ever detected + StartLogcatIfNecessary(device, adb_cmd, base_dir) + + time.sleep(5) + except SigtermError: + logging.info('Received SIGTERM, shutting down') + except: + logging.exception('Unexpected exception in main.') + finally: + for process, _ in devices.itervalues(): + if process: + try: + process.terminate() + except OSError: + pass + os.remove(pid_file_path) + + +if __name__ == '__main__': + if 2 <= len(sys.argv) <= 3: + print 'adb_logcat_monitor: Initializing' + sys.exit(main(*sys.argv[1:3])) + + print 'Usage: %s <base_dir> [<adb_binary_path>]' % sys.argv[0] diff --git a/build/android/adb_logcat_printer.py b/build/android/adb_logcat_printer.py new file mode 100755 index 0000000..6ea4797 --- /dev/null +++ b/build/android/adb_logcat_printer.py @@ -0,0 +1,202 @@ +#!/usr/bin/python +# +# Copyright (c) 2012 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. + +"""Shutdown adb_logcat_monitor and print accumulated logs. + +To test, call './adb_logcat_printer.py <base_dir>' where +<base_dir> contains 'adb logcat -v threadtime' files named as +logcat_<deviceID>_<sequenceNum> + +The script will print the files to out, and will combine multiple +logcats from a single device if there is overlap. + +Additionally, if a <base_dir>/LOGCAT_MONITOR_PID exists, the script +will attempt to terminate the contained PID by sending a SIGINT and +monitoring for the deletion of the aforementioned file. +""" + +import cStringIO +import logging +import os +import re +import signal +import sys +import time + + +# Set this to debug for more verbose output +LOG_LEVEL = logging.INFO + + +def CombineLogFiles(list_of_lists, logger): + """Splices together multiple logcats from the same device. + + Args: + list_of_lists: list of pairs (filename, list of timestamped lines) + logger: handler to log events + + Returns: + list of lines with duplicates removed + """ + cur_device_log = [''] + for cur_file, cur_file_lines in list_of_lists: + # Ignore files with just the logcat header + if len(cur_file_lines) < 2: + continue + common_index = 0 + # Skip this step if list just has empty string + if len(cur_device_log) > 1: + try: + line = cur_device_log[-1] + # Used to make sure we only splice on a timestamped line + if re.match('^\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3} ', line): + common_index = cur_file_lines.index(line) + else: + logger.warning('splice error - no timestamp in "%s"?', line.strip()) + except ValueError: + # The last line was valid but wasn't found in the next file + cur_device_log += ['***** POSSIBLE INCOMPLETE LOGCAT *****'] + logger.info('Unable to splice %s. Incomplete logcat?', cur_file) + + cur_device_log += ['*'*30 + ' %s' % cur_file] + cur_device_log.extend(cur_file_lines[common_index:]) + + return cur_device_log + + +def FindLogFiles(base_dir): + """Search a directory for logcat files. + + Args: + base_dir: directory to search + + Returns: + Mapping of device_id to a sorted list of file paths for a given device + """ + logcat_filter = re.compile('^logcat_(\w+)_(\d+)$') + # list of tuples (<device_id>, <seq num>, <full file path>) + filtered_list = [] + for cur_file in os.listdir(base_dir): + matcher = logcat_filter.match(cur_file) + if matcher: + filtered_list += [(matcher.group(1), int(matcher.group(2)), + os.path.join(base_dir, cur_file))] + filtered_list.sort() + file_map = {} + for device_id, _, cur_file in filtered_list: + if not device_id in file_map: + file_map[device_id] = [] + + file_map[device_id] += [cur_file] + return file_map + + +def GetDeviceLogs(log_filenames, logger): + """Read log files, combine and format. + + Args: + log_filenames: mapping of device_id to sorted list of file paths + logger: logger handle for logging events + + Returns: + list of formatted device logs, one for each device. + """ + device_logs = [] + + for device, device_files in log_filenames.iteritems(): + logger.debug('%s: %s', device, str(device_files)) + device_file_lines = [] + for cur_file in device_files: + with open(cur_file) as f: + device_file_lines += [(cur_file, f.read().splitlines())] + combined_lines = CombineLogFiles(device_file_lines, logger) + # Prepend each line with a short unique ID so it's easy to see + # when the device changes. We don't use the start of the device + # ID because it can be the same among devices. Example lines: + # AB324: foo + # AB324: blah + device_logs += [('\n' + device[-5:] + ': ').join(combined_lines)] + return device_logs + + +def ShutdownLogcatMonitor(base_dir, logger): + """Attempts to shutdown adb_logcat_monitor and blocks while waiting.""" + try: + monitor_pid_path = os.path.join(base_dir, 'LOGCAT_MONITOR_PID') + with open(monitor_pid_path) as f: + monitor_pid = int(f.readline()) + + logger.info('Sending SIGTERM to %d', monitor_pid) + os.kill(monitor_pid, signal.SIGTERM) + i = 0 + while True: + time.sleep(.2) + if not os.path.exists(monitor_pid_path): + return + if not os.path.exists('/proc/%d' % monitor_pid): + logger.warning('Monitor (pid %d) terminated uncleanly?', monitor_pid) + return + logger.info('Waiting for logcat process to terminate.') + i += 1 + if i >= 10: + logger.warning('Monitor pid did not terminate. Continuing anyway.') + return + + except (ValueError, IOError, OSError): + logger.exception('Error signaling logcat monitor - continuing') + + +def main(base_dir, output_file): + log_stringio = cStringIO.StringIO() + logger = logging.getLogger('LogcatPrinter') + logger.setLevel(LOG_LEVEL) + sh = logging.StreamHandler(log_stringio) + sh.setFormatter(logging.Formatter('%(asctime)-2s %(levelname)-8s' + ' %(message)s')) + logger.addHandler(sh) + + try: + # Wait at least 5 seconds after base_dir is created before printing. + # + # The idea is that 'adb logcat > file' output consists of 2 phases: + # 1 Dump all the saved logs to the file + # 2 Stream log messages as they are generated + # + # We want to give enough time for phase 1 to complete. There's no + # good method to tell how long to wait, but it usually only takes a + # second. On most bots, this code path won't occur at all, since + # adb_logcat_monitor.py command will have spawned more than 5 seconds + # prior to called this shell script. + try: + sleep_time = 5 - (time.time() - os.path.getctime(base_dir)) + except OSError: + sleep_time = 5 + if sleep_time > 0: + logger.warning('Monitor just started? Sleeping %.1fs', sleep_time) + time.sleep(sleep_time) + + assert os.path.exists(base_dir), '%s does not exist' % base_dir + ShutdownLogcatMonitor(base_dir, logger) + separator = '\n' + '*' * 80 + '\n\n' + for log in GetDeviceLogs(FindLogFiles(base_dir), logger): + output_file.write(log) + output_file.write(separator) + with open(os.path.join(base_dir, 'eventlog')) as f: + output_file.write('\nLogcat Monitor Event Log\n') + output_file.write(f.read()) + except: + logger.exception('Unexpected exception') + + logger.info('Done.') + sh.flush() + output_file.write('\nLogcat Printer Event Log\n') + output_file.write(log_stringio.getvalue()) + +if __name__ == '__main__': + if len(sys.argv) == 1: + print 'Usage: %s <base_dir>' % sys.argv[0] + sys.exit(1) + sys.exit(main(sys.argv[1], sys.stdout)) diff --git a/build/android/cmd_helper.py b/build/android/cmd_helper.py index 901cbe9a..9d688f7 100644..100755 --- a/build/android/cmd_helper.py +++ b/build/android/cmd_helper.py @@ -1,7 +1,11 @@ -# Copyright (c) 2011 The Chromium Authors. All rights reserved. +#!/usr/bin/python +# +# Copyright (c) 2012 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. +"""A wrapper for subprocess to make calling shell commands easier.""" + import logging import subprocess @@ -15,13 +19,16 @@ def RunCmd(args, cwd=None): the string or the first item in the args sequence. cwd: If not None, the subprocess's current directory will be changed to |cwd| before it's executed. + + Returns: + Return code from the command execution. """ logging.info(str(args) + ' ' + (cwd or '')) p = subprocess.Popen(args=args, cwd=cwd) return p.wait() -def GetCmdOutput(args, cwd=None): +def GetCmdOutput(args, cwd=None, shell=False): """Open a subprocess to execute a program and returns its output. Args: @@ -29,10 +36,15 @@ def GetCmdOutput(args, cwd=None): the string or the first item in the args sequence. cwd: If not None, the subprocess's current directory will be changed to |cwd| before it's executed. + shell: Whether to execute args as a shell command. + + Returns: + Captures and returns the command's stdout. + Prints the command's stderr to logger (which defaults to stdout). """ logging.info(str(args) + ' ' + (cwd or '')) p = subprocess.Popen(args=args, cwd=cwd, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + stderr=subprocess.PIPE, shell=shell) stdout, stderr = p.communicate() if stderr: logging.critical(stderr) diff --git a/build/android/device_status_check.py b/build/android/device_status_check.py new file mode 100755 index 0000000..5b45628 --- /dev/null +++ b/build/android/device_status_check.py @@ -0,0 +1,120 @@ +#!/usr/bin/python +# +# Copyright (c) 2012 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. + +"""A class to keep track of devices across builds and report state.""" + +import optparse +import os +import sys + +from android_commands import GetAttachedDevices +from cmd_helper import GetCmdOutput + + +def DeviceInfo(serial): + """Gathers info on a device via various adb calls. + + Args: + serial: The serial of the attached device to construct info about. + + Returns: + Tuple of device type, build id and report as a string. + """ + + def AdbShellCmd(cmd): + return GetCmdOutput('adb -s %s shell %s' % (serial, cmd), + shell=True).strip() + + device_type = AdbShellCmd('getprop ro.build.product') + device_build = AdbShellCmd('getprop ro.build.id') + + report = ['Device %s (%s)' % (serial, device_type), + ' Build: %s (%s)' % (device_build, + AdbShellCmd('getprop ro.build.fingerprint')), + ' Battery: %s%%' % AdbShellCmd('dumpsys battery | grep level ' + "| awk '{print $2}'"), + ' Battery temp: %s' % AdbShellCmd('dumpsys battery' + '| grep temp ' + "| awk '{print $2}'"), + ' IMEI slice: %s' % AdbShellCmd('dumpsys iphonesubinfo ' + '| grep Device' + "| awk '{print $4}'")[-6:], + ' Wifi IP: %s' % AdbShellCmd('getprop dhcp.wlan0.ipaddress'), + ''] + + return device_type, device_build, '\n'.join(report) + + +def CheckForMissingDevices(options, adb_online_devs): + """Uses file of previous online devices to detect broken phones. + + Args: + options: out_dir parameter of options argument is used as the base + directory to load and update the cache file. + adb_online_devs: A list of serial numbers of the currently visible + and online attached devices. + """ + + last_devices_path = os.path.abspath(os.path.join(options.out_dir, + '.last_devices')) + last_devices = [] + try: + with open(last_devices_path) as f: + last_devices = f.read().splitlines() + except IOError: + # Ignore error, file might not exist + pass + + missing_devs = list(set(last_devices) - set(adb_online_devs)) + if missing_devs: + print '@@@STEP_WARNINGS@@@' + print '@@@STEP_SUMMARY_TEXT@%s not detected.@@@' % missing_devs + print 'Current online devices: %s' % adb_online_devs + print '%s are no longer visible. Were they removed?\n' % missing_devs + print 'SHERIFF: See go/chrome_device_monitor' + print 'Cache file: %s\n\n' % last_devices_path + print 'adb devices' + print GetCmdOutput(['adb', 'devices']) + else: + new_devs = set(adb_online_devs) - set(last_devices) + if new_devs: + print '@@@STEP_WARNINGS@@@' + print '@@@STEP_SUMMARY_TEXT@New devices detected :-)@@@' + print ('New devices detected %s. And now back to your ' + 'regularly scheduled program.' % list(new_devs)) + + # Write devices currently visible plus devices previously seen. + with open(last_devices_path, 'w') as f: + f.write('\n'.join(set(adb_online_devs + last_devices))) + + +def main(): + parser = optparse.OptionParser() + parser.add_option('', '--out-dir', + help='Directory where the device path is stored', + default=os.path.join(os.path.dirname(__file__), '..', + '..', 'out')) + + options, args = parser.parse_args() + if args: + parser.error('Unknown options %s' % args) + devices = GetAttachedDevices() + + types, builds, reports = [], [], [] + if devices: + types, builds, reports = zip(*[DeviceInfo(dev) for dev in devices]) + + unique_types = list(set(types)) + unique_builds = list(set(builds)) + + print ('@@@BUILD_STEP Device Status Check - ' + '%d online devices, types %s, builds %s@@@' + % (len(devices), unique_types, unique_builds)) + print '\n'.join(reports) + CheckForMissingDevices(options, devices) + +if __name__ == '__main__': + sys.exit(main()) |