diff options
Diffstat (limited to 'systrace/catapult/devil/devil/android/device_utils.py')
-rw-r--r-- | systrace/catapult/devil/devil/android/device_utils.py | 202 |
1 files changed, 169 insertions, 33 deletions
diff --git a/systrace/catapult/devil/devil/android/device_utils.py b/systrace/catapult/devil/devil/android/device_utils.py index 3d8e203..7ba1b51 100644 --- a/systrace/catapult/devil/devil/android/device_utils.py +++ b/systrace/catapult/devil/devil/android/device_utils.py @@ -76,13 +76,22 @@ _PERMISSIONS_BLACKLIST = [ 'android.permission.ACCESS_LOCATION_EXTRA_COMMANDS', 'android.permission.ACCESS_MOCK_LOCATION', 'android.permission.ACCESS_NETWORK_STATE', + 'android.permission.ACCESS_NOTIFICATION_POLICY', 'android.permission.ACCESS_WIFI_STATE', 'android.permission.AUTHENTICATE_ACCOUNTS', 'android.permission.BLUETOOTH', 'android.permission.BLUETOOTH_ADMIN', + 'android.permission.BROADCAST_STICKY', + 'android.permission.CHANGE_NETWORK_STATE', + 'android.permission.CHANGE_WIFI_MULTICAST_STATE', + 'android.permission.CHANGE_WIFI_STATE', 'android.permission.DISABLE_KEYGUARD', 'android.permission.DOWNLOAD_WITHOUT_NOTIFICATION', + 'android.permission.EXPAND_STATUS_BAR', + 'android.permission.GET_PACKAGE_SIZE', + 'android.permission.INSTALL_SHORTCUT', 'android.permission.INTERNET', + 'android.permission.KILL_BACKGROUND_PROCESSES', 'android.permission.MANAGE_ACCOUNTS', 'android.permission.MODIFY_AUDIO_SETTINGS', 'android.permission.NFC', @@ -90,8 +99,16 @@ _PERMISSIONS_BLACKLIST = [ 'android.permission.READ_SYNC_STATS', 'android.permission.RECEIVE_BOOT_COMPLETED', 'android.permission.RECORD_VIDEO', + 'android.permission.REORDER_TASKS', + 'android.permission.REQUEST_INSTALL_PACKAGES', 'android.permission.RUN_INSTRUMENTATION', + 'android.permission.SET_ALARM', + 'android.permission.SET_TIME_ZONE', + 'android.permission.SET_WALLPAPER', + 'android.permission.SET_WALLPAPER_HINTS', + 'android.permission.TRANSMIT_IR', 'android.permission.USE_CREDENTIALS', + 'android.permission.USE_FINGERPRINT', 'android.permission.VIBRATE', 'android.permission.WAKE_LOCK', 'android.permission.WRITE_SYNC_SETTINGS', @@ -150,6 +167,17 @@ _FILE_MODE_SPECIAL = [ ('s', stat.S_ISGID), ('t', stat.S_ISVTX), ] +_SELINUX_MODE = { + 'enforcing': True, + 'permissive': False, + 'disabled': None +} +# Some devices require different logic for checking if root is necessary +_SPECIAL_ROOT_DEVICE_LIST = [ + 'marlin', + 'sailfish', +] + @decorators.WithExplicitTimeoutAndRetries( _DEFAULT_TIMEOUT, _DEFAULT_RETRIES) @@ -367,6 +395,8 @@ class DeviceUtils(object): DeviceUnreachableError on missing device. """ try: + if self.product_name in _SPECIAL_ROOT_DEVICE_LIST: + return self.GetProp('service.adb.root') == '1' self.RunShellCommand(['ls', '/root'], check_return=True) return True except device_errors.AdbCommandFailedError: @@ -390,9 +420,14 @@ class DeviceUtils(object): DeviceUnreachableError on missing device. """ if 'needs_su' not in self._cache: + cmd = '%s && ! ls /root' % self._Su('ls /root') + if self.product_name in _SPECIAL_ROOT_DEVICE_LIST: + if self.HasRoot(): + self._cache['needs_su'] = False + return False + cmd = 'which which && which su' try: - self.RunShellCommand( - '%s && ! ls /root' % self._Su('ls /root'), check_return=True, + self.RunShellCommand(cmd, shell=True, check_return=True, timeout=self._default_timeout if timeout is DEFAULT else timeout, retries=self._default_retries if retries is DEFAULT else retries) self._cache['needs_su'] = True @@ -400,6 +435,7 @@ class DeviceUtils(object): self._cache['needs_su'] = False return self._cache['needs_su'] + def _Su(self, command): if self.build_version_sdk >= version_codes.MARSHMALLOW: return 'su 0 %s' % command @@ -799,24 +835,27 @@ class DeviceUtils(object): device_serial=self.serial) @decorators.WithTimeoutAndRetriesFromInstance() - def RunShellCommand(self, cmd, check_return=False, cwd=None, env=None, - run_as=None, as_root=False, single_line=False, + def RunShellCommand(self, cmd, shell=False, check_return=False, cwd=None, + env=None, run_as=None, as_root=False, single_line=False, large_output=False, raw_output=False, timeout=None, retries=None): """Run an ADB shell command. - The command to run |cmd| should be a sequence of program arguments or else - a single string. + The command to run |cmd| should be a sequence of program arguments + (preferred) or a single string with a shell script to run. When |cmd| is a sequence, it is assumed to contain the name of the command to run followed by its arguments. In this case, arguments are passed to the - command exactly as given, without any further processing by the shell. This - allows to easily pass arguments containing spaces or special characters - without having to worry about getting quoting right. Whenever possible, it - is recomended to pass |cmd| as a sequence. + command exactly as given, preventing any further processing by the shell. + This allows callers to easily pass arguments with spaces or special + characters without having to worry about quoting rules. Whenever possible, + it is recomended to pass |cmd| as a sequence. - When |cmd| is given as a string, it will be interpreted and run by the - shell on the device. + When |cmd| is passed as a single string, |shell| should be set to True. + The command will be interpreted and run by the shell on the device, + allowing the use of shell features such as pipes, wildcards, or variables. + Failing to set shell=True will issue a warning, but this will be changed + to a hard failure in the future (see: catapult:#3242). This behaviour is consistent with that of command runners in cmd_helper as well as Python's own subprocess.Popen. @@ -825,8 +864,9 @@ class DeviceUtils(object): have switched to the new behaviour. Args: - cmd: A string with the full command to run on the device, or a sequence - containing the command and its arguments. + cmd: A sequence containing the command to run and its arguments, or a + string with a shell script to run (should also set shell=True). + shell: A boolean indicating whether shell features may be used in |cmd|. check_return: A boolean indicating whether or not the return code should be checked. cwd: The device directory in which the command should be run. @@ -906,7 +946,13 @@ class DeviceUtils(object): else: raise - if not isinstance(cmd, basestring): + if isinstance(cmd, basestring): + if not shell: + logging.warning( + 'The command to run should preferably be passed as a sequence of' + ' args. If shell features are needed (pipes, wildcards, variables)' + ' clients should explicitly set shell=True.') + else: cmd = ' '.join(cmd_helper.SingleQuote(s) for s in cmd) if env: env = ' '.join(env_quote(k, v) for k, v in env.iteritems()) @@ -941,7 +987,7 @@ class DeviceUtils(object): PIPESTATUS_LEADER = 'PIPESTATUS: ' script += '; echo "%s${PIPESTATUS[@]}"' % PIPESTATUS_LEADER - kwargs['check_return'] = True + kwargs.update(shell=True, check_return=True) output = self.RunShellCommand(script, **kwargs) pipestatus_line = output[-1] @@ -1073,7 +1119,7 @@ class DeviceUtils(object): package = component.split('/')[0] shell_snippet = 'p=%s;%s' % (package, cmd_helper.ShrinkToSnippet(cmd, 'p', package)) - return self.RunShellCommand(shell_snippet, check_return=True, + return self.RunShellCommand(shell_snippet, shell=True, check_return=True, large_output=True) @decorators.WithTimeoutAndRetriesFromInstance() @@ -1145,7 +1191,7 @@ class DeviceUtils(object): DeviceUnreachableError on missing device. """ cmd = 'p=%s;if [[ "$(ps)" = *$p* ]]; then am force-stop $p; fi' - self.RunShellCommand(cmd % package, check_return=True) + self.RunShellCommand(cmd % package, shell=True, check_return=True) @decorators.WithTimeoutAndRetriesFromInstance() def ClearApplicationState( @@ -1451,7 +1497,7 @@ class DeviceUtils(object): quoted_dirs = ' '.join(cmd_helper.SingleQuote(d) for d in dirs) self.RunShellCommand( 'unzip %s&&chmod -R 777 %s' % (device_temp.name, quoted_dirs), - as_root=True, + shell=True, as_root=True, env={'PATH': '%s:$PATH' % install_commands.BIN_DIR}, check_return=True) finally: @@ -1496,8 +1542,11 @@ class DeviceUtils(object): paths = device_paths if isinstance(paths, basestring): paths = (paths,) - condition = ' -a '.join('-e %s' % cmd_helper.SingleQuote(p) for p in paths) - cmd = 'test %s' % condition + if not paths: + return True + cmd = ['test', '-e', paths[0]] + for p in paths[1:]: + cmd.extend(['-a', '-e', p]) try: self.RunShellCommand(cmd, as_root=as_root, check_return=True, timeout=timeout, retries=retries) @@ -1603,7 +1652,7 @@ class DeviceUtils(object): cmd = 'SRC=%s DEST=%s;cp "$SRC" "$DEST" && chmod 666 "$DEST"' % ( cmd_helper.SingleQuote(device_path), cmd_helper.SingleQuote(device_temp.name)) - self.RunShellCommand(cmd, as_root=True, check_return=True) + self.RunShellCommand(cmd, shell=True, as_root=True, check_return=True) return self._ReadFileWithPull(device_temp.name) else: return self._ReadFileWithPull(device_path) @@ -1641,7 +1690,7 @@ class DeviceUtils(object): # a shell command rather than pushing a file. cmd = 'echo -n %s > %s' % (cmd_helper.SingleQuote(contents), cmd_helper.SingleQuote(device_path)) - self.RunShellCommand(cmd, as_root=as_root, check_return=True) + self.RunShellCommand(cmd, shell=True, as_root=as_root, check_return=True) elif as_root and self.NeedsSU(): # Adb does not allow to "push with su", so we first push to a temp file # on a safe location, and then copy it to the desired location with su. @@ -1993,7 +2042,8 @@ class DeviceUtils(object): 'echo "%s">$c &&' % token + 'getprop' ) - output = self.RunShellCommand(cmd, check_return=True, large_output=True) + output = self.RunShellCommand( + cmd, shell=True, check_return=True, large_output=True) # Error-checking for this existing is done in GetExternalStoragePath(). self._cache['external_storage'] = output[0] self._cache['prev_token'] = output[1] @@ -2091,13 +2141,14 @@ class DeviceUtils(object): return self.GetProp('ro.product.cpu.abi', cache=True) @decorators.WithTimeoutAndRetriesFromInstance() - def GetPids(self, process_name, timeout=None, retries=None): - """Returns the PIDs of processes with the given name. + def GetPids(self, process_name=None, timeout=None, retries=None): + """Returns the PIDs of processes containing the given name as substring. Note that the |process_name| is often the package name. Args: process_name: A string containing the process name to get the PIDs for. + If missing returns PIDs for all processes. timeout: timeout in seconds retries: number of retries @@ -2111,8 +2162,17 @@ class DeviceUtils(object): """ procs_pids = collections.defaultdict(list) try: - ps_output = self._RunPipedShellCommand( - 'ps | grep -F %s' % cmd_helper.SingleQuote(process_name)) + ps_cmd = 'ps' + # ps behavior was changed in Android above N, http://crbug.com/686716 + if (self.build_version_sdk >= version_codes.NOUGAT_MR1 + and self.build_id[0] > 'N'): + ps_cmd = 'ps -e' + if process_name: + ps_output = self._RunPipedShellCommand( + '%s | grep -F %s' % (ps_cmd, cmd_helper.SingleQuote(process_name))) + else: + ps_output = self.RunShellCommand( + ps_cmd.split(), check_return=True, large_output=True) except device_errors.AdbShellCommandFailedError as e: if e.status and isinstance(e.status, list) and not e.status[0]: # If ps succeeded but grep failed, there were no processes with the @@ -2121,16 +2181,91 @@ class DeviceUtils(object): else: raise + process_name = process_name or '' for line in ps_output: try: ps_data = line.split() - if process_name in ps_data[-1]: - pid, process = ps_data[1], ps_data[-1] + pid, process = ps_data[1], ps_data[-1] + if process_name in process and pid != 'PID': procs_pids[process].append(pid) except IndexError: pass return procs_pids + def GetApplicationPids(self, process_name, at_most_one=False, **kwargs): + """Returns the PID or PIDs of a given process name. + + Note that the |process_name|, often the package name, must match exactly. + + Args: + process_name: A string containing the process name to get the PIDs for. + at_most_one: A boolean indicating that at most one PID is expected to + be found. + timeout: timeout in seconds + retries: number of retries + + Returns: + A list of the PIDs for the named process. If at_most_one=True returns + the single PID found or None otherwise. + + Raises: + CommandFailedError if at_most_one=True and more than one PID is found + for the named process. + CommandTimeoutError on timeout. + DeviceUnreachableError on missing device. + """ + pids = self.GetPids(process_name, **kwargs).get(process_name, []) + if at_most_one: + if len(pids) > 1: + raise device_errors.CommandFailedError( + 'Expected a single process but found PIDs: %s.' % ', '.join(pids), + device_serial=str(self)) + return pids[0] if pids else None + else: + return pids + + @decorators.WithTimeoutAndRetriesFromInstance() + def GetEnforce(self, timeout=None, retries=None): + """Get the current mode of SELinux. + + Args: + timeout: timeout in seconds + retries: number of retries + + Returns: + True (enforcing), False (permissive), or None (disabled). + + Raises: + CommandFailedError on failure. + CommandTimeoutError on timeout. + DeviceUnreachableError on missing device. + """ + output = self.RunShellCommand( + ['getenforce'], check_return=True, single_line=True).lower() + if output not in _SELINUX_MODE: + raise device_errors.CommandFailedError( + 'Unexpected getenforce output: %s' % output) + return _SELINUX_MODE[output] + + @decorators.WithTimeoutAndRetriesFromInstance() + def SetEnforce(self, enabled, timeout=None, retries=None): + """Modify the mode SELinux is running in. + + Args: + enabled: a boolean indicating whether to put SELinux in encorcing mode + (if True), or permissive mode (otherwise). + timeout: timeout in seconds + retries: number of retries + + Raises: + CommandFailedError on failure. + CommandTimeoutError on timeout. + DeviceUnreachableError on missing device. + """ + self.RunShellCommand( + ['setenforce', '1' if int(enabled) else '0'], as_root=True, + check_return=True) + @decorators.WithTimeoutAndRetriesFromInstance() def TakeScreenshot(self, host_path=None, timeout=None, retries=None): """Takes a screenshot of the device. @@ -2440,7 +2575,8 @@ class DeviceUtils(object): logger.info('Restarting adbd on device.') with device_temp_file.DeviceTempFile(self.adb, suffix='.sh') as script: self.WriteFile(script.name, _RESTART_ADBD_SCRIPT) - self.RunShellCommand(['source', script.name], as_root=True) + self.RunShellCommand( + ['source', script.name], check_return=True, as_root=True) self.adb.WaitForDevice() @decorators.WithTimeoutAndRetriesFromInstance() @@ -2456,7 +2592,7 @@ class DeviceUtils(object): permissions.append('android.permission.READ_EXTERNAL_STORAGE') cmd = '&&'.join('pm grant %s %s' % (package, p) for p in permissions) if cmd: - output = self.RunShellCommand(cmd, check_return=True) + output = self.RunShellCommand(cmd, shell=True, check_return=True) if output: logger.warning('Possible problem when granting permissions. Blacklist ' 'may need to be updated.') @@ -2509,5 +2645,5 @@ class DeviceUtils(object): if screen_test(): logger.info('Screen already in expected state.') return - self.RunShellCommand('input keyevent 26') + self.SendKeyEvent(keyevent.KEYCODE_POWER) timeout_retry.WaitFor(screen_test, wait_period=1) |