Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(443)

Side by Side Diff: build/android/pylib/android_commands.py

Issue 10867008: Get rid of device/host clock synchronization in android_commands.py. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Created 8 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « build/all_android.gyp ('k') | build/android/pylib/base_test_runner.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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
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
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
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
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
OLDNEW
« no previous file with comments | « build/all_android.gyp ('k') | build/android/pylib/base_test_runner.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698