| #!/usr/bin/env python3 |
| # |
| # Copyright 2012 The Chromium Authors |
| # 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.""" |
| |
| |
| class SigtermError(Exception): |
| """Exception used to catch a sigterm.""" |
| |
| |
| 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 |
| 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('^(\\S+)\tdevice$', out.decode('latin1'), 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(_signum, _unused_frame): |
| raise TimeoutException() |
| signal.signal(signal.SIGALRM, TimeoutHandler) |
| |
| # Handle SIGTERMs to ensure clean shutdown |
| def SigtermHandler(_signum, _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: |
| subprocess.call([adb_cmd, '-s', device_id, 'logcat', '-c']) |
| 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: # pylint: disable=bare-except |
| logging.exception('Unexpected exception in main.') |
| finally: |
| for process, _ in devices.values(): |
| if process: |
| try: |
| process.terminate() |
| except OSError: |
| pass |
| os.remove(pid_file_path) |
| |
| |
| if __name__ == '__main__': |
| logging.basicConfig(level=logging.INFO) |
| if 2 <= len(sys.argv) <= 3: |
| print('adb_logcat_monitor: Initializing') |
| if len(sys.argv) == 2: |
| sys.exit(main(sys.argv[1])) |
| sys.exit(main(sys.argv[1], sys.argv[2])) |
| |
| print('Usage: %s <base_dir> [<adb_binary_path>]' % sys.argv[0]) |