Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 """Provides an interface to communicate with the device via the adb command. | 5 """Provides an interface to communicate with the device via the adb command. |
| 6 | 6 |
| 7 Assumes adb binary is currently on system path. | 7 Assumes adb binary is currently on system path. |
| 8 """ | 8 """ |
| 9 | 9 |
| 10 import collections | 10 import collections |
| 11 import datetime | 11 import datetime |
| 12 import logging | 12 import logging |
| 13 import os | 13 import os |
| 14 import re | 14 import re |
| 15 import shlex | 15 import shlex |
| 16 import subprocess | 16 import subprocess |
| 17 import sys | 17 import sys |
| 18 import tempfile | 18 import tempfile |
| 19 import time | 19 import time |
| 20 | 20 |
| 21 import pexpect | 21 import pexpect |
| 22 import io_stats_parser | 22 import io_stats_parser |
| 23 | 23 |
| 24 # adb_interface.py is under ../../../third_party/android_testrunner/ | 24 # adb_interface.py is under ../../../third_party/android_testrunner/ |
| 25 sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', | 25 sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', |
|
Isaac (away)
2012/08/27 10:43:15
Extract part of this into a var named CHROME_SRC?
Philippe
2012/08/28 09:48:29
Done.
| |
| 26 '..', '..', 'third_party', 'android_testrunner')) | 26 '..', '..', 'third_party', 'android_testrunner')) |
| 27 import adb_interface | 27 import adb_interface |
| 28 import cmd_helper | 28 import cmd_helper |
| 29 import errors # is under ../../../third_party/android_testrunner/errors.py | 29 import errors # is under ../../../third_party/android_testrunner/errors.py |
| 30 | 30 |
| 31 | 31 |
| 32 # Pattern to search for the next whole line of pexpect output and capture it | 32 # Pattern to search for the next whole line of pexpect output and capture it |
| 33 # into a match group. We can't use ^ and $ for line start end with pexpect, | 33 # into a match group. We can't use ^ and $ for line start end with pexpect, |
| 34 # see http://www.noah.org/python/pexpect/#doc for explanation why. | 34 # see http://www.noah.org/python/pexpect/#doc for explanation why. |
| 35 PEXPECT_LINE_RE = re.compile('\n([^\r]*)\r') | 35 PEXPECT_LINE_RE = re.compile('\n([^\r]*)\r') |
| (...skipping 22 matching lines...) Expand all Loading... | |
| 58 | 58 |
| 59 # Keycode "enum" suitable for passing to AndroidCommands.SendKey(). | 59 # Keycode "enum" suitable for passing to AndroidCommands.SendKey(). |
| 60 KEYCODE_HOME = 3 | 60 KEYCODE_HOME = 3 |
| 61 KEYCODE_BACK = 4 | 61 KEYCODE_BACK = 4 |
| 62 KEYCODE_DPAD_UP = 19 | 62 KEYCODE_DPAD_UP = 19 |
| 63 KEYCODE_DPAD_DOWN = 20 | 63 KEYCODE_DPAD_DOWN = 20 |
| 64 KEYCODE_DPAD_RIGHT = 22 | 64 KEYCODE_DPAD_RIGHT = 22 |
| 65 KEYCODE_ENTER = 66 | 65 KEYCODE_ENTER = 66 |
| 66 KEYCODE_MENU = 82 | 66 KEYCODE_MENU = 82 |
| 67 | 67 |
| 68 MD5SUM_DEVICE_PATH = '/data/local/tmp/md5sum' | |
| 68 | 69 |
| 69 def GetEmulators(): | 70 def GetEmulators(): |
| 70 """Returns a list of emulators. Does not filter by status (e.g. offline). | 71 """Returns a list of emulators. Does not filter by status (e.g. offline). |
| 71 | 72 |
| 72 Both devices starting with 'emulator' will be returned in below output: | 73 Both devices starting with 'emulator' will be returned in below output: |
| 73 | 74 |
| 74 * daemon not running. starting it now on port 5037 * | 75 * daemon not running. starting it now on port 5037 * |
| 75 * daemon started successfully * | 76 * daemon started successfully * |
| 76 List of devices attached | 77 List of devices attached |
| 77 027c10494100b4d7 device | 78 027c10494100b4d7 device |
| (...skipping 27 matching lines...) Expand all Loading... | |
| 105 emulator-5554 offline | 106 emulator-5554 offline |
| 106 """ | 107 """ |
| 107 re_device = re.compile('^([a-zA-Z0-9_:.-]+)\tdevice$', re.MULTILINE) | 108 re_device = re.compile('^([a-zA-Z0-9_:.-]+)\tdevice$', re.MULTILINE) |
| 108 devices = re_device.findall(cmd_helper.GetCmdOutput(['adb', 'devices'])) | 109 devices = re_device.findall(cmd_helper.GetCmdOutput(['adb', 'devices'])) |
| 109 preferred_device = os.environ.get('ANDROID_SERIAL') | 110 preferred_device = os.environ.get('ANDROID_SERIAL') |
| 110 if preferred_device in devices: | 111 if preferred_device in devices: |
| 111 devices.remove(preferred_device) | 112 devices.remove(preferred_device) |
| 112 devices.insert(0, preferred_device) | 113 devices.insert(0, preferred_device) |
| 113 return devices | 114 return devices |
| 114 | 115 |
| 115 | |
| 116 def _GetHostFileInfo(file_name): | |
| 117 """Returns a tuple containing size and modified UTC time for file_name.""" | |
| 118 # The time accuracy on device is only to minute level, remove the second and | |
| 119 # microsecond from host results. | |
| 120 utc_time = datetime.datetime.utcfromtimestamp(os.path.getmtime(file_name)) | |
| 121 time_delta = datetime.timedelta(seconds=utc_time.second, | |
| 122 microseconds=utc_time.microsecond) | |
| 123 return os.path.getsize(file_name), utc_time - time_delta | |
| 124 | |
| 125 | |
| 126 def ListHostPathContents(path): | |
| 127 """Lists files in all subdirectories of |path|. | |
| 128 | |
| 129 Args: | |
| 130 path: The path to list. | |
| 131 | |
| 132 Returns: | |
| 133 A dict of {"name": (size, lastmod), ...}. | |
| 134 """ | |
| 135 if os.path.isfile(path): | |
| 136 return {os.path.basename(path): _GetHostFileInfo(path)} | |
| 137 ret = {} | |
| 138 for root, dirs, files in os.walk(path): | |
| 139 for d in dirs: | |
| 140 if d.startswith('.'): | |
| 141 dirs.remove(d) # Prune the dir for subsequent iterations. | |
| 142 for f in files: | |
| 143 if f.startswith('.'): | |
| 144 continue | |
| 145 full_file_name = os.path.join(root, f) | |
| 146 file_name = os.path.relpath(full_file_name, path) | |
| 147 ret[file_name] = _GetHostFileInfo(full_file_name) | |
| 148 return ret | |
| 149 | |
| 150 | |
| 151 def _GetFilesFromRecursiveLsOutput(path, ls_output, re_file, utc_offset=None): | 116 def _GetFilesFromRecursiveLsOutput(path, ls_output, re_file, utc_offset=None): |
| 152 """Gets a list of files from `ls` command output. | 117 """Gets a list of files from `ls` command output. |
| 153 | 118 |
| 154 Python's os.walk isn't used because it doesn't work over adb shell. | 119 Python's os.walk isn't used because it doesn't work over adb shell. |
| 155 | 120 |
| 156 Args: | 121 Args: |
| 157 path: The path to list. | 122 path: The path to list. |
| 158 ls_output: A list of lines returned by an `ls -lR` command. | 123 ls_output: A list of lines returned by an `ls -lR` command. |
| 159 re_file: A compiled regular expression which parses a line into named groups | 124 re_file: A compiled regular expression which parses a line into named groups |
| 160 consisting of at minimum "filename", "date", "time", "size" and | 125 consisting of at minimum "filename", "date", "time", "size" and |
| (...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 193 utc_offset = file_match.group('timezone') | 158 utc_offset = file_match.group('timezone') |
| 194 if isinstance(utc_offset, str) and len(utc_offset) == 5: | 159 if isinstance(utc_offset, str) and len(utc_offset) == 5: |
| 195 utc_delta = datetime.timedelta(hours=int(utc_offset[1:3]), | 160 utc_delta = datetime.timedelta(hours=int(utc_offset[1:3]), |
| 196 minutes=int(utc_offset[3:5])) | 161 minutes=int(utc_offset[3:5])) |
| 197 if utc_offset[0:1] == '-': | 162 if utc_offset[0:1] == '-': |
| 198 utc_delta = -utc_delta | 163 utc_delta = -utc_delta |
| 199 lastmod -= utc_delta | 164 lastmod -= utc_delta |
| 200 files[filename] = (int(file_match.group('size')), lastmod) | 165 files[filename] = (int(file_match.group('size')), lastmod) |
| 201 return files | 166 return files |
| 202 | 167 |
| 168 def _ComputeFileListHash(md5sum_output): | |
|
Isaac (away)
2012/08/27 10:43:15
Consider using a list comprehension:
return [l.spl
Philippe
2012/08/28 09:48:29
Good point, indeed.
| |
| 169 """Returns a list of MD5 strings from the provided md5sum output.""" | |
| 170 hashes = [] | |
| 171 lines = md5sum_output | |
|
Isaac (away)
2012/08/27 10:43:15
Unnecessary variable?
Philippe
2012/08/28 09:48:29
Done.
| |
| 172 for line in lines: | |
| 173 hashes.append(line.split(' ')[0]) | |
| 174 return hashes | |
| 175 | |
| 203 | 176 |
| 204 def GetLogTimestamp(log_line, year): | 177 def GetLogTimestamp(log_line, year): |
| 205 """Returns the timestamp of the given |log_line| in the given year.""" | 178 """Returns the timestamp of the given |log_line| in the given year.""" |
| 206 try: | 179 try: |
| 207 return datetime.datetime.strptime('%s-%s' % (year, log_line[:18]), | 180 return datetime.datetime.strptime('%s-%s' % (year, log_line[:18]), |
| 208 '%Y-%m-%d %H:%M:%S.%f') | 181 '%Y-%m-%d %H:%M:%S.%f') |
| 209 except (ValueError, IndexError): | 182 except (ValueError, IndexError): |
| 210 logging.critical('Error reading timestamp from ' + log_line) | 183 logging.critical('Error reading timestamp from ' + log_line) |
| 211 return None | 184 return None |
| 212 | 185 |
| 213 | 186 |
| 214 class AndroidCommands(object): | 187 class AndroidCommands(object): |
| 215 """Helper class for communicating with Android device via adb. | 188 """Helper class for communicating with Android device via adb. |
| 216 | 189 |
| 217 Args: | 190 Args: |
| 218 device: If given, adb commands are only send to the device of this ID. | 191 device: If given, adb commands are only send to the device of this ID. |
| 219 Otherwise commands are sent to all attached devices. | 192 Otherwise commands are sent to all attached devices. |
| 220 """ | 193 """ |
| 221 | 194 |
| 222 def __init__(self, device=None): | 195 def __init__(self, device=None): |
| 223 self._adb = adb_interface.AdbInterface() | 196 self._adb = adb_interface.AdbInterface() |
| 224 if device: | 197 if device: |
| 225 self._adb.SetTargetSerial(device) | 198 self._adb.SetTargetSerial(device) |
| 226 self._logcat = None | 199 self._logcat = None |
| 227 self._original_governor = None | 200 self._original_governor = None |
| 228 self._pushed_files = [] | 201 self._pushed_files = [] |
| 229 self._device_utc_offset = self.RunShellCommand('date +%z')[0] | 202 self._device_utc_offset = self.RunShellCommand('date +%z')[0] |
| 203 chrome_src_path = os.getenv('CHROME_SRC') | |
|
Isaac (away)
2012/08/27 10:43:15
prefer we avoid new environment variables dependen
Philippe
2012/08/28 09:48:29
$CHROME_SRC is not new but indeed it's better to u
| |
| 204 assert chrome_src_path | |
| 205 self._md5sum_path = '%s/out/Debug/md5sum' % (chrome_src_path) | |
|
Isaac (away)
2012/08/27 10:43:15
rather than guessing Release vs. Debug, maybe take
Philippe
2012/08/28 09:48:29
Unfortunately some clients of AndroidCommands don'
Isaac (away)
2012/08/28 17:12:11
I've recently added the build type to buildbot fac
Philippe
2012/08/29 08:36:45
What about non-buildbot environments (i.e. develop
Isaac (away)
2012/08/30 06:50:57
Good point. I think this is OK for now then.
| |
| 206 if not os.path.exists(self._md5sum_path): | |
| 207 self._md5sum_path = '%s/out/Release/md5sum' % (chrome_src_path) | |
| 208 if not os.path.exists(self._md5sum_path): | |
| 209 print >>sys.stderr, 'Please build md5sum (\'make md5sum\')' | |
| 210 sys.exit(1) | |
| 211 # Push the md5sum binary to the device if needed. | |
| 212 if not self.FileExistsOnDevice(MD5SUM_DEVICE_PATH): | |
| 213 command = 'push %s %s' % (self._md5sum_path, MD5SUM_DEVICE_PATH) | |
| 214 logging.info(command) | |
| 215 assert self._adb.SendCommand(command) | |
| 230 | 216 |
| 231 def Adb(self): | 217 def Adb(self): |
| 232 """Returns our AdbInterface to avoid us wrapping all its methods.""" | 218 """Returns our AdbInterface to avoid us wrapping all its methods.""" |
| 233 return self._adb | 219 return self._adb |
| 234 | 220 |
| 235 def IsRootEnabled(self): | 221 def IsRootEnabled(self): |
| 236 """Checks if root is enabled on the device.""" | 222 """Checks if root is enabled on the device.""" |
| 237 root_test_output = self.RunShellCommand('ls /root') or [''] | 223 root_test_output = self.RunShellCommand('ls /root') or [''] |
| 238 return not 'Permission denied' in root_test_output[0] | 224 return not 'Permission denied' in root_test_output[0] |
| 239 | 225 |
| (...skipping 16 matching lines...) Expand all Loading... | |
| 256 try: | 242 try: |
| 257 self._adb.WaitForDevicePm() | 243 self._adb.WaitForDevicePm() |
| 258 return # Success | 244 return # Success |
| 259 except errors.WaitForResponseTimedOutError as e: | 245 except errors.WaitForResponseTimedOutError as e: |
| 260 last_err = e | 246 last_err = e |
| 261 logging.warning('Restarting and retrying after timeout: %s', e) | 247 logging.warning('Restarting and retrying after timeout: %s', e) |
| 262 retries -= 1 | 248 retries -= 1 |
| 263 self.RestartShell() | 249 self.RestartShell() |
| 264 raise last_err # Only reached after max retries, re-raise the last error. | 250 raise last_err # Only reached after max retries, re-raise the last error. |
| 265 | 251 |
| 266 def SynchronizeDateTime(self): | |
| 267 """Synchronize date/time between host and device.""" | |
| 268 self._adb.SendShellCommand('date -u %f' % time.time()) | |
| 269 | |
| 270 def RestartShell(self): | 252 def RestartShell(self): |
| 271 """Restarts the shell on the device. Does not block for it to return.""" | 253 """Restarts the shell on the device. Does not block for it to return.""" |
| 272 self.RunShellCommand('stop') | 254 self.RunShellCommand('stop') |
| 273 self.RunShellCommand('start') | 255 self.RunShellCommand('start') |
| 274 | 256 |
| 275 def Reboot(self, full_reboot=True): | 257 def Reboot(self, full_reboot=True): |
| 276 """Reboots the device and waits for the package manager to return. | 258 """Reboots the device and waits for the package manager to return. |
| 277 | 259 |
| 278 Args: | 260 Args: |
| 279 full_reboot: Whether to fully reboot the device or just restart the shell. | 261 full_reboot: Whether to fully reboot the device or just restart the shell. |
| (...skipping 259 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 539 | 521 |
| 540 Args: | 522 Args: |
| 541 keycode: Numeric keycode to send (see "enum" at top of file). | 523 keycode: Numeric keycode to send (see "enum" at top of file). |
| 542 """ | 524 """ |
| 543 self.RunShellCommand('input keyevent %d' % keycode) | 525 self.RunShellCommand('input keyevent %d' % keycode) |
| 544 | 526 |
| 545 def PushIfNeeded(self, local_path, device_path): | 527 def PushIfNeeded(self, local_path, device_path): |
| 546 """Pushes |local_path| to |device_path|. | 528 """Pushes |local_path| to |device_path|. |
| 547 | 529 |
| 548 Works for files and directories. This method skips copying any paths in | 530 Works for files and directories. This method skips copying any paths in |
| 549 |test_data_paths| that already exist on the device with the same timestamp | 531 |test_data_paths| that already exist on the device with the same md5. |
| 550 and size. | |
| 551 | 532 |
| 552 All pushed files can be removed by calling RemovePushedFiles(). | 533 All pushed files can be removed by calling RemovePushedFiles(). |
| 553 """ | 534 """ |
| 554 assert os.path.exists(local_path), 'Local path not found %s' % local_path | 535 assert os.path.exists(local_path), 'Local path not found %s' % local_path |
| 555 self._pushed_files.append(device_path) | 536 self._pushed_files.append(device_path) |
| 556 | 537 |
| 557 # If the path contents are the same, there's nothing to do. | 538 hashes_on_device = _ComputeFileListHash( |
| 558 local_contents = ListHostPathContents(local_path) | 539 self.RunShellCommand(MD5SUM_DEVICE_PATH + ' ' + device_path)) |
|
Isaac (away)
2012/08/27 10:43:15
Push md5sum binary if not on device?
Philippe
2012/08/28 09:48:29
This is in the constructor on line 212. I avoided
| |
| 559 device_contents = self.ListPathContents(device_path) | 540 assert os.path.exists(local_path), 'Local path not found %s' % local_path |
| 560 # Only compare the size and timestamp if only copying a file because | 541 hashes_on_host = _ComputeFileListHash( |
| 561 # the filename on device can be renamed. | 542 subprocess.Popen('%s_host_bin %s' % (self._md5sum_path, local_path), |
|
Isaac (away)
2012/08/27 10:43:15
Look at cmd_helper.py :: GetCmdOutput()
Philippe
2012/08/28 09:48:29
Thanks.
FYI, I'm using shell=True because subproc
| |
| 562 if os.path.isfile(local_path): | 543 shell=True, stdout=subprocess.PIPE).stdout) |
| 563 assert len(local_contents) == 1 | 544 if hashes_on_device == hashes_on_host: |
| 564 is_equal = local_contents.values() == device_contents.values() | |
| 565 else: | |
| 566 is_equal = local_contents == device_contents | |
| 567 if is_equal: | |
| 568 logging.info('%s is up-to-date. Skipping file push.', device_path) | |
| 569 return | 545 return |
| 570 | 546 |
| 571 # They don't match, so remove everything first and then create it. | 547 # They don't match, so remove everything first and then create it. |
| 572 if os.path.isdir(local_path): | 548 if os.path.isdir(local_path): |
| 573 self.RunShellCommand('rm -r %s' % device_path, timeout_time=2*60) | 549 self.RunShellCommand('rm -r %s' % device_path, timeout_time=2*60) |
| 574 self.RunShellCommand('mkdir -p %s' % device_path) | 550 self.RunShellCommand('mkdir -p %s' % device_path) |
| 575 | 551 |
| 576 # NOTE: We can't use adb_interface.Push() because it hardcodes a timeout of | 552 # NOTE: We can't use adb_interface.Push() because it hardcodes a timeout of |
| 577 # 60 seconds which isn't sufficient for a lot of users of this method. | 553 # 60 seconds which isn't sufficient for a lot of users of this method. |
| 578 push_command = 'push %s %s' % (local_path, device_path) | 554 push_command = 'push %s %s' % (local_path, device_path) |
| (...skipping 401 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 980 if len(process_results) <= 8: | 956 if len(process_results) <= 8: |
| 981 continue | 957 continue |
| 982 # Column 0 is the executable name | 958 # Column 0 is the executable name |
| 983 # Column 1 is the pid | 959 # Column 1 is the pid |
| 984 # Column 8 is the Inode in use | 960 # Column 8 is the Inode in use |
| 985 if process_results[8] == socket_name: | 961 if process_results[8] == socket_name: |
| 986 pids.append((int(process_results[1]), process_results[0])) | 962 pids.append((int(process_results[1]), process_results[0])) |
| 987 break | 963 break |
| 988 logging.info('PidsUsingDevicePort: %s', pids) | 964 logging.info('PidsUsingDevicePort: %s', pids) |
| 989 return pids | 965 return pids |
| 966 | |
| 967 def FileExistsOnDevice(self, file_name): | |
| 968 """Checks whether the given (regular) file exists on the device. | |
| 969 | |
| 970 Args: | |
| 971 file_name: Full path of file to check. | |
| 972 | |
| 973 Returns: | |
| 974 True if the file exists, False otherwise. | |
| 975 """ | |
| 976 assert '"' not in file_name, 'file_name cannot contain double quotes' | |
| 977 status = self._adb.SendShellCommand( | |
|
Isaac (away)
2012/08/27 10:43:15
1) Prefer self.RunShellCommand() (they do the same
Philippe
2012/08/28 09:48:29
This is coming from downstream. I personally think
Isaac (away)
2012/08/28 17:12:11
Quote escaping, as that will be hard to understand
| |
| 978 '"test -f \\"%s\\" && echo 1 || echo 0"' % (file_name)) | |
| 979 return int(status) == 1 | |
| OLD | NEW |