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 |