|
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 |
|
3 # found in the LICENSE file. |
|
4 |
|
5 """Provides an interface to communicate with the device via the adb command. |
|
6 |
|
7 Assumes adb binary is currently on system path. |
|
8 """ |
|
9 |
|
10 import collections |
|
11 import datetime |
|
12 import logging |
|
13 import os |
|
14 import re |
|
15 import shlex |
|
16 import subprocess |
|
17 import sys |
|
18 import tempfile |
|
19 import time |
|
20 |
|
21 import io_stats_parser |
|
22 from pylib import pexpect |
|
23 |
|
24 CHROME_SRC = os.path.join( |
|
25 os.path.abspath(os.path.dirname(__file__)), '..', '..', '..') |
|
26 |
|
27 sys.path.append(os.path.join(CHROME_SRC, 'third_party', 'android_testrunner')) |
|
28 import adb_interface |
|
29 |
|
30 import cmd_helper |
|
31 import errors # is under ../../../third_party/android_testrunner/errors.py |
|
32 |
|
33 |
|
34 # Pattern to search for the next whole line of pexpect output and capture it |
|
35 # into a match group. We can't use ^ and $ for line start end with pexpect, |
|
36 # see http://www.noah.org/python/pexpect/#doc for explanation why. |
|
37 PEXPECT_LINE_RE = re.compile('\n([^\r]*)\r') |
|
38 |
|
39 # Set the adb shell prompt to be a unique marker that will [hopefully] not |
|
40 # appear at the start of any line of a command's output. |
|
41 SHELL_PROMPT = '~+~PQ\x17RS~+~' |
|
42 |
|
43 # Java properties file |
|
44 LOCAL_PROPERTIES_PATH = '/data/local.prop' |
|
45 |
|
46 # Property in /data/local.prop that controls Java assertions. |
|
47 JAVA_ASSERT_PROPERTY = 'dalvik.vm.enableassertions' |
|
48 |
|
49 MEMORY_INFO_RE = re.compile('^(?P<key>\w+):\s+(?P<usage_kb>\d+) kB$') |
|
50 NVIDIA_MEMORY_INFO_RE = re.compile('^\s*(?P<user>\S+)\s*(?P<name>\S+)\s*' |
|
51 '(?P<pid>\d+)\s*(?P<usage_bytes>\d+)$') |
|
52 |
|
53 # Keycode "enum" suitable for passing to AndroidCommands.SendKey(). |
|
54 KEYCODE_HOME = 3 |
|
55 KEYCODE_BACK = 4 |
|
56 KEYCODE_DPAD_UP = 19 |
|
57 KEYCODE_DPAD_DOWN = 20 |
|
58 KEYCODE_DPAD_RIGHT = 22 |
|
59 KEYCODE_ENTER = 66 |
|
60 KEYCODE_MENU = 82 |
|
61 |
|
62 MD5SUM_DEVICE_PATH = '/data/local/tmp/md5sum_bin' |
|
63 |
|
64 def GetEmulators(): |
|
65 """Returns a list of emulators. Does not filter by status (e.g. offline). |
|
66 |
|
67 Both devices starting with 'emulator' will be returned in below output: |
|
68 |
|
69 * daemon not running. starting it now on port 5037 * |
|
70 * daemon started successfully * |
|
71 List of devices attached |
|
72 027c10494100b4d7 device |
|
73 emulator-5554 offline |
|
74 emulator-5558 device |
|
75 """ |
|
76 re_device = re.compile('^emulator-[0-9]+', re.MULTILINE) |
|
77 devices = re_device.findall(cmd_helper.GetCmdOutput(['adb', 'devices'])) |
|
78 return devices |
|
79 |
|
80 |
|
81 def GetAVDs(): |
|
82 """Returns a list of AVDs.""" |
|
83 re_avd = re.compile('^[ ]+Name: ([a-zA-Z0-9_:.-]+)', re.MULTILINE) |
|
84 avds = re_avd.findall(cmd_helper.GetCmdOutput(['android', 'list', 'avd'])) |
|
85 return avds |
|
86 |
|
87 |
|
88 def GetAttachedDevices(): |
|
89 """Returns a list of attached, online android devices. |
|
90 |
|
91 If a preferred device has been set with ANDROID_SERIAL, it will be first in |
|
92 the returned list. |
|
93 |
|
94 Example output: |
|
95 |
|
96 * daemon not running. starting it now on port 5037 * |
|
97 * daemon started successfully * |
|
98 List of devices attached |
|
99 027c10494100b4d7 device |
|
100 emulator-5554 offline |
|
101 """ |
|
102 re_device = re.compile('^([a-zA-Z0-9_:.-]+)\tdevice$', re.MULTILINE) |
|
103 devices = re_device.findall(cmd_helper.GetCmdOutput(['adb', 'devices'])) |
|
104 preferred_device = os.environ.get('ANDROID_SERIAL') |
|
105 if preferred_device in devices: |
|
106 devices.remove(preferred_device) |
|
107 devices.insert(0, preferred_device) |
|
108 return devices |
|
109 |
|
110 def _GetFilesFromRecursiveLsOutput(path, ls_output, re_file, utc_offset=None): |
|
111 """Gets a list of files from `ls` command output. |
|
112 |
|
113 Python's os.walk isn't used because it doesn't work over adb shell. |
|
114 |
|
115 Args: |
|
116 path: The path to list. |
|
117 ls_output: A list of lines returned by an `ls -lR` command. |
|
118 re_file: A compiled regular expression which parses a line into named groups |
|
119 consisting of at minimum "filename", "date", "time", "size" and |
|
120 optionally "timezone". |
|
121 utc_offset: A 5-character string of the form +HHMM or -HHMM, where HH is a |
|
122 2-digit string giving the number of UTC offset hours, and MM is a |
|
123 2-digit string giving the number of UTC offset minutes. If the input |
|
124 utc_offset is None, will try to look for the value of "timezone" if it |
|
125 is specified in re_file. |
|
126 |
|
127 Returns: |
|
128 A dict of {"name": (size, lastmod), ...} where: |
|
129 name: The file name relative to |path|'s directory. |
|
130 size: The file size in bytes (0 for directories). |
|
131 lastmod: The file last modification date in UTC. |
|
132 """ |
|
133 re_directory = re.compile('^%s/(?P<dir>[^:]+):$' % re.escape(path)) |
|
134 path_dir = os.path.dirname(path) |
|
135 |
|
136 current_dir = '' |
|
137 files = {} |
|
138 for line in ls_output: |
|
139 directory_match = re_directory.match(line) |
|
140 if directory_match: |
|
141 current_dir = directory_match.group('dir') |
|
142 continue |
|
143 file_match = re_file.match(line) |
|
144 if file_match: |
|
145 filename = os.path.join(current_dir, file_match.group('filename')) |
|
146 if filename.startswith(path_dir): |
|
147 filename = filename[len(path_dir)+1:] |
|
148 lastmod = datetime.datetime.strptime( |
|
149 file_match.group('date') + ' ' + file_match.group('time')[:5], |
|
150 '%Y-%m-%d %H:%M') |
|
151 if not utc_offset and 'timezone' in re_file.groupindex: |
|
152 utc_offset = file_match.group('timezone') |
|
153 if isinstance(utc_offset, str) and len(utc_offset) == 5: |
|
154 utc_delta = datetime.timedelta(hours=int(utc_offset[1:3]), |
|
155 minutes=int(utc_offset[3:5])) |
|
156 if utc_offset[0:1] == '-': |
|
157 utc_delta = -utc_delta |
|
158 lastmod -= utc_delta |
|
159 files[filename] = (int(file_match.group('size')), lastmod) |
|
160 return files |
|
161 |
|
162 def _ComputeFileListHash(md5sum_output): |
|
163 """Returns a list of MD5 strings from the provided md5sum output.""" |
|
164 return [line.split(' ')[0] for line in md5sum_output] |
|
165 |
|
166 def _HasAdbPushSucceeded(command_output): |
|
167 """Returns whether adb push has succeeded from the provided output.""" |
|
168 if not command_output: |
|
169 return False |
|
170 # Success looks like this: "3035 KB/s (12512056 bytes in 4.025s)" |
|
171 # Errors look like this: "failed to copy ... " |
|
172 if not re.search('^[0-9]', command_output.splitlines()[-1]): |
|
173 logging.critical('PUSH FAILED: ' + command_output) |
|
174 return False |
|
175 return True |
|
176 |
|
177 def GetLogTimestamp(log_line, year): |
|
178 """Returns the timestamp of the given |log_line| in the given year.""" |
|
179 try: |
|
180 return datetime.datetime.strptime('%s-%s' % (year, log_line[:18]), |
|
181 '%Y-%m-%d %H:%M:%S.%f') |
|
182 except (ValueError, IndexError): |
|
183 logging.critical('Error reading timestamp from ' + log_line) |
|
184 return None |
|
185 |
|
186 |
|
187 class AndroidCommands(object): |
|
188 """Helper class for communicating with Android device via adb. |
|
189 |
|
190 Args: |
|
191 device: If given, adb commands are only send to the device of this ID. |
|
192 Otherwise commands are sent to all attached devices. |
|
193 """ |
|
194 |
|
195 def __init__(self, device=None): |
|
196 self._adb = adb_interface.AdbInterface() |
|
197 if device: |
|
198 self._adb.SetTargetSerial(device) |
|
199 self._logcat = None |
|
200 self.logcat_process = None |
|
201 self._pushed_files = [] |
|
202 self._device_utc_offset = self.RunShellCommand('date +%z')[0] |
|
203 self._md5sum_path = '' |
|
204 self._external_storage = '' |
|
205 |
|
206 def Adb(self): |
|
207 """Returns our AdbInterface to avoid us wrapping all its methods.""" |
|
208 return self._adb |
|
209 |
|
210 def IsRootEnabled(self): |
|
211 """Checks if root is enabled on the device.""" |
|
212 root_test_output = self.RunShellCommand('ls /root') or [''] |
|
213 return not 'Permission denied' in root_test_output[0] |
|
214 |
|
215 def EnableAdbRoot(self): |
|
216 """Enables adb root on the device. |
|
217 |
|
218 Returns: |
|
219 True: if output from executing adb root was as expected. |
|
220 False: otherwise. |
|
221 """ |
|
222 return_value = self._adb.EnableAdbRoot() |
|
223 # EnableAdbRoot inserts a call for wait-for-device only when adb logcat |
|
224 # output matches what is expected. Just to be safe add a call to |
|
225 # wait-for-device. |
|
226 self._adb.SendCommand('wait-for-device') |
|
227 return return_value |
|
228 |
|
229 def GetDeviceYear(self): |
|
230 """Returns the year information of the date on device.""" |
|
231 return self.RunShellCommand('date +%Y')[0] |
|
232 |
|
233 def GetExternalStorage(self): |
|
234 if not self._external_storage: |
|
235 self._external_storage = self.RunShellCommand('echo $EXTERNAL_STORAGE')[0] |
|
236 assert self._external_storage, 'Unable to find $EXTERNAL_STORAGE' |
|
237 return self._external_storage |
|
238 |
|
239 def WaitForDevicePm(self): |
|
240 """Blocks until the device's package manager is available. |
|
241 |
|
242 To workaround http://b/5201039, we restart the shell and retry if the |
|
243 package manager isn't back after 120 seconds. |
|
244 |
|
245 Raises: |
|
246 errors.WaitForResponseTimedOutError after max retries reached. |
|
247 """ |
|
248 last_err = None |
|
249 retries = 3 |
|
250 while retries: |
|
251 try: |
|
252 self._adb.WaitForDevicePm() |
|
253 return # Success |
|
254 except errors.WaitForResponseTimedOutError as e: |
|
255 last_err = e |
|
256 logging.warning('Restarting and retrying after timeout: %s', e) |
|
257 retries -= 1 |
|
258 self.RestartShell() |
|
259 raise last_err # Only reached after max retries, re-raise the last error. |
|
260 |
|
261 def RestartShell(self): |
|
262 """Restarts the shell on the device. Does not block for it to return.""" |
|
263 self.RunShellCommand('stop') |
|
264 self.RunShellCommand('start') |
|
265 |
|
266 def Reboot(self, full_reboot=True): |
|
267 """Reboots the device and waits for the package manager to return. |
|
268 |
|
269 Args: |
|
270 full_reboot: Whether to fully reboot the device or just restart the shell. |
|
271 """ |
|
272 # TODO(torne): hive can't reboot the device either way without breaking the |
|
273 # connection; work out if we can handle this better |
|
274 if os.environ.get('USING_HIVE'): |
|
275 logging.warning('Ignoring reboot request as we are on hive') |
|
276 return |
|
277 if full_reboot or not self.IsRootEnabled(): |
|
278 self._adb.SendCommand('reboot') |
|
279 timeout = 300 |
|
280 else: |
|
281 self.RestartShell() |
|
282 timeout = 120 |
|
283 # To run tests we need at least the package manager and the sd card (or |
|
284 # other external storage) to be ready. |
|
285 self.WaitForDevicePm() |
|
286 self.WaitForSdCardReady(timeout) |
|
287 |
|
288 def Uninstall(self, package): |
|
289 """Uninstalls the specified package from the device. |
|
290 |
|
291 Args: |
|
292 package: Name of the package to remove. |
|
293 |
|
294 Returns: |
|
295 A status string returned by adb uninstall |
|
296 """ |
|
297 uninstall_command = 'uninstall %s' % package |
|
298 |
|
299 logging.info('>>> $' + uninstall_command) |
|
300 return self._adb.SendCommand(uninstall_command, timeout_time=60) |
|
301 |
|
302 def Install(self, package_file_path, reinstall=False): |
|
303 """Installs the specified package to the device. |
|
304 |
|
305 Args: |
|
306 package_file_path: Path to .apk file to install. |
|
307 reinstall: Reinstall an existing apk, keeping the data. |
|
308 |
|
309 Returns: |
|
310 A status string returned by adb install |
|
311 """ |
|
312 assert os.path.isfile(package_file_path), ('<%s> is not file' % |
|
313 package_file_path) |
|
314 |
|
315 install_cmd = ['install'] |
|
316 |
|
317 if reinstall: |
|
318 install_cmd.append('-r') |
|
319 |
|
320 install_cmd.append(package_file_path) |
|
321 install_cmd = ' '.join(install_cmd) |
|
322 |
|
323 logging.info('>>> $' + install_cmd) |
|
324 return self._adb.SendCommand(install_cmd, timeout_time=2*60, retry_count=0) |
|
325 |
|
326 def ManagedInstall(self, apk_path, keep_data=False, package_name=None, |
|
327 reboots_on_failure=2): |
|
328 """Installs specified package and reboots device on timeouts. |
|
329 |
|
330 Args: |
|
331 apk_path: Path to .apk file to install. |
|
332 keep_data: Reinstalls instead of uninstalling first, preserving the |
|
333 application data. |
|
334 package_name: Package name (only needed if keep_data=False). |
|
335 reboots_on_failure: number of time to reboot if package manager is frozen. |
|
336 |
|
337 Returns: |
|
338 A status string returned by adb install |
|
339 """ |
|
340 reboots_left = reboots_on_failure |
|
341 while True: |
|
342 try: |
|
343 if not keep_data: |
|
344 assert package_name |
|
345 self.Uninstall(package_name) |
|
346 install_status = self.Install(apk_path, reinstall=keep_data) |
|
347 if 'Success' in install_status: |
|
348 return install_status |
|
349 except errors.WaitForResponseTimedOutError: |
|
350 print '@@@STEP_WARNINGS@@@' |
|
351 logging.info('Timeout on installing %s' % apk_path) |
|
352 |
|
353 if reboots_left <= 0: |
|
354 raise Exception('Install failure') |
|
355 |
|
356 # Force a hard reboot on last attempt |
|
357 self.Reboot(full_reboot=(reboots_left == 1)) |
|
358 reboots_left -= 1 |
|
359 |
|
360 def MakeSystemFolderWritable(self): |
|
361 """Remounts the /system folder rw.""" |
|
362 out = self._adb.SendCommand('remount') |
|
363 if out.strip() != 'remount succeeded': |
|
364 raise errors.MsgException('Remount failed: %s' % out) |
|
365 |
|
366 def RestartAdbServer(self): |
|
367 """Restart the adb server.""" |
|
368 self.KillAdbServer() |
|
369 self.StartAdbServer() |
|
370 |
|
371 def KillAdbServer(self): |
|
372 """Kill adb server.""" |
|
373 adb_cmd = ['adb', 'kill-server'] |
|
374 return cmd_helper.RunCmd(adb_cmd) |
|
375 |
|
376 def StartAdbServer(self): |
|
377 """Start adb server.""" |
|
378 adb_cmd = ['adb', 'start-server'] |
|
379 return cmd_helper.RunCmd(adb_cmd) |
|
380 |
|
381 def WaitForSystemBootCompleted(self, wait_time): |
|
382 """Waits for targeted system's boot_completed flag to be set. |
|
383 |
|
384 Args: |
|
385 wait_time: time in seconds to wait |
|
386 |
|
387 Raises: |
|
388 WaitForResponseTimedOutError if wait_time elapses and flag still not |
|
389 set. |
|
390 """ |
|
391 logging.info('Waiting for system boot completed...') |
|
392 self._adb.SendCommand('wait-for-device') |
|
393 # Now the device is there, but system not boot completed. |
|
394 # Query the sys.boot_completed flag with a basic command |
|
395 boot_completed = False |
|
396 attempts = 0 |
|
397 wait_period = 5 |
|
398 while not boot_completed and (attempts * wait_period) < wait_time: |
|
399 output = self._adb.SendShellCommand('getprop sys.boot_completed', |
|
400 retry_count=1) |
|
401 output = output.strip() |
|
402 if output == '1': |
|
403 boot_completed = True |
|
404 else: |
|
405 # If 'error: xxx' returned when querying the flag, it means |
|
406 # adb server lost the connection to the emulator, so restart the adb |
|
407 # server. |
|
408 if 'error:' in output: |
|
409 self.RestartAdbServer() |
|
410 time.sleep(wait_period) |
|
411 attempts += 1 |
|
412 if not boot_completed: |
|
413 raise errors.WaitForResponseTimedOutError( |
|
414 'sys.boot_completed flag was not set after %s seconds' % wait_time) |
|
415 |
|
416 def WaitForSdCardReady(self, timeout_time): |
|
417 """Wait for the SD card ready before pushing data into it.""" |
|
418 logging.info('Waiting for SD card ready...') |
|
419 sdcard_ready = False |
|
420 attempts = 0 |
|
421 wait_period = 5 |
|
422 external_storage = self.GetExternalStorage() |
|
423 while not sdcard_ready and attempts * wait_period < timeout_time: |
|
424 output = self.RunShellCommand('ls ' + external_storage) |
|
425 if output: |
|
426 sdcard_ready = True |
|
427 else: |
|
428 time.sleep(wait_period) |
|
429 attempts += 1 |
|
430 if not sdcard_ready: |
|
431 raise errors.WaitForResponseTimedOutError( |
|
432 'SD card not ready after %s seconds' % timeout_time) |
|
433 |
|
434 # It is tempting to turn this function into a generator, however this is not |
|
435 # possible without using a private (local) adb_shell instance (to ensure no |
|
436 # other command interleaves usage of it), which would defeat the main aim of |
|
437 # being able to reuse the adb shell instance across commands. |
|
438 def RunShellCommand(self, command, timeout_time=20, log_result=False): |
|
439 """Send a command to the adb shell and return the result. |
|
440 |
|
441 Args: |
|
442 command: String containing the shell command to send. Must not include |
|
443 the single quotes as we use them to escape the whole command. |
|
444 timeout_time: Number of seconds to wait for command to respond before |
|
445 retrying, used by AdbInterface.SendShellCommand. |
|
446 log_result: Boolean to indicate whether we should log the result of the |
|
447 shell command. |
|
448 |
|
449 Returns: |
|
450 list containing the lines of output received from running the command |
|
451 """ |
|
452 logging.info('>>> $' + command) |
|
453 if "'" in command: logging.warning(command + " contains ' quotes") |
|
454 result = self._adb.SendShellCommand( |
|
455 "'%s'" % command, timeout_time).splitlines() |
|
456 if ['error: device not found'] == result: |
|
457 raise errors.DeviceUnresponsiveError('device not found') |
|
458 if log_result: |
|
459 logging.info('\n>>> '.join(result)) |
|
460 return result |
|
461 |
|
462 def KillAll(self, process): |
|
463 """Android version of killall, connected via adb. |
|
464 |
|
465 Args: |
|
466 process: name of the process to kill off |
|
467 |
|
468 Returns: |
|
469 the number of processes killed |
|
470 """ |
|
471 pids = self.ExtractPid(process) |
|
472 if pids: |
|
473 self.RunShellCommand('kill ' + ' '.join(pids)) |
|
474 return len(pids) |
|
475 |
|
476 def KillAllBlocking(self, process, timeout_sec): |
|
477 """Blocking version of killall, connected via adb. |
|
478 |
|
479 This waits until no process matching the corresponding name appears in ps' |
|
480 output anymore. |
|
481 |
|
482 Args: |
|
483 process: name of the process to kill off |
|
484 timeout_sec: the timeout in seconds |
|
485 |
|
486 Returns: |
|
487 the number of processes killed |
|
488 """ |
|
489 processes_killed = self.KillAll(process) |
|
490 if processes_killed: |
|
491 elapsed = 0 |
|
492 wait_period = 0.1 |
|
493 # Note that this doesn't take into account the time spent in ExtractPid(). |
|
494 while self.ExtractPid(process) and elapsed < timeout_sec: |
|
495 time.sleep(wait_period) |
|
496 elapsed += wait_period |
|
497 if elapsed >= timeout_sec: |
|
498 return 0 |
|
499 return processes_killed |
|
500 |
|
501 def StartActivity(self, package, activity, wait_for_completion=False, |
|
502 action='android.intent.action.VIEW', |
|
503 category=None, data=None, |
|
504 extras=None, trace_file_name=None): |
|
505 """Starts |package|'s activity on the device. |
|
506 |
|
507 Args: |
|
508 package: Name of package to start (e.g. 'com.google.android.apps.chrome'). |
|
509 activity: Name of activity (e.g. '.Main' or |
|
510 'com.google.android.apps.chrome.Main'). |
|
511 wait_for_completion: wait for the activity to finish launching (-W flag). |
|
512 action: string (e.g. "android.intent.action.MAIN"). Default is VIEW. |
|
513 category: string (e.g. "android.intent.category.HOME") |
|
514 data: Data string to pass to activity (e.g. 'http://www.example.com/'). |
|
515 extras: Dict of extras to pass to activity. Values are significant. |
|
516 trace_file_name: If used, turns on and saves the trace to this file name. |
|
517 """ |
|
518 cmd = 'am start -a %s' % action |
|
519 if wait_for_completion: |
|
520 cmd += ' -W' |
|
521 if category: |
|
522 cmd += ' -c %s' % category |
|
523 if package and activity: |
|
524 cmd += ' -n %s/%s' % (package, activity) |
|
525 if data: |
|
526 cmd += ' -d "%s"' % data |
|
527 if extras: |
|
528 for key in extras: |
|
529 value = extras[key] |
|
530 if isinstance(value, str): |
|
531 cmd += ' --es' |
|
532 elif isinstance(value, bool): |
|
533 cmd += ' --ez' |
|
534 elif isinstance(value, int): |
|
535 cmd += ' --ei' |
|
536 else: |
|
537 raise NotImplementedError( |
|
538 'Need to teach StartActivity how to pass %s extras' % type(value)) |
|
539 cmd += ' %s %s' % (key, value) |
|
540 if trace_file_name: |
|
541 cmd += ' --start-profiler ' + trace_file_name |
|
542 self.RunShellCommand(cmd) |
|
543 |
|
544 def GoHome(self): |
|
545 """Tell the device to return to the home screen. Blocks until completion.""" |
|
546 self.RunShellCommand('am start -W ' |
|
547 '-a android.intent.action.MAIN -c android.intent.category.HOME') |
|
548 |
|
549 def CloseApplication(self, package): |
|
550 """Attempt to close down the application, using increasing violence. |
|
551 |
|
552 Args: |
|
553 package: Name of the process to kill off, e.g. |
|
554 com.google.android.apps.chrome |
|
555 """ |
|
556 self.RunShellCommand('am force-stop ' + package) |
|
557 |
|
558 def ClearApplicationState(self, package): |
|
559 """Closes and clears all state for the given |package|.""" |
|
560 self.CloseApplication(package) |
|
561 self.RunShellCommand('rm -r /data/data/%s/app_*' % package) |
|
562 self.RunShellCommand('rm -r /data/data/%s/cache/*' % package) |
|
563 self.RunShellCommand('rm -r /data/data/%s/files/*' % package) |
|
564 self.RunShellCommand('rm -r /data/data/%s/shared_prefs/*' % package) |
|
565 |
|
566 def SendKeyEvent(self, keycode): |
|
567 """Sends keycode to the device. |
|
568 |
|
569 Args: |
|
570 keycode: Numeric keycode to send (see "enum" at top of file). |
|
571 """ |
|
572 self.RunShellCommand('input keyevent %d' % keycode) |
|
573 |
|
574 def PushIfNeeded(self, local_path, device_path): |
|
575 """Pushes |local_path| to |device_path|. |
|
576 |
|
577 Works for files and directories. This method skips copying any paths in |
|
578 |test_data_paths| that already exist on the device with the same hash. |
|
579 |
|
580 All pushed files can be removed by calling RemovePushedFiles(). |
|
581 """ |
|
582 assert os.path.exists(local_path), 'Local path not found %s' % local_path |
|
583 |
|
584 if not self._md5sum_path: |
|
585 default_build_type = os.environ.get('BUILD_TYPE', 'Debug') |
|
586 md5sum_path = '%s/out/%s/md5sum_bin' % (CHROME_SRC, default_build_type) |
|
587 if not os.path.exists(md5sum_path): |
|
588 md5sum_path = '%s/out/Release/md5sum_bin' % (CHROME_SRC) |
|
589 if not os.path.exists(md5sum_path): |
|
590 print >> sys.stderr, 'Please build md5sum.' |
|
591 sys.exit(1) |
|
592 command = 'push %s %s' % (md5sum_path, MD5SUM_DEVICE_PATH) |
|
593 assert _HasAdbPushSucceeded(self._adb.SendCommand(command)) |
|
594 self._md5sum_path = md5sum_path |
|
595 |
|
596 self._pushed_files.append(device_path) |
|
597 hashes_on_device = _ComputeFileListHash( |
|
598 self.RunShellCommand(MD5SUM_DEVICE_PATH + ' ' + device_path)) |
|
599 assert os.path.exists(local_path), 'Local path not found %s' % local_path |
|
600 hashes_on_host = _ComputeFileListHash( |
|
601 subprocess.Popen( |
|
602 '%s_host %s' % (self._md5sum_path, local_path), |
|
603 stdout=subprocess.PIPE, shell=True).stdout) |
|
604 if hashes_on_device == hashes_on_host: |
|
605 return |
|
606 |
|
607 # They don't match, so remove everything first and then create it. |
|
608 if os.path.isdir(local_path): |
|
609 self.RunShellCommand('rm -r %s' % device_path, timeout_time=2*60) |
|
610 self.RunShellCommand('mkdir -p %s' % device_path) |
|
611 |
|
612 # NOTE: We can't use adb_interface.Push() because it hardcodes a timeout of |
|
613 # 60 seconds which isn't sufficient for a lot of users of this method. |
|
614 push_command = 'push %s %s' % (local_path, device_path) |
|
615 logging.info('>>> $' + push_command) |
|
616 output = self._adb.SendCommand(push_command, timeout_time=30*60) |
|
617 assert _HasAdbPushSucceeded(output) |
|
618 |
|
619 |
|
620 def GetFileContents(self, filename, log_result=False): |
|
621 """Gets contents from the file specified by |filename|.""" |
|
622 return self.RunShellCommand('if [ -f "' + filename + '" ]; then cat "' + |
|
623 filename + '"; fi', log_result=log_result) |
|
624 |
|
625 def SetFileContents(self, filename, contents): |
|
626 """Writes |contents| to the file specified by |filename|.""" |
|
627 with tempfile.NamedTemporaryFile() as f: |
|
628 f.write(contents) |
|
629 f.flush() |
|
630 self._adb.Push(f.name, filename) |
|
631 |
|
632 def RemovePushedFiles(self): |
|
633 """Removes all files pushed with PushIfNeeded() from the device.""" |
|
634 for p in self._pushed_files: |
|
635 self.RunShellCommand('rm -r %s' % p, timeout_time=2*60) |
|
636 |
|
637 def ListPathContents(self, path): |
|
638 """Lists files in all subdirectories of |path|. |
|
639 |
|
640 Args: |
|
641 path: The path to list. |
|
642 |
|
643 Returns: |
|
644 A dict of {"name": (size, lastmod), ...}. |
|
645 """ |
|
646 # Example output: |
|
647 # /foo/bar: |
|
648 # -rw-r----- 1 user group 102 2011-05-12 12:29:54.131623387 +0100 baz.txt |
|
649 re_file = re.compile('^-(?P<perms>[^\s]+)\s+' |
|
650 '(?P<user>[^\s]+)\s+' |
|
651 '(?P<group>[^\s]+)\s+' |
|
652 '(?P<size>[^\s]+)\s+' |
|
653 '(?P<date>[^\s]+)\s+' |
|
654 '(?P<time>[^\s]+)\s+' |
|
655 '(?P<filename>[^\s]+)$') |
|
656 return _GetFilesFromRecursiveLsOutput( |
|
657 path, self.RunShellCommand('ls -lR %s' % path), re_file, |
|
658 self._device_utc_offset) |
|
659 |
|
660 |
|
661 def SetJavaAssertsEnabled(self, enable): |
|
662 """Sets or removes the device java assertions property. |
|
663 |
|
664 Args: |
|
665 enable: If True the property will be set. |
|
666 |
|
667 Returns: |
|
668 True if the file was modified (reboot is required for it to take effect). |
|
669 """ |
|
670 # First ensure the desired property is persisted. |
|
671 temp_props_file = tempfile.NamedTemporaryFile() |
|
672 properties = '' |
|
673 if self._adb.Pull(LOCAL_PROPERTIES_PATH, temp_props_file.name): |
|
674 properties = file(temp_props_file.name).read() |
|
675 re_search = re.compile(r'^\s*' + re.escape(JAVA_ASSERT_PROPERTY) + |
|
676 r'\s*=\s*all\s*$', re.MULTILINE) |
|
677 if enable != bool(re.search(re_search, properties)): |
|
678 re_replace = re.compile(r'^\s*' + re.escape(JAVA_ASSERT_PROPERTY) + |
|
679 r'\s*=\s*\w+\s*$', re.MULTILINE) |
|
680 properties = re.sub(re_replace, '', properties) |
|
681 if enable: |
|
682 properties += '\n%s=all\n' % JAVA_ASSERT_PROPERTY |
|
683 |
|
684 file(temp_props_file.name, 'w').write(properties) |
|
685 self._adb.Push(temp_props_file.name, LOCAL_PROPERTIES_PATH) |
|
686 |
|
687 # Next, check the current runtime value is what we need, and |
|
688 # if not, set it and report that a reboot is required. |
|
689 was_set = 'all' in self.RunShellCommand('getprop ' + JAVA_ASSERT_PROPERTY) |
|
690 if was_set == enable: |
|
691 return False |
|
692 |
|
693 self.RunShellCommand('setprop %s "%s"' % (JAVA_ASSERT_PROPERTY, |
|
694 enable and 'all' or '')) |
|
695 return True |
|
696 |
|
697 def GetBuildId(self): |
|
698 """Returns the build ID of the system (e.g. JRM79C).""" |
|
699 build_id = self.RunShellCommand('getprop ro.build.id')[0] |
|
700 assert build_id |
|
701 return build_id |
|
702 |
|
703 def GetBuildType(self): |
|
704 """Returns the build type of the system (e.g. eng).""" |
|
705 build_type = self.RunShellCommand('getprop ro.build.type')[0] |
|
706 assert build_type |
|
707 return build_type |
|
708 |
|
709 def StartMonitoringLogcat(self, clear=True, timeout=10, logfile=None, |
|
710 filters=None): |
|
711 """Starts monitoring the output of logcat, for use with WaitForLogMatch. |
|
712 |
|
713 Args: |
|
714 clear: If True the existing logcat output will be cleared, to avoiding |
|
715 matching historical output lurking in the log. |
|
716 timeout: How long WaitForLogMatch will wait for the given match |
|
717 filters: A list of logcat filters to be used. |
|
718 """ |
|
719 if clear: |
|
720 self.RunShellCommand('logcat -c') |
|
721 args = [] |
|
722 if self._adb._target_arg: |
|
723 args += shlex.split(self._adb._target_arg) |
|
724 args += ['logcat', '-v', 'threadtime'] |
|
725 if filters: |
|
726 args.extend(filters) |
|
727 else: |
|
728 args.append('*:v') |
|
729 |
|
730 if logfile: |
|
731 logfile = NewLineNormalizer(logfile) |
|
732 |
|
733 # Spawn logcat and syncronize with it. |
|
734 for _ in range(4): |
|
735 self._logcat = pexpect.spawn('adb', args, timeout=timeout, |
|
736 logfile=logfile) |
|
737 self.RunShellCommand('log startup_sync') |
|
738 if self._logcat.expect(['startup_sync', pexpect.EOF, |
|
739 pexpect.TIMEOUT]) == 0: |
|
740 break |
|
741 self._logcat.close(force=True) |
|
742 else: |
|
743 logging.critical('Error reading from logcat: ' + str(self._logcat.match)) |
|
744 sys.exit(1) |
|
745 |
|
746 def GetMonitoredLogCat(self): |
|
747 """Returns an "adb logcat" command as created by pexpected.spawn.""" |
|
748 if not self._logcat: |
|
749 self.StartMonitoringLogcat(clear=False) |
|
750 return self._logcat |
|
751 |
|
752 def WaitForLogMatch(self, success_re, error_re, clear=False): |
|
753 """Blocks until a matching line is logged or a timeout occurs. |
|
754 |
|
755 Args: |
|
756 success_re: A compiled re to search each line for. |
|
757 error_re: A compiled re which, if found, terminates the search for |
|
758 |success_re|. If None is given, no error condition will be detected. |
|
759 clear: If True the existing logcat output will be cleared, defaults to |
|
760 false. |
|
761 |
|
762 Raises: |
|
763 pexpect.TIMEOUT upon the timeout specified by StartMonitoringLogcat(). |
|
764 |
|
765 Returns: |
|
766 The re match object if |success_re| is matched first or None if |error_re| |
|
767 is matched first. |
|
768 """ |
|
769 logging.info('<<< Waiting for logcat:' + str(success_re.pattern)) |
|
770 t0 = time.time() |
|
771 while True: |
|
772 if not self._logcat: |
|
773 self.StartMonitoringLogcat(clear) |
|
774 try: |
|
775 while True: |
|
776 # Note this will block for upto the timeout _per log line_, so we need |
|
777 # to calculate the overall timeout remaining since t0. |
|
778 time_remaining = t0 + self._logcat.timeout - time.time() |
|
779 if time_remaining < 0: raise pexpect.TIMEOUT(self._logcat) |
|
780 self._logcat.expect(PEXPECT_LINE_RE, timeout=time_remaining) |
|
781 line = self._logcat.match.group(1) |
|
782 if error_re: |
|
783 error_match = error_re.search(line) |
|
784 if error_match: |
|
785 return None |
|
786 success_match = success_re.search(line) |
|
787 if success_match: |
|
788 return success_match |
|
789 logging.info('<<< Skipped Logcat Line:' + str(line)) |
|
790 except pexpect.TIMEOUT: |
|
791 raise pexpect.TIMEOUT( |
|
792 'Timeout (%ds) exceeded waiting for pattern "%s" (tip: use -vv ' |
|
793 'to debug)' % |
|
794 (self._logcat.timeout, success_re.pattern)) |
|
795 except pexpect.EOF: |
|
796 # It seems that sometimes logcat can end unexpectedly. This seems |
|
797 # to happen during Chrome startup after a reboot followed by a cache |
|
798 # clean. I don't understand why this happens, but this code deals with |
|
799 # getting EOF in logcat. |
|
800 logging.critical('Found EOF in adb logcat. Restarting...') |
|
801 # Rerun spawn with original arguments. Note that self._logcat.args[0] is |
|
802 # the path of adb, so we don't want it in the arguments. |
|
803 self._logcat = pexpect.spawn('adb', |
|
804 self._logcat.args[1:], |
|
805 timeout=self._logcat.timeout, |
|
806 logfile=self._logcat.logfile) |
|
807 |
|
808 def StartRecordingLogcat(self, clear=True, filters=['*:v']): |
|
809 """Starts recording logcat output to eventually be saved as a string. |
|
810 |
|
811 This call should come before some series of tests are run, with either |
|
812 StopRecordingLogcat or SearchLogcatRecord following the tests. |
|
813 |
|
814 Args: |
|
815 clear: True if existing log output should be cleared. |
|
816 filters: A list of logcat filters to be used. |
|
817 """ |
|
818 if clear: |
|
819 self._adb.SendCommand('logcat -c') |
|
820 logcat_command = 'adb %s logcat -v threadtime %s' % (self._adb._target_arg, |
|
821 ' '.join(filters)) |
|
822 self.logcat_process = subprocess.Popen(logcat_command, shell=True, |
|
823 stdout=subprocess.PIPE) |
|
824 |
|
825 def StopRecordingLogcat(self): |
|
826 """Stops an existing logcat recording subprocess and returns output. |
|
827 |
|
828 Returns: |
|
829 The logcat output as a string or an empty string if logcat was not |
|
830 being recorded at the time. |
|
831 """ |
|
832 if not self.logcat_process: |
|
833 return '' |
|
834 # Cannot evaluate directly as 0 is a possible value. |
|
835 # Better to read the self.logcat_process.stdout before killing it, |
|
836 # Otherwise the communicate may return incomplete output due to pipe break. |
|
837 if self.logcat_process.poll() is None: |
|
838 self.logcat_process.kill() |
|
839 (output, _) = self.logcat_process.communicate() |
|
840 self.logcat_process = None |
|
841 return output |
|
842 |
|
843 def SearchLogcatRecord(self, record, message, thread_id=None, proc_id=None, |
|
844 log_level=None, component=None): |
|
845 """Searches the specified logcat output and returns results. |
|
846 |
|
847 This method searches through the logcat output specified by record for a |
|
848 certain message, narrowing results by matching them against any other |
|
849 specified criteria. It returns all matching lines as described below. |
|
850 |
|
851 Args: |
|
852 record: A string generated by Start/StopRecordingLogcat to search. |
|
853 message: An output string to search for. |
|
854 thread_id: The thread id that is the origin of the message. |
|
855 proc_id: The process that is the origin of the message. |
|
856 log_level: The log level of the message. |
|
857 component: The name of the component that would create the message. |
|
858 |
|
859 Returns: |
|
860 A list of dictionaries represeting matching entries, each containing keys |
|
861 thread_id, proc_id, log_level, component, and message. |
|
862 """ |
|
863 if thread_id: |
|
864 thread_id = str(thread_id) |
|
865 if proc_id: |
|
866 proc_id = str(proc_id) |
|
867 results = [] |
|
868 reg = re.compile('(\d+)\s+(\d+)\s+([A-Z])\s+([A-Za-z]+)\s*:(.*)$', |
|
869 re.MULTILINE) |
|
870 log_list = reg.findall(record) |
|
871 for (tid, pid, log_lev, comp, msg) in log_list: |
|
872 if ((not thread_id or thread_id == tid) and |
|
873 (not proc_id or proc_id == pid) and |
|
874 (not log_level or log_level == log_lev) and |
|
875 (not component or component == comp) and msg.find(message) > -1): |
|
876 match = dict({'thread_id': tid, 'proc_id': pid, |
|
877 'log_level': log_lev, 'component': comp, |
|
878 'message': msg}) |
|
879 results.append(match) |
|
880 return results |
|
881 |
|
882 def ExtractPid(self, process_name): |
|
883 """Extracts Process Ids for a given process name from Android Shell. |
|
884 |
|
885 Args: |
|
886 process_name: name of the process on the device. |
|
887 |
|
888 Returns: |
|
889 List of all the process ids (as strings) that match the given name. |
|
890 If the name of a process exactly matches the given name, the pid of |
|
891 that process will be inserted to the front of the pid list. |
|
892 """ |
|
893 pids = [] |
|
894 for line in self.RunShellCommand('ps', log_result=False): |
|
895 data = line.split() |
|
896 try: |
|
897 if process_name in data[-1]: # name is in the last column |
|
898 if process_name == data[-1]: |
|
899 pids.insert(0, data[1]) # PID is in the second column |
|
900 else: |
|
901 pids.append(data[1]) |
|
902 except IndexError: |
|
903 pass |
|
904 return pids |
|
905 |
|
906 def GetIoStats(self): |
|
907 """Gets cumulative disk IO stats since boot (for all processes). |
|
908 |
|
909 Returns: |
|
910 Dict of {num_reads, num_writes, read_ms, write_ms} or None if there |
|
911 was an error. |
|
912 """ |
|
913 for line in self.GetFileContents('/proc/diskstats', log_result=False): |
|
914 stats = io_stats_parser.ParseIoStatsLine(line) |
|
915 if stats.device == 'mmcblk0': |
|
916 return { |
|
917 'num_reads': stats.num_reads_issued, |
|
918 'num_writes': stats.num_writes_completed, |
|
919 'read_ms': stats.ms_spent_reading, |
|
920 'write_ms': stats.ms_spent_writing, |
|
921 } |
|
922 logging.warning('Could not find disk IO stats.') |
|
923 return None |
|
924 |
|
925 def GetMemoryUsageForPid(self, pid): |
|
926 """Returns the memory usage for given pid. |
|
927 |
|
928 Args: |
|
929 pid: The pid number of the specific process running on device. |
|
930 |
|
931 Returns: |
|
932 A tuple containg: |
|
933 [0]: Dict of {metric:usage_kb}, for the process which has specified pid. |
|
934 The metric keys which may be included are: Size, Rss, Pss, Shared_Clean, |
|
935 Shared_Dirty, Private_Clean, Private_Dirty, Referenced, Swap, |
|
936 KernelPageSize, MMUPageSize, Nvidia (tablet only). |
|
937 [1]: Detailed /proc/[PID]/smaps information. |
|
938 """ |
|
939 usage_dict = collections.defaultdict(int) |
|
940 smaps = collections.defaultdict(dict) |
|
941 current_smap = '' |
|
942 for line in self.GetFileContents('/proc/%s/smaps' % pid, log_result=False): |
|
943 items = line.split() |
|
944 # See man 5 proc for more details. The format is: |
|
945 # address perms offset dev inode pathname |
|
946 if len(items) > 5: |
|
947 current_smap = ' '.join(items[5:]) |
|
948 elif len(items) > 3: |
|
949 current_smap = ' '.join(items[3:]) |
|
950 match = re.match(MEMORY_INFO_RE, line) |
|
951 if match: |
|
952 key = match.group('key') |
|
953 usage_kb = int(match.group('usage_kb')) |
|
954 usage_dict[key] += usage_kb |
|
955 if key not in smaps[current_smap]: |
|
956 smaps[current_smap][key] = 0 |
|
957 smaps[current_smap][key] += usage_kb |
|
958 if not usage_dict or not any(usage_dict.values()): |
|
959 # Presumably the process died between ps and calling this method. |
|
960 logging.warning('Could not find memory usage for pid ' + str(pid)) |
|
961 |
|
962 for line in self.GetFileContents('/d/nvmap/generic-0/clients', |
|
963 log_result=False): |
|
964 match = re.match(NVIDIA_MEMORY_INFO_RE, line) |
|
965 if match and match.group('pid') == pid: |
|
966 usage_bytes = int(match.group('usage_bytes')) |
|
967 usage_dict['Nvidia'] = int(round(usage_bytes / 1000.0)) # kB |
|
968 break |
|
969 |
|
970 return (usage_dict, smaps) |
|
971 |
|
972 def GetMemoryUsageForPackage(self, package): |
|
973 """Returns the memory usage for all processes whose name contains |pacakge|. |
|
974 |
|
975 Args: |
|
976 package: A string holding process name to lookup pid list for. |
|
977 |
|
978 Returns: |
|
979 A tuple containg: |
|
980 [0]: Dict of {metric:usage_kb}, summed over all pids associated with |
|
981 |name|. |
|
982 The metric keys which may be included are: Size, Rss, Pss, Shared_Clean, |
|
983 Shared_Dirty, Private_Clean, Private_Dirty, Referenced, Swap, |
|
984 KernelPageSize, MMUPageSize, Nvidia (tablet only). |
|
985 [1]: a list with detailed /proc/[PID]/smaps information. |
|
986 """ |
|
987 usage_dict = collections.defaultdict(int) |
|
988 pid_list = self.ExtractPid(package) |
|
989 smaps = collections.defaultdict(dict) |
|
990 |
|
991 for pid in pid_list: |
|
992 usage_dict_per_pid, smaps_per_pid = self.GetMemoryUsageForPid(pid) |
|
993 smaps[pid] = smaps_per_pid |
|
994 for (key, value) in usage_dict_per_pid.items(): |
|
995 usage_dict[key] += value |
|
996 |
|
997 return usage_dict, smaps |
|
998 |
|
999 def ProcessesUsingDevicePort(self, device_port): |
|
1000 """Lists processes using the specified device port on loopback interface. |
|
1001 |
|
1002 Args: |
|
1003 device_port: Port on device we want to check. |
|
1004 |
|
1005 Returns: |
|
1006 A list of (pid, process_name) tuples using the specified port. |
|
1007 """ |
|
1008 tcp_results = self.RunShellCommand('cat /proc/net/tcp', log_result=False) |
|
1009 tcp_address = '0100007F:%04X' % device_port |
|
1010 pids = [] |
|
1011 for single_connect in tcp_results: |
|
1012 connect_results = single_connect.split() |
|
1013 # Column 1 is the TCP port, and Column 9 is the inode of the socket |
|
1014 if connect_results[1] == tcp_address: |
|
1015 socket_inode = connect_results[9] |
|
1016 socket_name = 'socket:[%s]' % socket_inode |
|
1017 lsof_results = self.RunShellCommand('lsof', log_result=False) |
|
1018 for single_process in lsof_results: |
|
1019 process_results = single_process.split() |
|
1020 # Ignore the line if it has less than nine columns in it, which may |
|
1021 # be the case when a process stops while lsof is executing. |
|
1022 if len(process_results) <= 8: |
|
1023 continue |
|
1024 # Column 0 is the executable name |
|
1025 # Column 1 is the pid |
|
1026 # Column 8 is the Inode in use |
|
1027 if process_results[8] == socket_name: |
|
1028 pids.append((int(process_results[1]), process_results[0])) |
|
1029 break |
|
1030 logging.info('PidsUsingDevicePort: %s', pids) |
|
1031 return pids |
|
1032 |
|
1033 def FileExistsOnDevice(self, file_name): |
|
1034 """Checks whether the given file exists on the device. |
|
1035 |
|
1036 Args: |
|
1037 file_name: Full path of file to check. |
|
1038 |
|
1039 Returns: |
|
1040 True if the file exists, False otherwise. |
|
1041 """ |
|
1042 assert '"' not in file_name, 'file_name cannot contain double quotes' |
|
1043 status = self._adb.SendShellCommand( |
|
1044 '\'test -e "%s"; echo $?\'' % (file_name)) |
|
1045 if 'test: not found' not in status: |
|
1046 return int(status) == 0 |
|
1047 |
|
1048 status = self._adb.SendShellCommand( |
|
1049 '\'ls "%s" >/dev/null 2>&1; echo $?\'' % (file_name)) |
|
1050 return int(status) == 0 |
|
1051 |
|
1052 |
|
1053 class NewLineNormalizer(object): |
|
1054 """A file-like object to normalize EOLs to '\n'. |
|
1055 |
|
1056 Pexpect runs adb within a pseudo-tty device (see |
|
1057 http://www.noah.org/wiki/pexpect), so any '\n' printed by adb is written |
|
1058 as '\r\n' to the logfile. Since adb already uses '\r\n' to terminate |
|
1059 lines, the log ends up having '\r\r\n' at the end of each line. This |
|
1060 filter replaces the above with a single '\n' in the data stream. |
|
1061 """ |
|
1062 def __init__(self, output): |
|
1063 self._output = output |
|
1064 |
|
1065 def write(self, data): |
|
1066 data = data.replace('\r\r\n', '\n') |
|
1067 self._output.write(data) |
|
1068 |
|
1069 def flush(self): |
|
1070 self._output.flush() |
|
1071 |