157 lines
4.8 KiB
Python
157 lines
4.8 KiB
Python
|
#!/usr/bin/env 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:
|
||
|
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:
|
||
|
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]
|