• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2#
3#   Copyright 2016 - The Android Open Source Project
4#
5#   Licensed under the Apache License, Version 2.0 (the "License");
6#   you may not use this file except in compliance with the License.
7#   You may obtain a copy of the License at
8#
9#       http://www.apache.org/licenses/LICENSE-2.0
10#
11#   Unless required by applicable law or agreed to in writing, software
12#   distributed under the License is distributed on an "AS IS" BASIS,
13#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14#   See the License for the specific language governing permissions and
15#   limitations under the License.
16
17from builtins import str
18
19import logging
20import re
21import shellescape
22
23from acts import error
24from acts.libs.proc import job
25
26DEFAULT_ADB_TIMEOUT = 60
27DEFAULT_ADB_PULL_TIMEOUT = 180
28# Uses a regex to be backwards compatible with previous versions of ADB
29# (N and above add the serial to the error msg).
30DEVICE_NOT_FOUND_REGEX = re.compile('^error: device (?:\'.*?\' )?not found')
31DEVICE_OFFLINE_REGEX = re.compile('^error: device offline')
32# Raised when adb forward commands fail to forward a port.
33CANNOT_BIND_LISTENER_REGEX = re.compile('^error: cannot bind listener:')
34# Expected output is "Android Debug Bridge version 1.0.XX
35ADB_VERSION_REGEX = re.compile('Android Debug Bridge version 1.0.(\d+)')
36ROOT_USER_ID = '0'
37SHELL_USER_ID = '2000'
38
39
40def parsing_parcel_output(output):
41    """Parsing the adb output in Parcel format.
42
43    Parsing the adb output in format:
44      Result: Parcel(
45        0x00000000: 00000000 00000014 00390038 00340031 '........8.9.1.4.'
46        0x00000010: 00300038 00300030 00300030 00340032 '8.0.0.0.0.0.2.4.'
47        0x00000020: 00350034 00330035 00320038 00310033 '4.5.5.3.8.2.3.1.'
48        0x00000030: 00000000                            '....            ')
49    """
50    output = ''.join(re.findall(r"'(.*)'", output))
51    return re.sub(r'[.\s]', '', output)
52
53
54class AdbError(error.ActsError):
55    """Raised when there is an error in adb operations."""
56
57    def __init__(self, cmd, stdout, stderr, ret_code):
58        super().__init__()
59        self.cmd = cmd
60        self.stdout = stdout
61        self.stderr = stderr
62        self.ret_code = ret_code
63
64    def __str__(self):
65        return ("Error executing adb cmd '%s'. ret: %d, stdout: %s, stderr: %s"
66                ) % (self.cmd, self.ret_code, self.stdout, self.stderr)
67
68
69class AdbProxy(object):
70    """Proxy class for ADB.
71
72    For syntactic reasons, the '-' in adb commands need to be replaced with
73    '_'. Can directly execute adb commands on an object:
74    >> adb = AdbProxy(<serial>)
75    >> adb.start_server()
76    >> adb.devices() # will return the console output of "adb devices".
77    """
78
79    def __init__(self, serial="", ssh_connection=None):
80        """Construct an instance of AdbProxy.
81
82        Args:
83            serial: str serial number of Android device from `adb devices`
84            ssh_connection: SshConnection instance if the Android device is
85                            connected to a remote host that we can reach via SSH.
86        """
87        self.serial = serial
88        self._server_local_port = None
89        adb_path = job.run("which adb").stdout
90        adb_cmd = [adb_path]
91        if serial:
92            adb_cmd.append("-s %s" % serial)
93        if ssh_connection is not None:
94            # Kill all existing adb processes on the remote host (if any)
95            # Note that if there are none, then pkill exits with non-zero status
96            ssh_connection.run("pkill adb", ignore_status=True)
97            # Copy over the adb binary to a temp dir
98            temp_dir = ssh_connection.run("mktemp -d").stdout.strip()
99            ssh_connection.send_file(adb_path, temp_dir)
100            # Start up a new adb server running as root from the copied binary.
101            remote_adb_cmd = "%s/adb %s root" % (temp_dir, "-s %s" % serial
102                                                 if serial else "")
103            ssh_connection.run(remote_adb_cmd)
104            # Proxy a local port to the adb server port
105            local_port = ssh_connection.create_ssh_tunnel(5037)
106            self._server_local_port = local_port
107
108        if self._server_local_port:
109            adb_cmd.append("-P %d" % local_port)
110        self.adb_str = " ".join(adb_cmd)
111        self._ssh_connection = ssh_connection
112
113    def get_user_id(self):
114        """Returns the adb user. Either 2000 (shell) or 0 (root)."""
115        return self.shell('id -u')
116
117    def is_root(self, user_id=None):
118        """Checks if the user is root.
119
120        Args:
121            user_id: if supplied, the id to check against.
122        Returns:
123            True if the user is root. False otherwise.
124        """
125        if not user_id:
126            user_id = self.get_user_id()
127        return user_id == ROOT_USER_ID
128
129    def ensure_root(self):
130        """Ensures the user is root after making this call.
131
132        Note that this will still fail if the device is a user build, as root
133        is not accessible from a user build.
134
135        Returns:
136            False if the device is a user build. True otherwise.
137        """
138        self.ensure_user(ROOT_USER_ID)
139        return self.is_root()
140
141    def ensure_user(self, user_id=SHELL_USER_ID):
142        """Ensures the user is set to the given user.
143
144        Args:
145            user_id: The id of the user.
146        """
147        if self.is_root(user_id):
148            self.root()
149        else:
150            self.unroot()
151        self.wait_for_device()
152        return self.get_user_id() == user_id
153
154    def _exec_cmd(self, cmd, ignore_status=False, timeout=DEFAULT_ADB_TIMEOUT):
155        """Executes adb commands in a new shell.
156
157        This is specific to executing adb commands.
158
159        Args:
160            cmd: A string that is the adb command to execute.
161
162        Returns:
163            The stdout of the adb command.
164
165        Raises:
166            AdbError is raised if adb cannot find the device.
167        """
168        result = job.run(cmd, ignore_status=True, timeout=timeout)
169        ret, out, err = result.exit_status, result.stdout, result.stderr
170
171        if DEVICE_OFFLINE_REGEX.match(err):
172            raise AdbError(cmd=cmd, stdout=out, stderr=err, ret_code=ret)
173        if "Result: Parcel" in out:
174            return parsing_parcel_output(out)
175        if ignore_status:
176            return out or err
177        if ret == 1 and (DEVICE_NOT_FOUND_REGEX.match(err) or
178                         CANNOT_BIND_LISTENER_REGEX.match(err)):
179            raise AdbError(cmd=cmd, stdout=out, stderr=err, ret_code=ret)
180        else:
181            return out
182
183    def _exec_adb_cmd(self, name, arg_str, **kwargs):
184        return self._exec_cmd(' '.join((self.adb_str, name, arg_str)),
185                              **kwargs)
186
187    def _exec_cmd_nb(self, cmd, **kwargs):
188        """Executes adb commands in a new shell, non blocking.
189
190        Args:
191            cmds: A string that is the adb command to execute.
192
193        """
194        return job.run_async(cmd, **kwargs)
195
196    def _exec_adb_cmd_nb(self, name, arg_str, **kwargs):
197        return self._exec_cmd_nb(' '.join((self.adb_str, name, arg_str)),
198                                 **kwargs)
199
200    def tcp_forward(self, host_port, device_port):
201        """Starts tcp forwarding from localhost to this android device.
202
203        Args:
204            host_port: Port number to use on localhost
205            device_port: Port number to use on the android device.
206
207        Returns:
208            The command output for the forward command.
209        """
210        if self._ssh_connection:
211            # We have to hop through a remote host first.
212            #  1) Find some free port on the remote host's localhost
213            #  2) Setup forwarding between that remote port and the requested
214            #     device port
215            remote_port = self._ssh_connection.find_free_port()
216            local_port = self._ssh_connection.create_ssh_tunnel(
217                remote_port, local_port=host_port)
218            host_port = remote_port
219        output = self.forward("tcp:%d tcp:%d" % (host_port, device_port))
220        # If hinted_port is 0, the output will be the selected port.
221        # Otherwise, there will be no output upon successfully
222        # forwarding the hinted port.
223        if output:
224            return int(output)
225        else:
226            return local_port
227
228    def remove_tcp_forward(self, host_port):
229        """Stop tcp forwarding a port from localhost to this android device.
230
231        Args:
232            host_port: Port number to use on localhost
233        """
234        if self._ssh_connection:
235            remote_port = self._ssh_connection.close_ssh_tunnel(host_port)
236            if remote_port is None:
237                logging.warning("Cannot close unknown forwarded tcp port: %d",
238                                host_port)
239                return
240            # The actual port we need to disable via adb is on the remote host.
241            host_port = remote_port
242        self.forward("--remove tcp:%d" % host_port)
243
244    def getprop(self, prop_name):
245        """Get a property of the device.
246
247        This is a convenience wrapper for "adb shell getprop xxx".
248
249        Args:
250            prop_name: A string that is the name of the property to get.
251
252        Returns:
253            A string that is the value of the property, or None if the property
254            doesn't exist.
255        """
256        return self.shell("getprop %s" % prop_name)
257
258    # TODO: This should be abstracted out into an object like the other shell
259    # command.
260    def shell(self, command, ignore_status=False, timeout=DEFAULT_ADB_TIMEOUT):
261        return self._exec_adb_cmd(
262            'shell',
263            shellescape.quote(command),
264            ignore_status=ignore_status,
265            timeout=timeout)
266
267    def shell_nb(self, command):
268        return self._exec_adb_cmd_nb('shell', shellescape.quote(command))
269
270    def pull(self,
271             command,
272             ignore_status=False,
273             timeout=DEFAULT_ADB_PULL_TIMEOUT):
274        return self._exec_adb_cmd(
275            'pull', command, ignore_status=ignore_status, timeout=timeout)
276
277    def __getattr__(self, name):
278        def adb_call(*args, **kwargs):
279            clean_name = name.replace('_', '-')
280            arg_str = ' '.join(str(elem) for elem in args)
281            return self._exec_adb_cmd(clean_name, arg_str, **kwargs)
282
283        return adb_call
284
285    def get_version_number(self):
286        """Returns the version number of ADB as an int (XX in 1.0.XX).
287
288        Raises:
289            AdbError if the version number is not found/parsable.
290        """
291        version_output = self.version()
292        match = re.search(ADB_VERSION_REGEX,
293                          version_output)
294
295        if not match:
296            logging.error('Unable to capture ADB version from adb version '
297                          'output: %s' % version_output)
298            raise AdbError('adb version', version_output, '', '')
299        return int(match.group(1))
300