1# Copyright 2014 Google Inc. All rights reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14"""A libusb1-based ADB reimplementation.
15
16ADB was giving us trouble with its client/server architecture, which is great
17for users and developers, but not so great for reliable scripting. This will
18allow us to more easily catch errors as Python exceptions instead of checking
19random exit codes, and all the other great benefits from not going through
20subprocess and a network socket.
21
22All timeouts are in milliseconds.
23"""
24
25import io
26import os
27import socket
28import posixpath
29
30from adb import adb_protocol
31from adb import common
32from adb import filesync_protocol
33
34# From adb.h
35CLASS = 0xFF
36SUBCLASS = 0x42
37PROTOCOL = 0x01
38# pylint: disable=invalid-name
39DeviceIsAvailable = common.InterfaceMatcher(CLASS, SUBCLASS, PROTOCOL)
40
41try:
42    # Imported locally to keep compatibility with previous code.
43    from adb.sign_cryptography import CryptographySigner
44except ImportError:
45    # Ignore this error when cryptography is not installed, there are other options.
46    pass
47
48
49class AdbCommands(object):
50    """Exposes adb-like methods for use.
51
52    Some methods are more-pythonic and/or have more options.
53    """
54    protocol_handler = adb_protocol.AdbMessage
55    filesync_handler = filesync_protocol.FilesyncProtocol
56
57    def __init__(self):
58
59        self.__reset()
60
61    def __reset(self):
62        self.build_props = None
63        self._handle = None
64        self._device_state = None
65
66        # Connection table tracks each open AdbConnection objects per service type for program functions
67        # that choose to persist an AdbConnection object for their functionality, using
68        # self._get_service_connection
69        self._service_connections = {}
70
71    def _get_service_connection(self, service, service_command=None, create=True, timeout_ms=None):
72        """
73        Based on the service, get the AdbConnection for that service or create one if it doesnt exist
74
75        :param service:
76        :param service_command: Additional service parameters to append
77        :param create: If False, dont create a connection if it does not exist
78        :return:
79        """
80
81        connection = self._service_connections.get(service, None)
82
83        if connection:
84            return connection
85
86        if not connection and not create:
87            return None
88
89        if service_command:
90            destination_str = b'%s:%s' % (service, service_command)
91        else:
92            destination_str = service
93
94        connection = self.protocol_handler.Open(
95            self._handle, destination=destination_str, timeout_ms=timeout_ms)
96
97        self._service_connections.update({service: connection})
98
99        return connection
100
101    def ConnectDevice(self, port_path=None, serial=None, default_timeout_ms=None, **kwargs):
102        """Convenience function to setup a transport handle for the adb device from
103             usb path or serial then connect to it.
104
105        Args:
106          port_path: The filename of usb port to use.
107          serial: The serial number of the device to use.
108          default_timeout_ms: The default timeout in milliseconds to use.
109          kwargs: handle: Device handle to use (instance of common.TcpHandle or common.UsbHandle)
110                  banner: Connection banner to pass to the remote device
111                  rsa_keys: List of AuthSigner subclass instances to be used for
112                      authentication. The device can either accept one of these via the Sign
113                      method, or we will send the result of GetPublicKey from the first one
114                      if the device doesn't accept any of them.
115                  auth_timeout_ms: Timeout to wait for when sending a new public key. This
116                      is only relevant when we send a new public key. The device shows a
117                      dialog and this timeout is how long to wait for that dialog. If used
118                      in automation, this should be low to catch such a case as a failure
119                      quickly; while in interactive settings it should be high to allow
120                      users to accept the dialog. We default to automation here, so it's low
121                      by default.
122
123        If serial specifies a TCP address:port, then a TCP connection is
124        used instead of a USB connection.
125        """
126
127        # If there isnt a handle override (used by tests), build one here
128        if 'handle' in kwargs:
129            self._handle = kwargs.pop('handle')
130        else:
131            # if necessary, convert serial to a unicode string
132            if isinstance(serial, (bytes, bytearray)):
133                serial = serial.decode('utf-8')
134
135            if serial and ':' in serial:
136                self._handle = common.TcpHandle(serial, timeout_ms=default_timeout_ms)
137            else:
138                self._handle = common.UsbHandle.FindAndOpen(
139                    DeviceIsAvailable, port_path=port_path, serial=serial,
140                    timeout_ms=default_timeout_ms)
141
142        self._Connect(**kwargs)
143
144        return self
145
146    def Close(self):
147        for conn in list(self._service_connections.values()):
148            if conn:
149                try:
150                    conn.Close()
151                except:
152                    pass
153
154        if self._handle:
155            self._handle.Close()
156
157        self.__reset()
158
159    def _Connect(self, banner=None, **kwargs):
160        """Connect to the device.
161
162        Args:
163          banner: See protocol_handler.Connect.
164          **kwargs: See protocol_handler.Connect and adb_commands.ConnectDevice for kwargs.
165               Includes handle, rsa_keys, and auth_timeout_ms.
166        Returns:
167          An instance of this class if the device connected successfully.
168        """
169
170        if not banner:
171            banner = socket.gethostname().encode()
172
173        conn_str = self.protocol_handler.Connect(self._handle, banner=banner, **kwargs)
174
175        # Remove banner and colons after device state (state::banner)
176        parts = conn_str.split(b'::')
177        self._device_state = parts[0]
178
179        # Break out the build prop info
180        self.build_props = str(parts[1].split(b';'))
181
182        return True
183
184    @classmethod
185    def Devices(cls):
186        """Get a generator of UsbHandle for devices available."""
187        return common.UsbHandle.FindDevices(DeviceIsAvailable)
188
189    def GetState(self):
190        return self._device_state
191
192    def Install(self, apk_path, destination_dir='', replace_existing=True,
193                grant_permissions=False, timeout_ms=None, transfer_progress_callback=None):
194        """Install an apk to the device.
195
196        Doesn't support verifier file, instead allows destination directory to be
197        overridden.
198
199        Args:
200          apk_path: Local path to apk to install.
201          destination_dir: Optional destination directory. Use /system/app/ for
202            persistent applications.
203          replace_existing: whether to replace existing application
204          grant_permissions: If True, grant all permissions to the app specified in its manifest
205          timeout_ms: Expected timeout for pushing and installing.
206          transfer_progress_callback: callback method that accepts filename, bytes_written and total_bytes of APK transfer
207
208        Returns:
209          The pm install output.
210        """
211        if not destination_dir:
212            destination_dir = '/data/local/tmp/'
213        basename = os.path.basename(apk_path)
214        destination_path = posixpath.join(destination_dir, basename)
215        self.Push(apk_path, destination_path, timeout_ms=timeout_ms, progress_callback=transfer_progress_callback)
216
217        cmd = ['pm install']
218        if grant_permissions:
219            cmd.append('-g')
220        if replace_existing:
221            cmd.append('-r')
222        cmd.append('"{}"'.format(destination_path))
223
224        ret = self.Shell(' '.join(cmd), timeout_ms=timeout_ms)
225
226        # Remove the apk
227        rm_cmd = ['rm', destination_path]
228        rmret = self.Shell(' '.join(rm_cmd), timeout_ms=timeout_ms)
229
230        return ret
231
232    def Uninstall(self, package_name, keep_data=False, timeout_ms=None):
233        """Removes a package from the device.
234
235        Args:
236          package_name: Package name of target package.
237          keep_data: whether to keep the data and cache directories
238          timeout_ms: Expected timeout for pushing and installing.
239
240        Returns:
241          The pm uninstall output.
242        """
243        cmd = ['pm uninstall']
244        if keep_data:
245            cmd.append('-k')
246        cmd.append('"%s"' % package_name)
247
248        return self.Shell(' '.join(cmd), timeout_ms=timeout_ms)
249
250    def Push(self, source_file, device_filename, mtime='0', timeout_ms=None, progress_callback=None, st_mode=None):
251        """Push a file or directory to the device.
252
253        Args:
254          source_file: Either a filename, a directory or file-like object to push to
255                       the device.
256          device_filename: Destination on the device to write to.
257          mtime: Optional, modification time to set on the file.
258          timeout_ms: Expected timeout for any part of the push.
259          st_mode: stat mode for filename
260          progress_callback: callback method that accepts filename, bytes_written and total_bytes,
261                             total_bytes will be -1 for file-like objects
262        """
263
264        if isinstance(source_file, str):
265            if os.path.isdir(source_file):
266                self.Shell("mkdir " + device_filename)
267                for f in os.listdir(source_file):
268                    self.Push(os.path.join(source_file, f), device_filename + '/' + f,
269                              progress_callback=progress_callback)
270                return
271            source_file = open(source_file, "rb")
272
273        with source_file:
274            connection = self.protocol_handler.Open(
275                self._handle, destination=b'sync:', timeout_ms=timeout_ms)
276            kwargs={}
277            if st_mode is not None:
278                kwargs['st_mode'] = st_mode
279            self.filesync_handler.Push(connection, source_file, device_filename,
280                                       mtime=int(mtime), progress_callback=progress_callback, **kwargs)
281        connection.Close()
282
283    def Pull(self, device_filename, dest_file=None, timeout_ms=None, progress_callback=None):
284        """Pull a file from the device.
285
286        Args:
287          device_filename: Filename on the device to pull.
288          dest_file: If set, a filename or writable file-like object.
289          timeout_ms: Expected timeout for any part of the pull.
290          progress_callback: callback method that accepts filename, bytes_written and total_bytes,
291                             total_bytes will be -1 for file-like objects
292
293        Returns:
294          The file data if dest_file is not set. Otherwise, True if the destination file exists
295        """
296        if not dest_file:
297            dest_file = io.BytesIO()
298        elif isinstance(dest_file, str):
299            dest_file = open(dest_file, 'wb')
300        elif isinstance(dest_file, file):
301            pass
302        else:
303            raise ValueError("destfile is of unknown type")
304
305        conn = self.protocol_handler.Open(
306            self._handle, destination=b'sync:', timeout_ms=timeout_ms)
307
308        self.filesync_handler.Pull(conn, device_filename, dest_file, progress_callback)
309
310        conn.Close()
311        if isinstance(dest_file, io.BytesIO):
312            return dest_file.getvalue()
313        else:
314            dest_file.close()
315            if hasattr(dest_file, 'name'):
316                return os.path.exists(dest_file.name)
317            # We don't know what the path is, so we just assume it exists.
318            return True
319
320    def Stat(self, device_filename):
321        """Get a file's stat() information."""
322        connection = self.protocol_handler.Open(self._handle, destination=b'sync:')
323        mode, size, mtime = self.filesync_handler.Stat(
324            connection, device_filename)
325        connection.Close()
326        return mode, size, mtime
327
328    def List(self, device_path):
329        """Return a directory listing of the given path.
330
331        Args:
332          device_path: Directory to list.
333        """
334        connection = self.protocol_handler.Open(self._handle, destination=b'sync:')
335        listing = self.filesync_handler.List(connection, device_path)
336        connection.Close()
337        return listing
338
339    def Reboot(self, destination=b''):
340        """Reboot the device.
341
342        Args:
343          destination: Specify 'bootloader' for fastboot.
344        """
345        self.protocol_handler.Open(self._handle, b'reboot:%s' % destination)
346
347    def RebootBootloader(self):
348        """Reboot device into fastboot."""
349        self.Reboot(b'bootloader')
350
351    def Remount(self):
352        """Remount / as read-write."""
353        return self.protocol_handler.Command(self._handle, service=b'remount')
354
355    def Root(self):
356        """Restart adbd as root on the device."""
357        return self.protocol_handler.Command(self._handle, service=b'root')
358
359    def EnableVerity(self):
360        """Re-enable dm-verity checking on userdebug builds"""
361        return self.protocol_handler.Command(self._handle, service=b'enable-verity')
362
363    def DisableVerity(self):
364        """Disable dm-verity checking on userdebug builds"""
365        return self.protocol_handler.Command(self._handle, service=b'disable-verity')
366
367    def Shell(self, command, timeout_ms=None):
368        """Run command on the device, returning the output.
369
370        Args:
371          command: Shell command to run
372          timeout_ms: Maximum time to allow the command to run.
373        """
374        return self.protocol_handler.Command(
375            self._handle, service=b'shell', command=command,
376            timeout_ms=timeout_ms)
377
378    def StreamingShell(self, command, timeout_ms=None):
379        """Run command on the device, yielding each line of output.
380
381        Args:
382          command: Command to run on the target.
383          timeout_ms: Maximum time to allow the command to run.
384
385        Yields:
386          The responses from the shell command.
387        """
388        return self.protocol_handler.StreamingCommand(
389            self._handle, service=b'shell', command=command,
390            timeout_ms=timeout_ms)
391
392    def Logcat(self, options, timeout_ms=None):
393        """Run 'shell logcat' and stream the output to stdout.
394
395        Args:
396          options: Arguments to pass to 'logcat'.
397          timeout_ms: Maximum time to allow the command to run.
398        """
399        return self.StreamingShell('logcat %s' % options, timeout_ms)
400
401    def InteractiveShell(self, cmd=None, strip_cmd=True, delim=None, strip_delim=True):
402        """Get stdout from the currently open interactive shell and optionally run a command
403            on the device, returning all output.
404
405        Args:
406          cmd: Optional. Command to run on the target.
407          strip_cmd: Optional (default True). Strip command name from stdout.
408          delim: Optional. Delimiter to look for in the output to know when to stop expecting more output
409          (usually the shell prompt)
410          strip_delim: Optional (default True): Strip the provided delimiter from the output
411
412        Returns:
413          The stdout from the shell command.
414        """
415        conn = self._get_service_connection(b'shell:')
416
417        return self.protocol_handler.InteractiveShellCommand(
418            conn, cmd=cmd, strip_cmd=strip_cmd,
419            delim=delim, strip_delim=strip_delim)
420