1#
2#   Copyright 2016 - The Android Open Source Project
3#
4#   Licensed under the Apache License, Version 2.0 (the "License");
5#   you may not use this file except in compliance with the License.
6#   You may obtain a copy of the License at
7#
8#       http://www.apache.org/licenses/LICENSE-2.0
9#
10#   Unless required by applicable law or agreed to in writing, software
11#   distributed under the License is distributed on an "AS IS" BASIS,
12#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13#   See the License for the specific language governing permissions and
14#   limitations under the License.
15
16from builtins import str
17
18import logging
19import random
20import socket
21import subprocess
22import time
23
24from vts.runners.host import const
25from vts.utils.python.common import cmd_utils
26
27
28# Default adb timeout 5 minutes
29DEFAULT_ADB_TIMEOUT = 300
30# Adb long timeout (10 minutes) for adb push/pull/bugreport/bugreportz
31DEFAULT_ADB_LONG_TIMEOUT = 600
32# Adb short timeout (30 seconds)
33DEFAULT_ADB_SHORT_TIMEOUT = 30
34
35class AdbError(Exception):
36    """Raised when there is an error in adb operations."""
37
38    def __init__(self, cmd, stdout, stderr, ret_code):
39        self.cmd = cmd
40        self.stdout = stdout
41        self.stderr = stderr
42        self.ret_code = ret_code
43
44    def __str__(self):
45        return ("Error executing adb cmd '%s'. ret: %d, stdout: %s, stderr: %s"
46                ) % (self.cmd, self.ret_code, self.stdout, self.stderr)
47
48
49def get_available_host_port():
50    """Gets a host port number available for adb forward.
51
52    Returns:
53        An integer representing a port number on the host available for adb
54        forward.
55    """
56    while True:
57        port = random.randint(1024, 9900)
58        if is_port_available(port):
59            return port
60
61
62def is_port_available(port):
63    """Checks if a given port number is available on the system.
64
65    Args:
66        port: An integer which is the port number to check.
67
68    Returns:
69        True if the port is available; False otherwise.
70    """
71    # Make sure adb is not using this port so we don't accidentally interrupt
72    # ongoing runs by trying to bind to the port.
73    if port in list_occupied_adb_ports():
74        return False
75    s = None
76    try:
77        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
78        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
79        s.bind(('localhost', port))
80        return True
81    except socket.error:
82        return False
83    finally:
84        if s:
85            s.close()
86
87
88def list_occupied_adb_ports():
89    """Lists all the host ports occupied by adb forward.
90
91    This is useful because adb will silently override the binding if an attempt
92    to bind to a port already used by adb was made, instead of throwing binding
93    error. So one should always check what ports adb is using before trying to
94    bind to a port with adb.
95
96    Returns:
97        A list of integers representing occupied host ports.
98    """
99    out = AdbProxy().forward("--list")
100    clean_lines = str(out, 'utf-8').strip().split('\n')
101    used_ports = []
102    for line in clean_lines:
103        tokens = line.split(" tcp:")
104        if len(tokens) != 3:
105            continue
106        used_ports.append(int(tokens[1]))
107    return used_ports
108
109
110class AdbProxy():
111    """Proxy class for ADB.
112
113    For syntactic reasons, the '-' in adb commands need to be replaced with
114    '_'. Can directly execute adb commands on an object:
115    >> adb = AdbProxy(<serial>)
116    >> adb.start_server()
117    >> adb.devices() # will return the console output of "adb devices".
118    """
119
120    def __init__(self, serial="", log=None):
121        self.serial = serial
122        if serial:
123            self.adb_str = "adb -s {}".format(serial)
124        else:
125            self.adb_str = "adb"
126        self.log = log
127
128    def _exec_cmd(self, cmd, no_except=False, timeout=DEFAULT_ADB_TIMEOUT):
129        """Executes adb commands in a new shell.
130
131        This is specific to executing adb binary because stderr is not a good
132        indicator of cmd execution status.
133
134        Args:
135            cmd: string, the adb command to execute.
136            no_except: bool, controls whether exception can be thrown.
137            timeout: float, timeout in seconds. If the command times out, the
138                     exit code is not 0.
139
140        Returns:
141            The output of the adb command run if the exit code is 0 and if
142            exceptions are allowed. Otherwise, returns a dictionary containing
143            stdout, stderr, and exit code.
144
145        Raises:
146            AdbError if the adb command exit code is not 0 and exceptions are
147            allowed.
148        """
149        out, err, ret = cmd_utils.ExecuteOneShellCommand(cmd, timeout)
150        logging.debug("cmd: %s, stdout: %s, stderr: %s, ret: %s", cmd, out,
151                      err, ret)
152        if no_except:
153            return {
154                const.STDOUT: out,
155                const.STDERR: err,
156                const.EXIT_CODE: ret,
157            }
158        else:
159            if ret == 0:
160                return out
161            else:
162                raise AdbError(cmd=cmd, stdout=out, stderr=err, ret_code=ret)
163
164    def tcp_forward(self, host_port, device_port):
165        """Starts TCP forwarding.
166
167        Args:
168            host_port: Port number to use on the computer.
169            device_port: Port number to use on the android device.
170        """
171        self.forward("tcp:{} tcp:{}".format(host_port, device_port))
172
173    def reverse_tcp_forward(self, device_port, host_port):
174        """Starts reverse TCP forwarding.
175
176        Args:
177            device_port: Port number to use on the android device.
178            host_port: Port number to use on the computer.
179        """
180        self.reverse("tcp:{} tcp:{}".format(device_port, host_port))
181
182    def __getattr__(self, name):
183
184        def adb_call(*args, **kwargs):
185            clean_name = name.replace('_', '-')
186            arg_str = ' '.join(str(elem) for elem in args)
187            if clean_name == 'shell':
188                arg_str = self._quote_wrap_shell_command(arg_str)
189            elif "timeout" not in kwargs.keys():
190                # for non-shell command like adb pull/push/bugreport, set longer default timeout
191                kwargs["timeout"] = DEFAULT_ADB_LONG_TIMEOUT
192            return self._exec_cmd(' '.join((self.adb_str, clean_name, arg_str)),
193                                  **kwargs)
194
195        return adb_call
196
197    def _quote_wrap_shell_command(self, cmd):
198        """Wraps adb shell command with double quotes.
199
200        Double quotes inside the command will be replaced with \".
201
202        Args:
203            cmd: string, command string.
204
205        Returns:
206            string, quote wrapped command.
207        """
208        return '"%s"' % cmd.replace('"', '\\"')