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 = [shellescape.quote(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)
178                         or CANNOT_BIND_LISTENER_REGEX.match(err)):
179            raise AdbError(cmd=cmd, stdout=out, stderr=err, ret_code=ret)
180        if ret == 2:
181            raise AdbError(cmd=cmd, stdout=out, stderr=err, ret_code=ret)
182        else:
183            return out
184
185    def _exec_adb_cmd(self, name, arg_str, **kwargs):
186        return self._exec_cmd(' '.join((self.adb_str, name, arg_str)),
187                              **kwargs)
188
189    def _exec_cmd_nb(self, cmd, **kwargs):
190        """Executes adb commands in a new shell, non blocking.
191
192        Args:
193            cmds: A string that is the adb command to execute.
194
195        """
196        return job.run_async(cmd, **kwargs)
197
198    def _exec_adb_cmd_nb(self, name, arg_str, **kwargs):
199        return self._exec_cmd_nb(' '.join((self.adb_str, name, arg_str)),
200                                 **kwargs)
201
202    def tcp_forward(self, host_port, device_port):
203        """Starts tcp forwarding from localhost to this android device.
204
205        Args:
206            host_port: Port number to use on localhost
207            device_port: Port number to use on the android device.
208
209        Returns:
210            The command output for the forward command.
211        """
212        if self._ssh_connection:
213            # We have to hop through a remote host first.
214            #  1) Find some free port on the remote host's localhost
215            #  2) Setup forwarding between that remote port and the requested
216            #     device port
217            remote_port = self._ssh_connection.find_free_port()
218            host_port = self._ssh_connection.create_ssh_tunnel(
219                remote_port, local_port=host_port)
220        output = self.forward("tcp:%d tcp:%d" % (host_port, device_port))
221        # If hinted_port is 0, the output will be the selected port.
222        # Otherwise, there will be no output upon successfully
223        # forwarding the hinted port.
224        if output:
225            return int(output)
226        else:
227            return host_port
228
229    def remove_tcp_forward(self, host_port):
230        """Stop tcp forwarding a port from localhost to this android device.
231
232        Args:
233            host_port: Port number to use on localhost
234        """
235        if self._ssh_connection:
236            remote_port = self._ssh_connection.close_ssh_tunnel(host_port)
237            if remote_port is None:
238                logging.warning("Cannot close unknown forwarded tcp port: %d",
239                                host_port)
240                return
241            # The actual port we need to disable via adb is on the remote host.
242            host_port = remote_port
243        self.forward("--remove tcp:%d" % host_port)
244
245    def getprop(self, prop_name):
246        """Get a property of the device.
247
248        This is a convenience wrapper for "adb shell getprop xxx".
249
250        Args:
251            prop_name: A string that is the name of the property to get.
252
253        Returns:
254            A string that is the value of the property, or None if the property
255            doesn't exist.
256        """
257        return self.shell("getprop %s" % prop_name)
258
259    # TODO: This should be abstracted out into an object like the other shell
260    # command.
261    def shell(self, command, ignore_status=False, timeout=DEFAULT_ADB_TIMEOUT):
262        return self._exec_adb_cmd(
263            'shell',
264            shellescape.quote(command),
265            ignore_status=ignore_status,
266            timeout=timeout)
267
268    def shell_nb(self, command):
269        return self._exec_adb_cmd_nb('shell', shellescape.quote(command))
270
271    def pull(self,
272             command,
273             ignore_status=False,
274             timeout=DEFAULT_ADB_PULL_TIMEOUT):
275        return self._exec_adb_cmd(
276            'pull', command, ignore_status=ignore_status, timeout=timeout)
277
278    def __getattr__(self, name):
279        def adb_call(*args, **kwargs):
280            clean_name = name.replace('_', '-')
281            arg_str = ' '.join(str(elem) for elem in args)
282            return self._exec_adb_cmd(clean_name, arg_str, **kwargs)
283
284        return adb_call
285
286    def get_version_number(self):
287        """Returns the version number of ADB as an int (XX in 1.0.XX).
288
289        Raises:
290            AdbError if the version number is not found/parsable.
291        """
292        version_output = self.version()
293        match = re.search(ADB_VERSION_REGEX, 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