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