Giulio 4 years ago
commit
94d105e9bc

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+*.o
+*.pyc

+ 4 - 0
README.md

@@ -0,0 +1,4 @@
+Simple filesystem explorer with upload and download functionality for Prolis OS based PAX devices.
+Tested and wotking on the PAX D200.
+
+The "Xos Communication Bridge" protocol is based on ADB with some custom command and functionalities.

+ 0 - 0
adb/__init__.py


+ 419 - 0
adb/adb_commands.py

@@ -0,0 +1,419 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""A libusb1-based ADB reimplementation.
+
+ADB was giving us trouble with its client/server architecture, which is great
+for users and developers, but not so great for reliable scripting. This will
+allow us to more easily catch errors as Python exceptions instead of checking
+random exit codes, and all the other great benefits from not going through
+subprocess and a network socket.
+
+All timeouts are in milliseconds.
+"""
+
+import io
+import os
+import socket
+import posixpath
+
+from adb import adb_protocol
+from adb import common
+from adb import filesync_protocol
+
+# From adb.h
+CLASS = 0xFF
+SUBCLASS = 0x42
+PROTOCOL = 0x01
+# pylint: disable=invalid-name
+DeviceIsAvailable = common.InterfaceMatcher(CLASS, SUBCLASS, PROTOCOL)
+
+try:
+    # Imported locally to keep compatibility with previous code.
+    from adb.sign_m2crypto import M2CryptoSigner
+except ImportError:
+    # Ignore this error when M2Crypto is not installed, there are other options.
+    pass
+
+
+class AdbCommands(object):
+    """Exposes adb-like methods for use.
+
+    Some methods are more-pythonic and/or have more options.
+    """
+    protocol_handler = adb_protocol.AdbMessage
+    filesync_handler = filesync_protocol.FilesyncProtocol
+
+    def __init__(self):
+
+        self.__reset()
+
+    def __reset(self):
+        self.build_props = None
+        self._handle = None
+        self._device_state = None
+
+        # Connection table tracks each open AdbConnection objects per service type for program functions
+        # that choose to persist an AdbConnection object for their functionality, using
+        # self._get_service_connection
+        self._service_connections = {}
+
+    def _get_service_connection(self, service, service_command=None, create=True, timeout_ms=None):
+        """
+        Based on the service, get the AdbConnection for that service or create one if it doesnt exist
+
+        :param service:
+        :param service_command: Additional service parameters to append
+        :param create: If False, dont create a connection if it does not exist
+        :return:
+        """
+
+        connection = self._service_connections.get(service, None)
+
+        if connection:
+            return connection
+
+        if not connection and not create:
+            return None
+
+        if service_command:
+            destination_str = b'%s:%s' % (service, service_command)
+        else:
+            destination_str = service
+
+        connection = self.protocol_handler.Open(
+            self._handle, destination=destination_str, timeout_ms=timeout_ms)
+
+        self._service_connections.update({service: connection})
+
+        return connection
+
+    def ConnectDevice(self, port_path=None, serial=None, default_timeout_ms=None, **kwargs):
+        """Convenience function to setup a transport handle for the adb device from
+             usb path or serial then connect to it.
+
+        Args:
+          port_path: The filename of usb port to use.
+          serial: The serial number of the device to use.
+          default_timeout_ms: The default timeout in milliseconds to use.
+          kwargs: handle: Device handle to use (instance of common.TcpHandle or common.UsbHandle)
+                  banner: Connection banner to pass to the remote device
+                  rsa_keys: List of AuthSigner subclass instances to be used for
+                      authentication. The device can either accept one of these via the Sign
+                      method, or we will send the result of GetPublicKey from the first one
+                      if the device doesn't accept any of them.
+                  auth_timeout_ms: Timeout to wait for when sending a new public key. This
+                      is only relevant when we send a new public key. The device shows a
+                      dialog and this timeout is how long to wait for that dialog. If used
+                      in automation, this should be low to catch such a case as a failure
+                      quickly; while in interactive settings it should be high to allow
+                      users to accept the dialog. We default to automation here, so it's low
+                      by default.
+
+        If serial specifies a TCP address:port, then a TCP connection is
+        used instead of a USB connection.
+        """
+
+        # If there isnt a handle override (used by tests), build one here
+        if 'handle' in kwargs:
+            self._handle = kwargs.pop('handle')
+        else:
+            # if necessary, convert serial to a unicode string
+            if isinstance(serial, (bytes, bytearray)):
+                serial = serial.decode('utf-8')
+
+            if serial and ':' in serial:
+                self._handle = common.TcpHandle(serial, timeout_ms=default_timeout_ms)
+            else:
+                self._handle = common.UsbHandle.FindAndOpen(
+                    DeviceIsAvailable, port_path=port_path, serial=serial,
+                    timeout_ms=default_timeout_ms)
+
+        self._Connect(**kwargs)
+
+        return self
+
+    def Close(self):
+        for conn in list(self._service_connections.values()):
+            if conn:
+                try:
+                    conn.Close()
+                except:
+                    pass
+
+        if self._handle:
+            self._handle.Close()
+
+        self.__reset()
+
+    def _Connect(self, banner=None, **kwargs):
+        """Connect to the device.
+
+        Args:
+          banner: See protocol_handler.Connect.
+          **kwargs: See protocol_handler.Connect and adb_commands.ConnectDevice for kwargs.
+               Includes handle, rsa_keys, and auth_timeout_ms.
+        Returns:
+          An instance of this class if the device connected successfully.
+        """
+
+        if not banner:
+            banner = socket.gethostname().encode()
+
+        conn_str = self.protocol_handler.Connect(self._handle, banner=banner, **kwargs)
+
+        # Remove banner and colons after device state (state::banner)
+        parts = conn_str.split(b'::')
+        self._device_state = parts[0]
+
+        # Break out the build prop info
+        self.build_props = str(parts[1].split(b';'))
+
+        return True
+
+    @classmethod
+    def Devices(cls):
+        """Get a generator of UsbHandle for devices available."""
+        return common.UsbHandle.FindDevices(DeviceIsAvailable)
+
+    def GetState(self):
+        return self._device_state
+
+    def Install(self, apk_path, destination_dir='', replace_existing=True,
+                grant_permissions=False, timeout_ms=None, transfer_progress_callback=None):
+        """Install an apk to the device.
+
+        Doesn't support verifier file, instead allows destination directory to be
+        overridden.
+
+        Args:
+          apk_path: Local path to apk to install.
+          destination_dir: Optional destination directory. Use /system/app/ for
+            persistent applications.
+          replace_existing: whether to replace existing application
+          grant_permissions: If True, grant all permissions to the app specified in its manifest
+          timeout_ms: Expected timeout for pushing and installing.
+          transfer_progress_callback: callback method that accepts filename, bytes_written and total_bytes of APK transfer
+
+        Returns:
+          The pm install output.
+        """
+        if not destination_dir:
+            destination_dir = '/data/local/tmp/'
+        basename = os.path.basename(apk_path)
+        destination_path = posixpath.join(destination_dir, basename)
+        self.Push(apk_path, destination_path, timeout_ms=timeout_ms, progress_callback=transfer_progress_callback)
+
+        cmd = ['pm install']
+        if grant_permissions:
+            cmd.append('-g')
+        if replace_existing:
+            cmd.append('-r')
+        cmd.append('"{}"'.format(destination_path))
+
+        ret = self.Shell(' '.join(cmd), timeout_ms=timeout_ms)
+
+        # Remove the apk
+        rm_cmd = ['rm', destination_path]
+        rmret = self.Shell(' '.join(rm_cmd), timeout_ms=timeout_ms)
+
+        return ret
+
+    def Uninstall(self, package_name, keep_data=False, timeout_ms=None):
+        """Removes a package from the device.
+
+        Args:
+          package_name: Package name of target package.
+          keep_data: whether to keep the data and cache directories
+          timeout_ms: Expected timeout for pushing and installing.
+
+        Returns:
+          The pm uninstall output.
+        """
+        cmd = ['pm uninstall']
+        if keep_data:
+            cmd.append('-k')
+        cmd.append('"%s"' % package_name)
+
+        return self.Shell(' '.join(cmd), timeout_ms=timeout_ms)
+
+    def Push(self, source_file, device_filename, mtime='0', timeout_ms=None, progress_callback=None, st_mode=None):
+        """Push a file or directory to the device.
+
+        Args:
+          source_file: Either a filename, a directory or file-like object to push to
+                       the device.
+          device_filename: Destination on the device to write to.
+          mtime: Optional, modification time to set on the file.
+          timeout_ms: Expected timeout for any part of the push.
+          st_mode: stat mode for filename
+          progress_callback: callback method that accepts filename, bytes_written and total_bytes,
+                             total_bytes will be -1 for file-like objects
+        """
+
+        if isinstance(source_file, str):
+            if os.path.isdir(source_file):
+                self.Shell("mkdir " + device_filename)
+                for f in os.listdir(source_file):
+                    self.Push(os.path.join(source_file, f), device_filename + '/' + f,
+                              progress_callback=progress_callback)
+                return
+            source_file = open(source_file, "rb")
+
+        with source_file:
+            connection = self.protocol_handler.Open(
+                self._handle, destination=b'installer:', timeout_ms=timeout_ms)
+            kwargs={}
+            if st_mode is not None:
+                kwargs['st_mode'] = st_mode
+            self.filesync_handler.Push(connection, source_file, device_filename,
+                                       mtime=int(mtime), progress_callback=progress_callback, **kwargs)
+        connection.Close()
+
+    def Pull(self, device_filename, dest_file=None, timeout_ms=None, progress_callback=None):
+        """Pull a file from the device.
+
+        Args:
+          device_filename: Filename on the device to pull.
+          dest_file: If set, a filename or writable file-like object.
+          timeout_ms: Expected timeout for any part of the pull.
+          progress_callback: callback method that accepts filename, bytes_written and total_bytes,
+                             total_bytes will be -1 for file-like objects
+
+        Returns:
+          The file data if dest_file is not set. Otherwise, True if the destination file exists
+        """
+        if not dest_file:
+            dest_file = io.BytesIO()
+        elif isinstance(dest_file, str):
+            dest_file = open(dest_file, 'wb')
+        elif isinstance(dest_file, file):
+            pass
+        else:
+            raise ValueError("destfile is of unknown type")
+
+        conn = self.protocol_handler.Open(
+            self._handle, destination=b'installer:', timeout_ms=timeout_ms)
+
+        self.filesync_handler.Pull(conn, device_filename, dest_file, progress_callback)
+
+        conn.Close()
+        if isinstance(dest_file, io.BytesIO):
+            return dest_file.getvalue()
+        else:
+            dest_file.close()
+            if hasattr(dest_file, 'name'):
+                return os.path.exists(dest_file.name)
+            # We don't know what the path is, so we just assume it exists.
+            return True
+
+    def Stat(self, device_filename):
+        """Get a file's stat() information."""
+        connection = self.protocol_handler.Open(self._handle, destination=b'installer:')
+        mode, size, mtime = self.filesync_handler.Stat(
+            connection, device_filename)
+        connection.Close()
+        return mode, size, mtime
+
+    def List(self, device_path):
+        """Return a directory listing of the given path.
+
+        Args:
+          device_path: Directory to list.
+        """
+        connection = self.protocol_handler.Open(self._handle, destination=b'installer:')
+        listing = self.filesync_handler.List(connection, device_path)
+        connection.Close()
+        return listing
+
+    def Reboot(self, destination=b''):
+        """Reboot the device.
+
+        Args:
+          destination: Specify 'bootloader' for fastboot.
+        """
+        self.protocol_handler.Open(self._handle, b'reboot:%s' % destination)
+
+    def RebootBootloader(self):
+        """Reboot device into fastboot."""
+        self.Reboot(b'bootloader')
+
+    def Remount(self):
+        """Remount / as read-write."""
+        return self.protocol_handler.Command(self._handle, service=b'remount')
+
+    def Root(self):
+        """Restart adbd as root on the device."""
+        return self.protocol_handler.Command(self._handle, service=b'root')
+
+    def EnableVerity(self):
+        """Re-enable dm-verity checking on userdebug builds"""
+        return self.protocol_handler.Command(self._handle, service=b'enable-verity')
+
+    def DisableVerity(self):
+        """Disable dm-verity checking on userdebug builds"""
+        return self.protocol_handler.Command(self._handle, service=b'disable-verity')
+
+    def Shell(self, command, timeout_ms=None):
+        """Run command on the device, returning the output.
+
+        Args:
+          command: Shell command to run
+          timeout_ms: Maximum time to allow the command to run.
+        """
+        return self.protocol_handler.Command(
+            self._handle, service=b'shell', command=command,
+            timeout_ms=timeout_ms)
+
+    def StreamingShell(self, command, timeout_ms=None):
+        """Run command on the device, yielding each line of output.
+
+        Args:
+          command: Command to run on the target.
+          timeout_ms: Maximum time to allow the command to run.
+
+        Yields:
+          The responses from the shell command.
+        """
+        return self.protocol_handler.StreamingCommand(
+            self._handle, service=b'shell', command=command,
+            timeout_ms=timeout_ms)
+
+    def Logcat(self, options, timeout_ms=None):
+        """Run 'shell logcat' and stream the output to stdout.
+
+        Args:
+          options: Arguments to pass to 'logcat'.
+          timeout_ms: Maximum time to allow the command to run.
+        """
+        return self.StreamingShell('logcat %s' % options, timeout_ms)
+
+    def InteractiveShell(self, cmd=None, strip_cmd=True, delim=None, strip_delim=True):
+        """Get stdout from the currently open interactive shell and optionally run a command
+            on the device, returning all output.
+
+        Args:
+          cmd: Optional. Command to run on the target.
+          strip_cmd: Optional (default True). Strip command name from stdout.
+          delim: Optional. Delimiter to look for in the output to know when to stop expecting more output
+          (usually the shell prompt)
+          strip_delim: Optional (default True): Strip the provided delimiter from the output
+
+        Returns:
+          The stdout from the shell command.
+        """
+        conn = self._get_service_connection(b'shell:')
+
+        return self.protocol_handler.InteractiveShellCommand(
+            conn, cmd=cmd, strip_cmd=strip_cmd,
+            delim=delim, strip_delim=strip_delim)

+ 211 - 0
adb/adb_debug.py

@@ -0,0 +1,211 @@
+#!/usr/bin/env python
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Daemon-less ADB client in python."""
+
+import argparse
+import functools
+import logging
+import os
+import stat
+import sys
+import time
+
+from adb import adb_commands
+from adb import common_cli
+
+try:
+    from adb import sign_m2crypto
+
+    rsa_signer = sign_m2crypto.M2CryptoSigner
+except ImportError:
+    try:
+        from adb import sign_pythonrsa
+
+        rsa_signer = sign_pythonrsa.PythonRSASigner.FromRSAKeyPath
+    except ImportError:
+        try:
+            from adb import sign_pycryptodome
+
+            rsa_signer = sign_pycryptodome.PycryptodomeAuthSigner
+        except ImportError:
+            rsa_signer = None
+
+
+def Devices(args):
+    """Lists the available devices.
+
+    Mimics 'adb devices' output:
+      List of devices attached
+      015DB7591102001A        device        1,2
+    """
+    for d in adb_commands.AdbCommands.Devices():
+        if args.output_port_path:
+            print('%s\tdevice\t%s' % (
+                d.serial_number, ','.join(str(p) for p in d.port_path)))
+        else:
+            print('%s\tdevice' % d.serial_number)
+    return 0
+
+
+def List(device, device_path):
+    """Prints a directory listing.
+
+    Args:
+      device_path: Directory to list.
+    """
+    files = device.List(device_path)
+    files.sort(key=lambda x: x.filename)
+    maxname = max(len(f.filename) for f in files)
+    maxsize = max(len(str(f.size)) for f in files)
+    for f in files:
+        mode = (
+                ('d' if stat.S_ISDIR(f.mode) else '-') +
+                ('r' if f.mode & stat.S_IRUSR else '-') +
+                ('w' if f.mode & stat.S_IWUSR else '-') +
+                ('x' if f.mode & stat.S_IXUSR else '-') +
+                ('r' if f.mode & stat.S_IRGRP else '-') +
+                ('w' if f.mode & stat.S_IWGRP else '-') +
+                ('x' if f.mode & stat.S_IXGRP else '-') +
+                ('r' if f.mode & stat.S_IROTH else '-') +
+                ('w' if f.mode & stat.S_IWOTH else '-') +
+                ('x' if f.mode & stat.S_IXOTH else '-'))
+        t = time.gmtime(f.mtime)
+        yield '%s %*d %04d-%02d-%02d %02d:%02d:%02d %-*s\n' % (
+            mode, maxsize, f.size,
+            t.tm_year, t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec,
+            maxname, f.filename)
+
+
+@functools.wraps(adb_commands.AdbCommands.Logcat)
+def Logcat(device, *options):
+    return device.Logcat(
+        device, ' '.join(options), timeout_ms=0)
+
+
+def Shell(device, *command):
+    """Runs a command on the device and prints the stdout.
+
+    Args:
+      command: Command to run on the target.
+    """
+    if command:
+        return device.StreamingShell(' '.join(command))
+    else:
+        # Retrieve the initial terminal prompt to use as a delimiter for future reads
+        terminal_prompt = device.InteractiveShell()
+        print(terminal_prompt.decode('utf-8'))
+
+        # Accept user input in a loop and write that into the interactive shells stdin, then print output
+        while True:
+            cmd = input('> ')
+            if not cmd:
+                continue
+            elif cmd == 'exit':
+                break
+            else:
+                stdout = device.InteractiveShell(cmd, strip_cmd=True, delim=terminal_prompt, strip_delim=True)
+                if stdout:
+                    if isinstance(stdout, bytes):
+                        stdout = stdout.decode('utf-8')
+                        print(stdout)
+
+        device.Close()
+
+
+def main():
+    common = common_cli.GetCommonArguments()
+    common.add_argument(
+        '--rsa_key_path', action='append', default=[],
+        metavar='~/.android/adbkey',
+        help='RSA key(s) to use, use multiple times to load mulitple keys')
+    common.add_argument(
+        '--auth_timeout_s', default=60., metavar='60', type=int,
+        help='Seconds to wait for the dialog to be accepted when using '
+             'authenticated ADB.')
+    device = common_cli.GetDeviceArguments()
+    parents = [common, device]
+
+    parser = argparse.ArgumentParser(
+        description=sys.modules[__name__].__doc__, parents=[common])
+    subparsers = parser.add_subparsers(title='Commands', dest='command_name')
+
+    subparser = subparsers.add_parser(
+        name='help', help='Prints the commands available')
+    subparser = subparsers.add_parser(
+        name='devices', help='Lists the available devices', parents=[common])
+    subparser.add_argument(
+        '--output_port_path', action='store_true',
+        help='Outputs the port_path alongside the serial')
+
+    common_cli.MakeSubparser(
+        subparsers, parents, adb_commands.AdbCommands.Install)
+    common_cli.MakeSubparser(subparsers, parents, adb_commands.AdbCommands.Uninstall)
+    common_cli.MakeSubparser(subparsers, parents, List)
+    common_cli.MakeSubparser(subparsers, parents, Logcat)
+    common_cli.MakeSubparser(
+        subparsers, parents, adb_commands.AdbCommands.Push,
+        {'source_file': 'Filename or directory to push to the device.'})
+    common_cli.MakeSubparser(
+        subparsers, parents, adb_commands.AdbCommands.Pull,
+        {
+            'dest_file': 'Filename to write to on the host, if not specified, '
+                         'prints the content to stdout.',
+        })
+    common_cli.MakeSubparser(
+        subparsers, parents, adb_commands.AdbCommands.Reboot)
+    common_cli.MakeSubparser(
+        subparsers, parents, adb_commands.AdbCommands.RebootBootloader)
+    common_cli.MakeSubparser(
+        subparsers, parents, adb_commands.AdbCommands.Remount)
+    common_cli.MakeSubparser(subparsers, parents, adb_commands.AdbCommands.Root)
+    common_cli.MakeSubparser(subparsers, parents, adb_commands.AdbCommands.EnableVerity)
+    common_cli.MakeSubparser(subparsers, parents, adb_commands.AdbCommands.DisableVerity)
+    common_cli.MakeSubparser(subparsers, parents, Shell)
+
+    if len(sys.argv) == 1:
+        parser.print_help()
+        return 2
+
+    args = parser.parse_args()
+    if args.verbose:
+        logging.basicConfig(level=logging.DEBUG)
+    if not args.rsa_key_path:
+        default = os.path.expanduser('~/.android/adbkey')
+        if os.path.isfile(default):
+            args.rsa_key_path = [default]
+    if args.rsa_key_path and not rsa_signer:
+        parser.error('Please install either M2Crypto, python-rsa, or PycryptoDome')
+
+    # Hacks so that the generated doc is nicer.
+    if args.command_name == 'devices':
+        return Devices(args)
+    if args.command_name == 'help':
+        parser.print_help()
+        return 0
+    if args.command_name == 'logcat':
+        args.positional = args.options
+    elif args.command_name == 'shell':
+        args.positional = args.command
+
+    return common_cli.StartCli(
+        args,
+        adb_commands.AdbCommands,
+        auth_timeout_ms=int(args.auth_timeout_s * 1000),
+        rsa_keys=[rsa_signer(path) for path in args.rsa_key_path])
+
+
+if __name__ == '__main__':
+    sys.exit(main())

+ 563 - 0
adb/adb_protocol.py

@@ -0,0 +1,563 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""ADB protocol implementation.
+
+Implements the ADB protocol as seen in android's adb/adbd binaries, but only the
+host side.
+"""
+
+import struct
+import time
+from io import BytesIO
+from adb import usb_exceptions
+
+# Maximum amount of data in an ADB packet.
+MAX_ADB_DATA = 4096
+# ADB protocol version.
+VERSION = 0x01000000
+
+# AUTH constants for arg0.
+AUTH_TOKEN = 1
+AUTH_SIGNATURE = 2
+AUTH_RSAPUBLICKEY = 3
+
+
+def find_backspace_runs(stdout_bytes, start_pos):
+    first_backspace_pos = stdout_bytes[start_pos:].find(b'\x08')
+    if first_backspace_pos == -1:
+        return -1, 0
+
+    end_backspace_pos = (start_pos + first_backspace_pos) + 1
+    while True:
+        if chr(stdout_bytes[end_backspace_pos]) == '\b':
+            end_backspace_pos += 1
+        else:
+            break
+
+    num_backspaces = end_backspace_pos - (start_pos + first_backspace_pos)
+
+    return (start_pos + first_backspace_pos), num_backspaces
+
+
+class InvalidCommandError(Exception):
+    """Got an invalid command over USB."""
+
+    def __init__(self, message, response_header, response_data):
+        if response_header == b'FAIL':
+            message = 'Command failed, device said so. (%s)' % message
+        super(InvalidCommandError, self).__init__(
+            message, response_header, response_data)
+
+
+class InvalidResponseError(Exception):
+    """Got an invalid response to our command."""
+
+
+class InvalidChecksumError(Exception):
+    """Checksum of data didn't match expected checksum."""
+
+
+class InterleavedDataError(Exception):
+    """We only support command sent serially."""
+
+
+def MakeWireIDs(ids):
+    id_to_wire = {
+        cmd_id: sum(c << (i * 8) for i, c in enumerate(bytearray(cmd_id)))
+        for cmd_id in ids
+    }
+    wire_to_id = {wire: cmd_id for cmd_id, wire in id_to_wire.items()}
+    return id_to_wire, wire_to_id
+
+
+class AuthSigner(object):
+    """Signer for use with authenticated ADB, introduced in 4.4.x/KitKat."""
+
+    def Sign(self, data):
+        """Signs given data using a private key."""
+        raise NotImplementedError()
+
+    def GetPublicKey(self):
+        """Returns the public key in PEM format without headers or newlines."""
+        raise NotImplementedError()
+
+
+class _AdbConnection(object):
+    """ADB Connection."""
+
+    def __init__(self, usb, local_id, remote_id, timeout_ms):
+        self.usb = usb
+        self.local_id = local_id
+        self.remote_id = remote_id
+        self.timeout_ms = timeout_ms
+
+    def _Send(self, command, arg0, arg1, data=b''):
+        message = AdbMessage(command, arg0, arg1, data)
+        message.Send(self.usb, self.timeout_ms)
+
+    def Write(self, data):
+        """Write a packet and expect an Ack."""
+        self._Send(b'WRTE', arg0=self.local_id, arg1=self.remote_id, data=data)
+        # Expect an ack in response.
+        cmd, okay_data = self.ReadUntil(b'OKAY')
+        if cmd != b'OKAY':
+            if cmd == b'FAIL':
+                raise usb_exceptions.AdbCommandFailureException(
+                    'Command failed.', okay_data)
+            raise InvalidCommandError(
+                'Expected an OKAY in response to a WRITE, got %s (%s)',
+                cmd, okay_data)
+        return len(data)
+
+    def Okay(self):
+        self._Send(b'OKAY', arg0=self.local_id, arg1=self.remote_id)
+
+    def ReadUntil(self, *expected_cmds):
+        """Read a packet, Ack any write packets."""
+        cmd, remote_id, local_id, data = AdbMessage.Read(
+            self.usb, expected_cmds, self.timeout_ms)
+        if local_id != 0 and self.local_id != local_id:
+            raise InterleavedDataError("We don't support multiple streams...")
+        if remote_id != 0 and self.remote_id != remote_id:
+            raise InvalidResponseError(
+                'Incorrect remote id, expected %s got %s' % (
+                    self.remote_id, remote_id))
+        # Ack write packets.
+        if cmd == b'WRTE':
+            self.Okay()
+        return cmd, data
+
+    def ReadUntilClose(self):
+        """Yield packets until a Close packet is received."""
+        while True:
+            cmd, data = self.ReadUntil(b'CLSE', b'WRTE')
+            if cmd == b'CLSE':
+                self._Send(b'CLSE', arg0=self.local_id, arg1=self.remote_id)
+                break
+            if cmd != b'WRTE':
+                if cmd == b'FAIL':
+                    raise usb_exceptions.AdbCommandFailureException(
+                        'Command failed.', data)
+                raise InvalidCommandError('Expected a WRITE or a CLOSE, got %s (%s)',
+                                          cmd, data)
+            yield data
+
+    def Close(self):
+        self._Send(b'CLSE', arg0=self.local_id, arg1=self.remote_id)
+        cmd, data = self.ReadUntil(b'CLSE')
+        if cmd != b'CLSE':
+            if cmd == b'FAIL':
+                raise usb_exceptions.AdbCommandFailureException('Command failed.', data)
+            raise InvalidCommandError('Expected a CLSE response, got %s (%s)',
+                                      cmd, data)
+
+
+class AdbMessage(object):
+    """ADB Protocol and message class.
+
+    Protocol Notes
+
+    local_id/remote_id:
+      Turns out the documentation is host/device ambidextrous, so local_id is the
+      id for 'the sender' and remote_id is for 'the recipient'. So since we're
+      only on the host, we'll re-document with host_id and device_id:
+
+      OPEN(host_id, 0, 'shell:XXX')
+      READY/OKAY(device_id, host_id, '')
+      WRITE(0, host_id, 'data')
+      CLOSE(device_id, host_id, '')
+    """
+
+    ids = [b'SYNC', b'CNXN', b'AUTH', b'OPEN', b'OKAY', b'CLSE', b'WRTE']
+    commands, constants = MakeWireIDs(ids)
+    # An ADB message is 6 words in little-endian.
+    format = b'<6I'
+
+    connections = 0
+
+    def __init__(self, command=None, arg0=None, arg1=None, data=b''):
+        self.command = self.commands[command]
+        self.magic = self.command ^ 0xFFFFFFFF
+        self.arg0 = arg0
+        self.arg1 = arg1
+        self.data = data
+
+    @property
+    def checksum(self):
+        return self.CalculateChecksum(self.data)
+
+    @staticmethod
+    def CalculateChecksum(data):
+        # The checksum is just a sum of all the bytes. I swear.
+        if isinstance(data, bytearray):
+            total = sum(data)
+        elif isinstance(data, bytes):
+            if data and isinstance(data[0], bytes):
+                # Python 2 bytes (str) index as single-character strings.
+                total = sum(map(ord, data))
+            else:
+                # Python 3 bytes index as numbers (and PY2 empty strings sum() to 0)
+                total = sum(data)
+        else:
+            # Unicode strings (should never see?)
+            total = sum(map(ord, data))
+        return total & 0xFFFFFFFF
+
+    def Pack(self):
+        """Returns this message in an over-the-wire format."""
+        return struct.pack(self.format, self.command, self.arg0, self.arg1,
+                           len(self.data), self.checksum, self.magic)
+
+    @classmethod
+    def Unpack(cls, message):
+        try:
+            cmd, arg0, arg1, data_length, data_checksum, unused_magic = struct.unpack(
+                cls.format, message)
+        except struct.error as e:
+            raise ValueError('Unable to unpack ADB command.', cls.format, message, e)
+        return cmd, arg0, arg1, data_length, data_checksum
+
+    def Send(self, usb, timeout_ms=None):
+        """Send this message over USB."""
+        usb.BulkWrite(self.Pack(), timeout_ms)
+        usb.BulkWrite(self.data, timeout_ms)
+
+    @classmethod
+    def Read(cls, usb, expected_cmds, timeout_ms=None, total_timeout_ms=None):
+        """Receive a response from the device."""
+        total_timeout_ms = usb.Timeout(total_timeout_ms)
+        start = time.time()
+        while True:
+            msg = usb.BulkRead(24, timeout_ms)
+            cmd, arg0, arg1, data_length, data_checksum = cls.Unpack(msg)
+            command = cls.constants.get(cmd)
+            if not command:
+                raise InvalidCommandError(
+                    'Unknown command: %x' % cmd, cmd, (arg0, arg1))
+            if command in expected_cmds:
+                break
+
+            if time.time() - start > total_timeout_ms:
+                raise InvalidCommandError(
+                    'Never got one of the expected responses (%s)' % expected_cmds,
+                    cmd, (timeout_ms, total_timeout_ms))
+
+        if data_length > 0:
+            data = bytearray()
+            while data_length > 0:
+                temp = usb.BulkRead(data_length, timeout_ms)
+                if len(temp) != data_length:
+                    print(
+                        "Data_length {} does not match actual number of bytes read: {}".format(data_length, len(temp)))
+                data += temp
+
+                data_length -= len(temp)
+
+            actual_checksum = cls.CalculateChecksum(data)
+            if actual_checksum != data_checksum:
+                raise InvalidChecksumError(
+                    'Received checksum %s != %s', (actual_checksum, data_checksum))
+        else:
+            data = b''
+        return command, arg0, arg1, bytes(data)
+
+    @classmethod
+    def Connect(cls, usb, banner=b'notadb', rsa_keys=None, auth_timeout_ms=100):
+        """Establish a new connection to the device.
+
+        Args:
+          usb: A USBHandle with BulkRead and BulkWrite methods.
+          banner: A string to send as a host identifier.
+          rsa_keys: List of AuthSigner subclass instances to be used for
+              authentication. The device can either accept one of these via the Sign
+              method, or we will send the result of GetPublicKey from the first one
+              if the device doesn't accept any of them.
+          auth_timeout_ms: Timeout to wait for when sending a new public key. This
+              is only relevant when we send a new public key. The device shows a
+              dialog and this timeout is how long to wait for that dialog. If used
+              in automation, this should be low to catch such a case as a failure
+              quickly; while in interactive settings it should be high to allow
+              users to accept the dialog. We default to automation here, so it's low
+              by default.
+
+        Returns:
+          The device's reported banner. Always starts with the state (device,
+              recovery, or sideload), sometimes includes information after a : with
+              various product information.
+
+        Raises:
+          usb_exceptions.DeviceAuthError: When the device expects authentication,
+              but we weren't given any valid keys.
+          InvalidResponseError: When the device does authentication in an
+              unexpected way.
+        """
+        # In py3, convert unicode to bytes. In py2, convert str to bytes.
+        # It's later joined into a byte string, so in py2, this ends up kind of being a no-op.
+        if isinstance(banner, str):
+            banner = bytearray(banner, 'utf-8')
+
+        msg = cls(
+            command=b'CNXN', arg0=VERSION, arg1=MAX_ADB_DATA,
+            data=b'host::%s\0' % banner)
+        msg.Send(usb)
+        cmd, arg0, arg1, banner = cls.Read(usb, [b'CNXN', b'AUTH'])
+        if cmd == b'AUTH':
+            if not rsa_keys:
+                raise usb_exceptions.DeviceAuthError(
+                    'Device authentication required, no keys available.')
+            # Loop through our keys, signing the last 'banner' or token.
+            for rsa_key in rsa_keys:
+                if arg0 != AUTH_TOKEN:
+                    raise InvalidResponseError(
+                        'Unknown AUTH response: %s %s %s' % (arg0, arg1, banner))
+
+                # Do not mangle the banner property here by converting it to a string
+                signed_token = rsa_key.Sign(banner)
+                msg = cls(
+                    command=b'AUTH', arg0=AUTH_SIGNATURE, arg1=0, data=signed_token)
+                msg.Send(usb)
+                cmd, arg0, unused_arg1, banner = cls.Read(usb, [b'CNXN', b'AUTH'])
+                if cmd == b'CNXN':
+                    return banner
+            # None of the keys worked, so send a public key.
+            msg = cls(
+                command=b'AUTH', arg0=AUTH_RSAPUBLICKEY, arg1=0,
+                data=rsa_keys[0].GetPublicKey() + b'\0')
+            msg.Send(usb)
+            try:
+                cmd, arg0, unused_arg1, banner = cls.Read(
+                    usb, [b'CNXN'], timeout_ms=auth_timeout_ms)
+            except usb_exceptions.ReadFailedError as e:
+                if e.usb_error.value == -7:  # Timeout.
+                    raise usb_exceptions.DeviceAuthError(
+                        'Accept auth key on device, then retry.')
+                raise
+            # This didn't time-out, so we got a CNXN response.
+            return banner
+        return banner
+
+    @classmethod
+    def Open(cls, usb, destination, timeout_ms=None):
+        """Opens a new connection to the device via an OPEN message.
+
+        Not the same as the posix 'open' or any other google3 Open methods.
+
+        Args:
+          usb: USB device handle with BulkRead and BulkWrite methods.
+          destination: The service:command string.
+          timeout_ms: Timeout in milliseconds for USB packets.
+
+        Raises:
+          InvalidResponseError: Wrong local_id sent to us.
+          InvalidCommandError: Didn't get a ready response.
+
+        Returns:
+          The local connection id.
+        """
+        local_id = 1
+        msg = cls(
+            command=b'OPEN', arg0=local_id, arg1=0,
+            data=destination + b'\0')
+        msg.Send(usb, timeout_ms)
+        cmd, remote_id, their_local_id, _ = cls.Read(usb, [b'CLSE', b'OKAY'],
+                                                     timeout_ms=timeout_ms)
+        if local_id != their_local_id:
+            raise InvalidResponseError(
+                'Expected the local_id to be {}, got {}'.format(local_id, their_local_id))
+        if cmd == b'CLSE':
+            # Some devices seem to be sending CLSE once more after a request, this *should* handle it
+            cmd, remote_id, their_local_id, _ = cls.Read(usb, [b'CLSE', b'OKAY'],
+                                                         timeout_ms=timeout_ms)
+            # Device doesn't support this service.
+            if cmd == b'CLSE':
+                return None
+        if cmd != b'OKAY':
+            raise InvalidCommandError('Expected a ready response, got {}'.format(cmd),
+                                      cmd, (remote_id, their_local_id))
+        return _AdbConnection(usb, local_id, remote_id, timeout_ms)
+
+    @classmethod
+    def Command(cls, usb, service, command='', timeout_ms=None):
+        """One complete set of USB packets for a single command.
+
+        Sends service:command in a new connection, reading the data for the
+        response. All the data is held in memory, large responses will be slow and
+        can fill up memory.
+
+        Args:
+          usb: USB device handle with BulkRead and BulkWrite methods.
+          service: The service on the device to talk to.
+          command: The command to send to the service.
+          timeout_ms: Timeout for USB packets, in milliseconds.
+
+        Raises:
+          InterleavedDataError: Multiple streams running over usb.
+          InvalidCommandError: Got an unexpected response command.
+
+        Returns:
+          The response from the service.
+        """
+        return ''.join(cls.StreamingCommand(usb, service, command, timeout_ms))
+
+    @classmethod
+    def StreamingCommand(cls, usb, service, command='', timeout_ms=None):
+        """One complete set of USB packets for a single command.
+
+        Sends service:command in a new connection, reading the data for the
+        response. All the data is held in memory, large responses will be slow and
+        can fill up memory.
+
+        Args:
+          usb: USB device handle with BulkRead and BulkWrite methods.
+          service: The service on the device to talk to.
+          command: The command to send to the service.
+          timeout_ms: Timeout for USB packets, in milliseconds.
+
+        Raises:
+          InterleavedDataError: Multiple streams running over usb.
+          InvalidCommandError: Got an unexpected response command.
+
+        Yields:
+          The responses from the service.
+        """
+        if not isinstance(command, bytes):
+            command = command.encode('utf8')
+        connection = cls.Open(
+            usb, destination=b'%s:%s' % (service, command),
+            timeout_ms=timeout_ms)
+        for data in connection.ReadUntilClose():
+            yield data.decode('utf8')
+
+    @classmethod
+    def InteractiveShellCommand(cls, conn, cmd=None, strip_cmd=True, delim=None, strip_delim=True, clean_stdout=True):
+        """Retrieves stdout of the current InteractiveShell and sends a shell command if provided
+        TODO: Should we turn this into a yield based function so we can stream all output?
+
+        Args:
+          conn: Instance of AdbConnection
+          cmd: Optional. Command to run on the target.
+          strip_cmd: Optional (default True). Strip command name from stdout.
+          delim: Optional. Delimiter to look for in the output to know when to stop expecting more output
+          (usually the shell prompt)
+          strip_delim: Optional (default True): Strip the provided delimiter from the output
+          clean_stdout: Cleanup the stdout stream of any backspaces and the characters that were deleted by the backspace
+        Returns:
+          The stdout from the shell command.
+        """
+
+        if delim is not None and not isinstance(delim, bytes):
+            delim = delim.encode('utf-8')
+
+        # Delimiter may be shell@hammerhead:/ $
+        # The user or directory could change, making the delimiter somthing like root@hammerhead:/data/local/tmp $
+        # Handle a partial delimiter to search on and clean up
+        if delim:
+            user_pos = delim.find(b'@')
+            dir_pos = delim.rfind(b':/')
+            if user_pos != -1 and dir_pos != -1:
+                partial_delim = delim[user_pos:dir_pos + 1]  # e.g. @hammerhead:
+            else:
+                partial_delim = delim
+        else:
+            partial_delim = None
+
+        stdout = ''
+        stdout_stream = BytesIO()
+        original_cmd = ''
+
+        try:
+
+            if cmd:
+                original_cmd = str(cmd)
+                cmd += '\r'  # Required. Send a carriage return right after the cmd
+                cmd = cmd.encode('utf8')
+
+                # Send the cmd raw
+                bytes_written = conn.Write(cmd)
+
+                if delim:
+                    # Expect multiple WRTE cmds until the delim (usually terminal prompt) is detected
+
+                    data = b''
+                    while partial_delim not in data:
+                        cmd, data = conn.ReadUntil(b'WRTE')
+                        stdout_stream.write(data)
+
+                else:
+                    # Otherwise, expect only a single WRTE
+                    cmd, data = conn.ReadUntil(b'WRTE')
+
+                    # WRTE cmd from device will follow with stdout data
+                    stdout_stream.write(data)
+
+            else:
+
+                # No cmd provided means we should just expect a single line from the terminal. Use this sparingly
+                cmd, data = conn.ReadUntil(b'WRTE')
+                if cmd == b'WRTE':
+                    # WRTE cmd from device will follow with stdout data
+                    stdout_stream.write(data)
+                else:
+                    print("Unhandled cmd: {}".format(cmd))
+
+            cleaned_stdout_stream = BytesIO()
+            if clean_stdout:
+                stdout_bytes = stdout_stream.getvalue()
+
+                bsruns = {}  # Backspace runs tracking
+                next_start_pos = 0
+                last_run_pos, last_run_len = find_backspace_runs(stdout_bytes, next_start_pos)
+
+                if last_run_pos != -1 and last_run_len != 0:
+                    bsruns.update({last_run_pos: last_run_len})
+                    cleaned_stdout_stream.write(stdout_bytes[next_start_pos:(last_run_pos - last_run_len)])
+                    next_start_pos += last_run_pos + last_run_len
+
+                while last_run_pos != -1:
+                    last_run_pos, last_run_len = find_backspace_runs(stdout_bytes[next_start_pos:], next_start_pos)
+
+                    if last_run_pos != -1:
+                        bsruns.update({last_run_pos: last_run_len})
+                        cleaned_stdout_stream.write(stdout_bytes[next_start_pos:(last_run_pos - last_run_len)])
+                        next_start_pos += last_run_pos + last_run_len
+
+                cleaned_stdout_stream.write(stdout_bytes[next_start_pos:])
+
+            else:
+                cleaned_stdout_stream.write(stdout_stream.getvalue())
+
+            stdout = cleaned_stdout_stream.getvalue()
+
+            # Strip original cmd that will come back in stdout
+            if original_cmd and strip_cmd:
+                findstr = original_cmd.encode('utf-8') + b'\r\r\n'
+                pos = stdout.find(findstr)
+                while pos >= 0:
+                    stdout = stdout.replace(findstr, b'')
+                    pos = stdout.find(findstr)
+
+                if b'\r\r\n' in stdout:
+                    stdout = stdout.split(b'\r\r\n')[1]
+
+            # Strip delim if requested
+            # TODO: Handling stripping partial delims here - not a deal breaker the way we're handling it now
+            if delim and strip_delim:
+                stdout = stdout.replace(delim, b'')
+
+            stdout = stdout.rstrip()
+
+        except Exception as e:
+            print("InteractiveShell exception (most likely timeout): {}".format(e))
+
+        return stdout

+ 8 - 0
adb/client.py

@@ -0,0 +1,8 @@
+from adb import adb_commands
+from adb import sign_m2crypto
+
+
+# Connect to the device
+device = adb_commands.AdbCommands()
+device.ConnectDevice(port_path=Non, serial="192.168.43.100")
+device.Stat('/tmp')

+ 355 - 0
adb/common.py

@@ -0,0 +1,355 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Common code for ADB and Fastboot.
+
+Common usb browsing, and usb communication.
+"""
+import logging
+import platform
+import socket
+import threading
+import weakref
+import select
+
+import libusb1
+import usb1
+
+from adb import usb_exceptions
+
+DEFAULT_TIMEOUT_MS = 10000
+
+_LOG = logging.getLogger('android_usb')
+
+
+def GetInterface(setting):
+    """Get the class, subclass, and protocol for the given USB setting."""
+    return (setting.getClass(), setting.getSubClass(), setting.getProtocol())
+
+
+def InterfaceMatcher(clazz, subclass, protocol):
+    """Returns a matcher that returns the setting with the given interface."""
+    interface = (clazz, subclass, protocol)
+
+    def Matcher(device):
+        for setting in device.iterSettings():
+            if GetInterface(setting) == interface:
+                return setting
+
+    return Matcher
+
+
+class UsbHandle(object):
+    """USB communication object. Not thread-safe.
+
+    Handles reading and writing over USB with the proper endpoints, exceptions,
+    and interface claiming.
+
+    Important methods:
+      FlushBuffers()
+      BulkRead(int length)
+      BulkWrite(bytes data)
+    """
+
+    _HANDLE_CACHE = weakref.WeakValueDictionary()
+    _HANDLE_CACHE_LOCK = threading.Lock()
+
+    def __init__(self, device, setting, usb_info=None, timeout_ms=None):
+        """Initialize USB Handle.
+
+        Arguments:
+          device: libusb_device to connect to.
+          setting: libusb setting with the correct endpoints to communicate with.
+          usb_info: String describing the usb path/serial/device, for debugging.
+          timeout_ms: Timeout in milliseconds for all I/O.
+        """
+        self._setting = setting
+        self._device = device
+        self._handle = None
+
+        self._usb_info = usb_info or ''
+        self._timeout_ms = timeout_ms if timeout_ms else DEFAULT_TIMEOUT_MS
+        self._max_read_packet_len = 0
+
+    @property
+    def usb_info(self):
+        try:
+            sn = self.serial_number
+        except libusb1.USBError:
+            sn = ''
+        if sn and sn != self._usb_info:
+            return '%s %s' % (self._usb_info, sn)
+        return self._usb_info
+
+    def Open(self):
+        """Opens the USB device for this setting, and claims the interface."""
+        # Make sure we close any previous handle open to this usb device.
+        port_path = tuple(self.port_path)
+        with self._HANDLE_CACHE_LOCK:
+            old_handle = self._HANDLE_CACHE.get(port_path)
+            if old_handle is not None:
+                old_handle.Close()
+
+        self._read_endpoint = None
+        self._write_endpoint = None
+
+        for endpoint in self._setting.iterEndpoints():
+            address = endpoint.getAddress()
+            if address & libusb1.USB_ENDPOINT_DIR_MASK:
+                self._read_endpoint = address
+                self._max_read_packet_len = endpoint.getMaxPacketSize()
+            else:
+                self._write_endpoint = address
+
+        assert self._read_endpoint is not None
+        assert self._write_endpoint is not None
+
+        handle = self._device.open()
+        iface_number = self._setting.getNumber()
+        try:
+            if (platform.system() != 'Windows'
+                    and handle.kernelDriverActive(iface_number)):
+                handle.detachKernelDriver(iface_number)
+        except libusb1.USBError as e:
+            if e.value == libusb1.LIBUSB_ERROR_NOT_FOUND:
+                _LOG.warning('Kernel driver not found for interface: %s.', iface_number)
+            else:
+                raise
+        handle.claimInterface(iface_number)
+        self._handle = handle
+        self._interface_number = iface_number
+
+        with self._HANDLE_CACHE_LOCK:
+            self._HANDLE_CACHE[port_path] = self
+        # When this object is deleted, make sure it's closed.
+        weakref.ref(self, self.Close)
+
+    @property
+    def serial_number(self):
+        return self._device.getSerialNumber()
+
+    @property
+    def port_path(self):
+        return [self._device.getBusNumber()] + self._device.getPortNumberList()
+
+    def Close(self):
+        if self._handle is None:
+            return
+        try:
+            self._handle.releaseInterface(self._interface_number)
+            self._handle.close()
+        except libusb1.USBError:
+            _LOG.info('USBError while closing handle %s: ',
+                      self.usb_info, exc_info=True)
+        finally:
+            self._handle = None
+
+    def Timeout(self, timeout_ms):
+        return timeout_ms if timeout_ms is not None else self._timeout_ms
+
+    def FlushBuffers(self):
+        while True:
+            try:
+                self.BulkRead(self._max_read_packet_len, timeout_ms=10)
+            except usb_exceptions.ReadFailedError as e:
+                if e.usb_error.value == libusb1.LIBUSB_ERROR_TIMEOUT:
+                    break
+                raise
+
+    def BulkWrite(self, data, timeout_ms=None):
+        if self._handle is None:
+            raise usb_exceptions.WriteFailedError(
+                'This handle has been closed, probably due to another being opened.',
+                None)
+        try:
+            return self._handle.bulkWrite(
+                self._write_endpoint, data, timeout=self.Timeout(timeout_ms))
+        except libusb1.USBError as e:
+            raise usb_exceptions.WriteFailedError(
+                'Could not send data to %s (timeout %sms)' % (
+                    self.usb_info, self.Timeout(timeout_ms)), e)
+
+    def BulkRead(self, length, timeout_ms=None):
+        if self._handle is None:
+            raise usb_exceptions.ReadFailedError(
+                'This handle has been closed, probably due to another being opened.',
+                None)
+        try:
+            # python-libusb1 > 1.6 exposes bytearray()s now instead of bytes/str.
+            # To support older and newer versions, we ensure everything's bytearray()
+            # from here on out.
+            return bytearray(self._handle.bulkRead(
+                self._read_endpoint, length, timeout=self.Timeout(timeout_ms)))
+        except libusb1.USBError as e:
+            raise usb_exceptions.ReadFailedError(
+                'Could not receive data from %s (timeout %sms)' % (
+                    self.usb_info, self.Timeout(timeout_ms)), e)
+
+    def BulkReadAsync(self, length, timeout_ms=None):
+        # See: https://pypi.python.org/pypi/libusb1 "Asynchronous I/O" section
+        return
+
+    @classmethod
+    def PortPathMatcher(cls, port_path):
+        """Returns a device matcher for the given port path."""
+        if isinstance(port_path, str):
+            # Convert from sysfs path to port_path.
+            port_path = [int(part) for part in SYSFS_PORT_SPLIT_RE.split(port_path)]
+        return lambda device: device.port_path == port_path
+
+    @classmethod
+    def SerialMatcher(cls, serial):
+        """Returns a device matcher for the given serial."""
+        return lambda device: device.serial_number == serial
+
+    @classmethod
+    def FindAndOpen(cls, setting_matcher,
+                    port_path=None, serial=None, timeout_ms=None):
+        dev = cls.Find(
+            setting_matcher, port_path=port_path, serial=serial,
+            timeout_ms=timeout_ms)
+        dev.Open()
+        dev.FlushBuffers()
+        return dev
+
+    @classmethod
+    def Find(cls, setting_matcher, port_path=None, serial=None, timeout_ms=None):
+        """Gets the first device that matches according to the keyword args."""
+        if port_path:
+            device_matcher = cls.PortPathMatcher(port_path)
+            usb_info = port_path
+        elif serial:
+            device_matcher = cls.SerialMatcher(serial)
+            usb_info = serial
+        else:
+            device_matcher = None
+            usb_info = 'first'
+        return cls.FindFirst(setting_matcher, device_matcher,
+                             usb_info=usb_info, timeout_ms=timeout_ms)
+
+    @classmethod
+    def FindFirst(cls, setting_matcher, device_matcher=None, **kwargs):
+        """Find and return the first matching device.
+
+        Args:
+          setting_matcher: See cls.FindDevices.
+          device_matcher: See cls.FindDevices.
+          **kwargs: See cls.FindDevices.
+
+        Returns:
+          An instance of UsbHandle.
+
+        Raises:
+          DeviceNotFoundError: Raised if the device is not available.
+        """
+        try:
+            return next(cls.FindDevices(
+                setting_matcher, device_matcher=device_matcher, **kwargs))
+        except StopIteration:
+            raise usb_exceptions.DeviceNotFoundError(
+                'No device available, or it is in the wrong configuration.')
+
+    @classmethod
+    def FindDevices(cls, setting_matcher, device_matcher=None,
+                    usb_info='', timeout_ms=None):
+        """Find and yield the devices that match.
+
+        Args:
+          setting_matcher: Function that returns the setting to use given a
+            usb1.USBDevice, or None if the device doesn't have a valid setting.
+          device_matcher: Function that returns True if the given UsbHandle is
+            valid. None to match any device.
+          usb_info: Info string describing device(s).
+          timeout_ms: Default timeout of commands in milliseconds.
+
+        Yields:
+          UsbHandle instances
+        """
+        ctx = usb1.USBContext()
+        for device in ctx.getDeviceList(skip_on_error=True):
+            setting = setting_matcher(device)
+            if setting is None:
+                continue
+
+            handle = cls(device, setting, usb_info=usb_info, timeout_ms=timeout_ms)
+            if device_matcher is None or device_matcher(handle):
+                yield handle
+
+
+class TcpHandle(object):
+    """TCP connection object.
+
+       Provides same interface as UsbHandle. """
+
+    def __init__(self, serial, timeout_ms=None):
+        """Initialize the TCP Handle.
+        Arguments:
+          serial: Android device serial of the form host or host:port.
+
+        Host may be an IP address or a host name.
+        """
+        # if necessary, convert serial to a unicode string
+        if isinstance(serial, (bytes, bytearray)):
+            serial = serial.decode('utf-8')
+
+        if ':' in serial:
+            self.host, self.port = serial.split(':')
+        else:
+            self.host = serial
+            self.port = "5555"
+
+        self._connection = None
+        self._serial_number = '%s:%s' % (self.host, self.port)
+        self._timeout_ms = float(timeout_ms) if timeout_ms else None
+
+        self._connect()
+
+    def _connect(self):
+        timeout = self.TimeoutSeconds(self._timeout_ms)
+        
+        self._connection = socket.create_connection((self.host, self.port),
+                                                    timeout=timeout)
+        if timeout:
+            self._connection.setblocking(0)
+
+    @property
+    def serial_number(self):
+        return self._serial_number
+
+    def BulkWrite(self, data, timeout=None):
+        t = self.TimeoutSeconds(timeout)
+        _, writeable, _ = select.select([], [self._connection], [], t)
+        if writeable:
+            return self._connection.send(data)
+        msg = 'Sending data to {} timed out after {}s. No data was sent.'.format(
+            self.serial_number, t)
+        raise usb_exceptions.TcpTimeoutException(msg)
+
+    def BulkRead(self, numbytes, timeout=None):
+        t = self.TimeoutSeconds(timeout)
+        readable, _, _ = select.select([self._connection], [], [], t)
+        if readable:
+            return self._connection.recv(numbytes)
+        msg = 'Reading from {} timed out (Timeout {}s)'.format(
+            self._serial_number, t)
+        raise usb_exceptions.TcpTimeoutException(msg)
+
+    def Timeout(self, timeout_ms):
+        return float(timeout_ms) if timeout_ms is not None else self._timeout_ms
+
+    def TimeoutSeconds(self, timeout_ms):
+        timeout = self.Timeout(timeout_ms)
+        return timeout / 1000.0 if timeout is not None else timeout
+
+    def Close(self):
+        return self._connection.close()

+ 164 - 0
adb/common_cli.py

@@ -0,0 +1,164 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Common code for ADB and Fastboot CLI.
+
+Usage introspects the given class for methods, args, and docs to show the user.
+
+StartCli handles connecting to a device, calling the expected method, and
+outputting the results.
+"""
+
+from __future__ import print_function
+import argparse
+import io
+import inspect
+import logging
+import re
+import sys
+import types
+
+from adb import usb_exceptions
+
+
+class _PortPathAction(argparse.Action):
+    def __call__(self, parser, namespace, values, option_string=None):
+        setattr(
+            namespace, self.dest,
+            [int(i) for i in values.replace('/', ',').split(',')])
+
+
+class PositionalArg(argparse.Action):
+    def __call__(self, parser, namespace, values, option_string=None):
+        namespace.positional.append(values)
+
+
+def GetDeviceArguments():
+    group = argparse.ArgumentParser('Device', add_help=False)
+    group.add_argument(
+        '--timeout_ms', default=10000, type=int, metavar='10000',
+        help='Timeout in milliseconds.')
+    group.add_argument(
+        '--port_path', action=_PortPathAction,
+        help='USB port path integers (eg 1,2 or 2,1,1)')
+    group.add_argument(
+        '-s', '--serial',
+        help='Device serial to look for (host:port or USB serial)')
+    return group
+
+
+def GetCommonArguments():
+    group = argparse.ArgumentParser('Common', add_help=False)
+    group.add_argument('--verbose', action='store_true', help='Enable logging')
+    return group
+
+
+def _DocToArgs(doc):
+    """Converts a docstring documenting arguments into a dict."""
+    m = None
+    offset = None
+    in_arg = False
+    out = {}
+    for l in doc.splitlines():
+        if l.strip() == 'Args:':
+            in_arg = True
+        elif in_arg:
+            if not l.strip():
+                break
+            if offset is None:
+                offset = len(l) - len(l.lstrip())
+            l = l[offset:]
+            if l[0] == ' ' and m:
+                out[m.group(1)] += ' ' + l.lstrip()
+            else:
+                m = re.match(r'^([a-z_]+): (.+)$', l.strip())
+                out[m.group(1)] = m.group(2)
+    return out
+
+
+def MakeSubparser(subparsers, parents, method, arguments=None):
+    """Returns an argparse subparser to create a 'subcommand' to adb."""
+    name = ('-'.join(re.split(r'([A-Z][a-z]+)', method.__name__)[1:-1:2])).lower()
+    help = method.__doc__.splitlines()[0]
+    subparser = subparsers.add_parser(
+        name=name, description=help, help=help.rstrip('.'), parents=parents)
+    subparser.set_defaults(method=method, positional=[])
+    argspec = inspect.getargspec(method)
+
+    # Figure out positionals and default argument, if any. Explicitly includes
+    # arguments that default to '' but excludes arguments that default to None.
+    offset = len(argspec.args) - len(argspec.defaults or []) - 1
+    positional = []
+    for i in range(1, len(argspec.args)):
+        if i > offset and argspec.defaults[i - offset - 1] is None:
+            break
+        positional.append(argspec.args[i])
+    defaults = [None] * offset + list(argspec.defaults or [])
+
+    # Add all arguments so they append to args.positional.
+    args_help = _DocToArgs(method.__doc__)
+    for name, default in zip(positional, defaults):
+        if not isinstance(default, (None.__class__, str)):
+            continue
+        subparser.add_argument(
+            name, help=(arguments or {}).get(name, args_help.get(name)),
+            default=default, nargs='?' if default is not None else None,
+            action=PositionalArg)
+    if argspec.varargs:
+        subparser.add_argument(
+            argspec.varargs, nargs=argparse.REMAINDER,
+            help=(arguments or {}).get(argspec.varargs, args_help.get(argspec.varargs)))
+    return subparser
+
+
+def _RunMethod(dev, args, extra):
+    """Runs a method registered via MakeSubparser."""
+    logging.info('%s(%s)', args.method.__name__, ', '.join(args.positional))
+    result = args.method(dev, *args.positional, **extra)
+    if result is not None:
+        if isinstance(result, io.StringIO):
+            sys.stdout.write(result.getvalue())
+        elif isinstance(result, (list, types.GeneratorType)):
+            r = ''
+            for r in result:
+                r = str(r)
+                sys.stdout.write(r)
+            if not r.endswith('\n'):
+                sys.stdout.write('\n')
+        else:
+            result = str(result)
+            sys.stdout.write(result)
+            if not result.endswith('\n'):
+                sys.stdout.write('\n')
+    return 0
+
+
+def StartCli(args, adb_commands, extra=None, **device_kwargs):
+    """Starts a common CLI interface for this usb path and protocol."""
+    try:
+        dev = adb_commands()
+        dev.ConnectDevice(port_path=args.port_path, serial=args.serial, default_timeout_ms=args.timeout_ms,
+                          **device_kwargs)
+    except usb_exceptions.DeviceNotFoundError as e:
+        print('No device found: {}'.format(e), file=sys.stderr)
+        return 1
+    except usb_exceptions.CommonUsbError as e:
+        print('Could not connect to device: {}'.format(e), file=sys.stderr)
+        return 1
+    try:
+        return _RunMethod(dev, args, extra or {})
+    except Exception as e:  # pylint: disable=broad-except
+        sys.stdout.write(str(e))
+        return 1
+    finally:
+        dev.Close()

+ 398 - 0
adb/fastboot.py

@@ -0,0 +1,398 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""A libusb1-based fastboot implementation."""
+
+import binascii
+import collections
+import io
+import logging
+import os
+import struct
+
+from adb import common
+from adb import usb_exceptions
+
+_LOG = logging.getLogger('fastboot')
+
+DEFAULT_MESSAGE_CALLBACK = lambda m: logging.info('Got %s from device', m)
+FastbootMessage = collections.namedtuple(  # pylint: disable=invalid-name
+    'FastbootMessage', ['message', 'header'])
+
+# From fastboot.c
+VENDORS = {0x18D1, 0x0451, 0x0502, 0x0FCE, 0x05C6, 0x22B8, 0x0955,
+           0x413C, 0x2314, 0x0BB4, 0x8087}
+CLASS = 0xFF
+SUBCLASS = 0x42
+PROTOCOL = 0x03
+# pylint: disable=invalid-name
+DeviceIsAvailable = common.InterfaceMatcher(CLASS, SUBCLASS, PROTOCOL)
+
+
+# pylint doesn't understand cross-module exception baseclasses.
+# pylint: disable=nonstandard-exception
+class FastbootTransferError(usb_exceptions.FormatMessageWithArgumentsException):
+    """Transfer error."""
+
+
+class FastbootRemoteFailure(usb_exceptions.FormatMessageWithArgumentsException):
+    """Remote error."""
+
+
+class FastbootStateMismatch(usb_exceptions.FormatMessageWithArgumentsException):
+    """Fastboot and uboot's state machines are arguing. You Lose."""
+
+
+class FastbootInvalidResponse(
+    usb_exceptions.FormatMessageWithArgumentsException):
+    """Fastboot responded with a header we didn't expect."""
+
+
+class FastbootProtocol(object):
+    """Encapsulates the fastboot protocol."""
+    FINAL_HEADERS = {b'OKAY', b'DATA'}
+
+    def __init__(self, usb, chunk_kb=1024):
+        """Constructs a FastbootProtocol instance.
+
+        Args:
+          usb: UsbHandle instance.
+          chunk_kb: Packet size. For older devices, 4 may be required.
+        """
+        self.usb = usb
+        self.chunk_kb = chunk_kb
+
+    @property
+    def usb_handle(self):
+        return self.usb
+
+    def SendCommand(self, command, arg=None):
+        """Sends a command to the device.
+
+        Args:
+          command: The command to send.
+          arg: Optional argument to the command.
+        """
+        if arg is not None:
+            if not isinstance(arg, bytes):
+                arg = arg.encode('utf8')
+            command = b'%s:%s' % (command, arg)
+
+        self._Write(io.BytesIO(command), len(command))
+
+    def HandleSimpleResponses(
+            self, timeout_ms=None, info_cb=DEFAULT_MESSAGE_CALLBACK):
+        """Accepts normal responses from the device.
+
+        Args:
+          timeout_ms: Timeout in milliseconds to wait for each response.
+          info_cb: Optional callback for text sent from the bootloader.
+
+        Returns:
+          OKAY packet's message.
+        """
+        return self._AcceptResponses(b'OKAY', info_cb, timeout_ms=timeout_ms)
+
+    def HandleDataSending(self, source_file, source_len,
+                          info_cb=DEFAULT_MESSAGE_CALLBACK,
+                          progress_callback=None, timeout_ms=None):
+        """Handles the protocol for sending data to the device.
+
+        Args:
+          source_file: File-object to read from for the device.
+          source_len: Amount of data, in bytes, to send to the device.
+          info_cb: Optional callback for text sent from the bootloader.
+          progress_callback: Callback that takes the current and the total progress
+            of the current file.
+          timeout_ms: Timeout in milliseconds to wait for each response.
+
+        Raises:
+          FastbootTransferError: When fastboot can't handle this amount of data.
+          FastbootStateMismatch: Fastboot responded with the wrong packet type.
+          FastbootRemoteFailure: Fastboot reported failure.
+          FastbootInvalidResponse: Fastboot responded with an unknown packet type.
+
+        Returns:
+          OKAY packet's message.
+        """
+        accepted_size = self._AcceptResponses(
+            b'DATA', info_cb, timeout_ms=timeout_ms)
+
+        accepted_size = binascii.unhexlify(accepted_size[:8])
+        accepted_size, = struct.unpack(b'>I', accepted_size)
+        if accepted_size != source_len:
+            raise FastbootTransferError(
+                'Device refused to download %s bytes of data (accepts %s bytes)',
+                source_len, accepted_size)
+        self._Write(source_file, accepted_size, progress_callback)
+        return self._AcceptResponses(b'OKAY', info_cb, timeout_ms=timeout_ms)
+
+    def _AcceptResponses(self, expected_header, info_cb, timeout_ms=None):
+        """Accepts responses until the expected header or a FAIL.
+
+        Args:
+          expected_header: OKAY or DATA
+          info_cb: Optional callback for text sent from the bootloader.
+          timeout_ms: Timeout in milliseconds to wait for each response.
+
+        Raises:
+          FastbootStateMismatch: Fastboot responded with the wrong packet type.
+          FastbootRemoteFailure: Fastboot reported failure.
+          FastbootInvalidResponse: Fastboot responded with an unknown packet type.
+
+        Returns:
+          OKAY packet's message.
+        """
+        while True:
+            response = self.usb.BulkRead(64, timeout_ms=timeout_ms)
+            header = bytes(response[:4])
+            remaining = bytes(response[4:])
+
+            if header == b'INFO':
+                info_cb(FastbootMessage(remaining, header))
+            elif header in self.FINAL_HEADERS:
+                if header != expected_header:
+                    raise FastbootStateMismatch(
+                        'Expected %s, got %s', expected_header, header)
+                if header == b'OKAY':
+                    info_cb(FastbootMessage(remaining, header))
+                return remaining
+            elif header == b'FAIL':
+                info_cb(FastbootMessage(remaining, header))
+                raise FastbootRemoteFailure('FAIL: %s', remaining)
+            else:
+                raise FastbootInvalidResponse(
+                    'Got unknown header %s and response %s', header, remaining)
+
+    def _HandleProgress(self, total, progress_callback):
+        """Calls the callback with the current progress and total ."""
+        current = 0
+        while True:
+            current += yield
+            try:
+                progress_callback(current, total)
+            except Exception:  # pylint: disable=broad-except
+                _LOG.exception('Progress callback raised an exception. %s',
+                               progress_callback)
+                continue
+
+    def _Write(self, data, length, progress_callback=None):
+        """Sends the data to the device, tracking progress with the callback."""
+        if progress_callback:
+            progress = self._HandleProgress(length, progress_callback)
+            next(progress)
+        while length:
+            tmp = data.read(self.chunk_kb * 1024)
+            length -= len(tmp)
+            self.usb.BulkWrite(tmp)
+
+            if progress_callback and progress:
+                progress.send(len(tmp))
+
+
+class FastbootCommands(object):
+    """Encapsulates the fastboot commands."""
+
+    def __init__(self):
+        """Constructs a FastbootCommands instance.
+
+        Args:
+          usb: UsbHandle instance.
+        """
+        self.__reset()
+
+    def __reset(self):
+        self._handle = None
+        self._protocol = None
+
+    @property
+    def usb_handle(self):
+        return self._handle
+
+    def Close(self):
+        self._handle.Close()
+
+    def ConnectDevice(self, port_path=None, serial=None, default_timeout_ms=None, chunk_kb=1024, **kwargs):
+        """Convenience function to get an adb device from usb path or serial.
+
+        Args:
+          port_path: The filename of usb port to use.
+          serial: The serial number of the device to use.
+          default_timeout_ms: The default timeout in milliseconds to use.
+          chunk_kb: Amount of data, in kilobytes, to break fastboot packets up into
+          kwargs: handle: Device handle to use (instance of common.TcpHandle or common.UsbHandle)
+                  banner: Connection banner to pass to the remote device
+                  rsa_keys: List of AuthSigner subclass instances to be used for
+                      authentication. The device can either accept one of these via the Sign
+                      method, or we will send the result of GetPublicKey from the first one
+                      if the device doesn't accept any of them.
+                  auth_timeout_ms: Timeout to wait for when sending a new public key. This
+                      is only relevant when we send a new public key. The device shows a
+                      dialog and this timeout is how long to wait for that dialog. If used
+                      in automation, this should be low to catch such a case as a failure
+                      quickly; while in interactive settings it should be high to allow
+                      users to accept the dialog. We default to automation here, so it's low
+                      by default.
+
+        If serial specifies a TCP address:port, then a TCP connection is
+        used instead of a USB connection.
+        """
+
+        if 'handle' in kwargs:
+            self._handle = kwargs['handle']
+
+        else:
+            self._handle = common.UsbHandle.FindAndOpen(
+                DeviceIsAvailable, port_path=port_path, serial=serial,
+                timeout_ms=default_timeout_ms)
+
+        self._protocol = FastbootProtocol(self._handle, chunk_kb)
+
+        return self
+
+    @classmethod
+    def Devices(cls):
+        """Get a generator of UsbHandle for devices available."""
+        return common.UsbHandle.FindDevices(DeviceIsAvailable)
+
+    def _SimpleCommand(self, command, arg=None, **kwargs):
+        self._protocol.SendCommand(command, arg)
+        return self._protocol.HandleSimpleResponses(**kwargs)
+
+    def FlashFromFile(self, partition, source_file, source_len=0,
+                      info_cb=DEFAULT_MESSAGE_CALLBACK, progress_callback=None):
+        """Flashes a partition from the file on disk.
+
+        Args:
+          partition: Partition name to flash to.
+          source_file: Filename to download to the device.
+          source_len: Optional length of source_file, uses os.stat if not provided.
+          info_cb: See Download.
+          progress_callback: See Download.
+
+        Returns:
+          Download and flash responses, normally nothing.
+        """
+        if source_len == 0:
+            # Fall back to stat.
+            source_len = os.stat(source_file).st_size
+        download_response = self.Download(
+            source_file, source_len=source_len, info_cb=info_cb,
+            progress_callback=progress_callback)
+        flash_response = self.Flash(partition, info_cb=info_cb)
+        return download_response + flash_response
+
+    def Download(self, source_file, source_len=0,
+                 info_cb=DEFAULT_MESSAGE_CALLBACK, progress_callback=None):
+        """Downloads a file to the device.
+
+        Args:
+          source_file: A filename or file-like object to download to the device.
+          source_len: Optional length of source_file. If source_file is a file-like
+              object and source_len is not provided, source_file is read into
+              memory.
+          info_cb: Optional callback accepting FastbootMessage for text sent from
+              the bootloader.
+          progress_callback: Optional callback called with the percent of the
+              source_file downloaded. Note, this doesn't include progress of the
+              actual flashing.
+
+        Returns:
+          Response to a download request, normally nothing.
+        """
+        if isinstance(source_file, str):
+            source_len = os.stat(source_file).st_size
+            source_file = open(source_file)
+
+        with source_file:
+            if source_len == 0:
+                # Fall back to storing it all in memory :(
+                data = source_file.read()
+                source_file = io.BytesIO(data.encode('utf8'))
+                source_len = len(data)
+
+            self._protocol.SendCommand(b'download', b'%08x' % source_len)
+            return self._protocol.HandleDataSending(
+                source_file, source_len, info_cb, progress_callback=progress_callback)
+
+    def Flash(self, partition, timeout_ms=0, info_cb=DEFAULT_MESSAGE_CALLBACK):
+        """Flashes the last downloaded file to the given partition.
+
+        Args:
+          partition: Partition to overwrite with the new image.
+          timeout_ms: Optional timeout in milliseconds to wait for it to finish.
+          info_cb: See Download. Usually no messages.
+
+        Returns:
+          Response to a download request, normally nothing.
+        """
+        return self._SimpleCommand(b'flash', arg=partition, info_cb=info_cb,
+                                   timeout_ms=timeout_ms)
+
+    def Erase(self, partition, timeout_ms=None):
+        """Erases the given partition.
+
+        Args:
+          partition: Partition to clear.
+        """
+        self._SimpleCommand(b'erase', arg=partition, timeout_ms=timeout_ms)
+
+    def Getvar(self, var, info_cb=DEFAULT_MESSAGE_CALLBACK):
+        """Returns the given variable's definition.
+
+        Args:
+          var: A variable the bootloader tracks. Use 'all' to get them all.
+          info_cb: See Download. Usually no messages.
+
+        Returns:
+          Value of var according to the current bootloader.
+        """
+        return self._SimpleCommand(b'getvar', arg=var, info_cb=info_cb)
+
+    def Oem(self, command, timeout_ms=None, info_cb=DEFAULT_MESSAGE_CALLBACK):
+        """Executes an OEM command on the device.
+
+        Args:
+          command: Command to execute, such as 'poweroff' or 'bootconfig read'.
+          timeout_ms: Optional timeout in milliseconds to wait for a response.
+          info_cb: See Download. Messages vary based on command.
+
+        Returns:
+          The final response from the device.
+        """
+        if not isinstance(command, bytes):
+            command = command.encode('utf8')
+        return self._SimpleCommand(
+            b'oem %s' % command, timeout_ms=timeout_ms, info_cb=info_cb)
+
+    def Continue(self):
+        """Continues execution past fastboot into the system."""
+        return self._SimpleCommand(b'continue')
+
+    def Reboot(self, target_mode=b'', timeout_ms=None):
+        """Reboots the device.
+
+        Args:
+            target_mode: Normal reboot when unspecified. Can specify other target
+                modes such as 'recovery' or 'bootloader'.
+            timeout_ms: Optional timeout in milliseconds to wait for a response.
+
+        Returns:
+            Usually the empty string. Depends on the bootloader and the target_mode.
+        """
+        return self._SimpleCommand(
+            b'reboot', arg=target_mode or None, timeout_ms=timeout_ms)
+
+    def RebootBootloader(self, timeout_ms=None):
+        """Reboots into the bootloader, usually equiv to Reboot('bootloader')."""
+        return self._SimpleCommand(b'reboot-bootloader', timeout_ms=timeout_ms)

+ 127 - 0
adb/fastboot_debug.py

@@ -0,0 +1,127 @@
+#!/usr/bin/env python
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Fastboot in python.
+
+Call it similar to how you call android's fastboot. Call it similar to how you
+call android's fastboot, but this only accepts usb paths and no serials.
+"""
+
+import argparse
+import inspect
+import logging
+import sys
+
+from adb import common_cli
+from adb import fastboot
+
+try:
+    import progressbar
+except ImportError:
+    # progressbar is optional.
+    progressbar = None
+
+
+def Devices(args):
+    """Lists the available devices.
+
+    List of devices attached
+    015DB7591102001A        device
+    """
+    for device in fastboot.FastbootCommands.Devices():
+        print('%s\tdevice' % device.serial_number)
+    return 0
+
+
+def _InfoCb(message):
+    # Use an unbuffered version of stdout.
+    if not message.message:
+        return
+    sys.stdout.write('%s: %s\n' % (message.header, message.message))
+    sys.stdout.flush()
+
+
+def main():
+    common = common_cli.GetCommonArguments()
+    device = common_cli.GetDeviceArguments()
+    device.add_argument(
+        '--chunk_kb', type=int, default=1024, metavar='1024',
+        help='Size of packets to write in Kb. For older devices, it may be '
+             'required to use 4.')
+    parents = [common, device]
+
+    parser = argparse.ArgumentParser(
+        description=sys.modules[__name__].__doc__, parents=[common])
+    subparsers = parser.add_subparsers(title='Commands', dest='command_name')
+
+    subparser = subparsers.add_parser(
+        name='help', help='Prints the commands available')
+    subparser = subparsers.add_parser(
+        name='devices', help='Lists the available devices', parents=[common])
+    common_cli.MakeSubparser(
+        subparsers, parents, fastboot.FastbootCommands.Continue)
+
+    common_cli.MakeSubparser(
+        subparsers, parents, fastboot.FastbootCommands.Download,
+        {'source_file': 'Filename on the host to push'})
+    common_cli.MakeSubparser(
+        subparsers, parents, fastboot.FastbootCommands.Erase)
+    common_cli.MakeSubparser(
+        subparsers, parents, fastboot.FastbootCommands.Flash)
+    common_cli.MakeSubparser(
+        subparsers, parents, fastboot.FastbootCommands.Getvar)
+    common_cli.MakeSubparser(
+        subparsers, parents, fastboot.FastbootCommands.Oem)
+    common_cli.MakeSubparser(
+        subparsers, parents, fastboot.FastbootCommands.Reboot)
+
+    if len(sys.argv) == 1:
+        parser.print_help()
+        return 2
+
+    args = parser.parse_args()
+    if args.verbose:
+        logging.basicConfig(level=logging.DEBUG)
+    if args.command_name == 'devices':
+        return Devices(args)
+    if args.command_name == 'help':
+        parser.print_help()
+        return 0
+
+    kwargs = {}
+    argspec = inspect.getargspec(args.method)
+    if 'info_cb' in argspec.args:
+        kwargs['info_cb'] = _InfoCb
+    if 'progress_callback' in argspec.args and progressbar:
+        bar = progressbar.ProgessBar(
+            widgets=[progressbar.Bar(), progressbar.Percentage()])
+        bar.start()
+
+        def SetProgress(current, total):
+            bar.update(current / total * 100.0)
+            if current == total:
+                bar.finish()
+
+        kwargs['progress_callback'] = SetProgress
+
+    return common_cli.StartCli(
+        args,
+        fastboot.FastbootCommands,
+        chunk_kb=args.chunk_kb,
+        extra=kwargs)
+
+
+if __name__ == '__main__':
+    sys.exit(main())

+ 268 - 0
adb/filesync_protocol.py

@@ -0,0 +1,268 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""ADB protocol implementation.
+
+Implements the ADB protocol as seen in android's adb/adbd binaries, but only the
+host side.
+"""
+
+import collections
+import os
+import stat
+import struct
+import time
+
+import libusb1
+
+from adb import adb_protocol
+from adb import usb_exceptions
+
+# Default mode for pushed files.
+DEFAULT_PUSH_MODE = stat.S_IFREG | stat.S_IRWXU | stat.S_IRWXG
+# Maximum size of a filesync DATA packet.
+MAX_PUSH_DATA = 2 * 1024
+
+
+class InvalidChecksumError(Exception):
+    """Checksum of data didn't match expected checksum."""
+
+
+class InterleavedDataError(Exception):
+    """We only support command sent serially."""
+
+
+class PushFailedError(Exception):
+    """Pushing a file failed for some reason."""
+
+
+class PullFailedError(Exception):
+    """Pulling a file failed for some reason."""
+
+
+DeviceFile = collections.namedtuple('DeviceFile', [
+    'filename', 'mode', 'size', 'mtime'])
+
+
+class FilesyncProtocol(object):
+    """Implements the FileSync protocol as described in sync.txt."""
+
+    @staticmethod
+    def Stat(connection, filename):
+        cnxn = FileSyncConnection(connection, b'<4I')
+        cnxn.Send(b'STAT', filename)
+        command, (mode, size, mtime) = cnxn.Read((b'STAT',), read_data=False)
+
+        if command != b'STAT':
+            raise adb_protocol.InvalidResponseError(
+                'Expected STAT response to STAT, got %s' % command)
+        return mode, size, mtime
+
+    @classmethod
+    def List(cls, connection, path):
+        cnxn = FileSyncConnection(connection, b'<5I')
+        cnxn.Send(b'LIST', path)
+        files = []
+        for cmd_id, header, filename in cnxn.ReadUntil((b'DENT',), b'DONE'):
+            if cmd_id == b'DONE':
+                break
+            mode, size, mtime = header
+            files.append(DeviceFile(filename, mode, size, mtime))
+        return files
+
+    @classmethod
+    def Pull(cls, connection, filename, dest_file, progress_callback):
+        """Pull a file from the device into the file-like dest_file."""
+        if progress_callback:
+            total_bytes = cls.Stat(connection, filename)[1]
+            progress = cls._HandleProgress(lambda current: progress_callback(filename, current, total_bytes))
+            next(progress)
+
+        cnxn = FileSyncConnection(connection, b'<2I')
+        try:
+            cnxn.Send(b'RECV', filename)
+            for cmd_id, _, data in cnxn.ReadUntil((b'DATA',), b'DONE'):
+                if cmd_id == b'DONE':
+                    break
+                dest_file.write(data)
+                if progress_callback:
+                    progress.send(len(data))
+        except usb_exceptions.CommonUsbError as e:
+            raise PullFailedError('Unable to pull file %s due to: %s' % (filename, e))
+
+    @classmethod
+    def _HandleProgress(cls, progress_callback):
+        """Calls the callback with the current progress and total bytes written/received.
+
+        Args:
+          progress_callback: callback method that accepts filename, bytes_written and total_bytes,
+                     total_bytes will be -1 for file-like objects
+        """
+        current = 0
+        while True:
+            current += yield
+            try:
+                progress_callback(current)
+            except Exception:  # pylint: disable=broad-except
+                continue
+
+    @classmethod
+    def Push(cls, connection, datafile, filename,
+             st_mode=DEFAULT_PUSH_MODE, mtime=0, progress_callback=None):
+        """Push a file-like object to the device.
+
+        Args:
+          connection: ADB connection
+          datafile: File-like object for reading from
+          filename: Filename to push to
+          st_mode: stat mode for filename
+          mtime: modification time
+          progress_callback: callback method that accepts filename, bytes_written and total_bytes
+
+        Raises:
+          PushFailedError: Raised on push failure.
+        """
+
+        fileinfo = ('{},{}'.format(filename, int(st_mode))).encode('utf-8')
+
+        cnxn = FileSyncConnection(connection, b'<2I')
+        cnxn.Send(b'SEND', fileinfo)
+
+        if progress_callback:
+            total_bytes = os.fstat(datafile.fileno()).st_size if isinstance(datafile, file) else -1
+            progress = cls._HandleProgress(lambda current: progress_callback(filename, current, total_bytes))
+            next(progress)
+
+        while True:
+            data = datafile.read(MAX_PUSH_DATA)
+            if data:
+                cnxn.Send(b'DATA', data)
+
+                if progress_callback:
+                    progress.send(len(data))
+            else:
+                break
+
+        if mtime == 0:
+            mtime = int(time.time())
+        # DONE doesn't send data, but it hides the last bit of data in the size
+        # field.
+        cnxn.Send(b'DONE', size=mtime)
+        for cmd_id, _, data in cnxn.ReadUntil((), b'OKAY', b'FAIL'):
+            if cmd_id == b'OKAY':
+                return
+            raise PushFailedError(data)
+
+
+class FileSyncConnection(object):
+    """Encapsulate a FileSync service connection."""
+
+    ids = [
+        b'STAT', b'LIST', b'SEND', b'RECV', b'DENT', b'DONE', b'DATA', b'OKAY',
+        b'FAIL', b'QUIT',
+    ]
+    id_to_wire, wire_to_id = adb_protocol.MakeWireIDs(ids)
+
+    def __init__(self, adb_connection, recv_header_format):
+        self.adb = adb_connection
+
+        # Sending
+        # Using a bytearray() saves a copy later when using libusb.
+        self.send_buffer = bytearray(adb_protocol.MAX_ADB_DATA)
+        self.send_idx = 0
+        self.send_header_len = struct.calcsize(b'<2I')
+
+        # Receiving
+        self.recv_buffer = bytearray()
+        self.recv_header_format = recv_header_format
+        self.recv_header_len = struct.calcsize(recv_header_format)
+
+    def Send(self, command_id, data=b'', size=0):
+        """Send/buffer FileSync packets.
+
+        Packets are buffered and only flushed when this connection is read from. All
+        messages have a response from the device, so this will always get flushed.
+
+        Args:
+          command_id: Command to send.
+          data: Optional data to send, must set data or size.
+          size: Optionally override size from len(data).
+        """
+        if data:
+            if not isinstance(data, bytes):
+                data = data.encode('utf8')
+            size = len(data)
+
+        if not self._CanAddToSendBuffer(len(data)):
+            self._Flush()
+        buf = struct.pack(b'<2I', self.id_to_wire[command_id], size) + data
+        self.send_buffer[self.send_idx:self.send_idx + len(buf)] = buf
+        self.send_idx += len(buf)
+
+    def Read(self, expected_ids, read_data=True):
+        """Read ADB messages and return FileSync packets."""
+        if self.send_idx:
+            self._Flush()
+
+        # Read one filesync packet off the recv buffer.
+        header_data = self._ReadBuffered(self.recv_header_len)
+        header = struct.unpack(self.recv_header_format, header_data)
+        # Header is (ID, ...).
+        command_id = self.wire_to_id[header[0]]
+
+        if command_id not in expected_ids:
+            if command_id == b'FAIL':
+                reason = ''
+                if self.recv_buffer:
+                    reason = self.recv_buffer.decode('utf-8', errors='ignore')
+                raise usb_exceptions.AdbCommandFailureException('Command failed: {}'.format(reason))
+            raise adb_protocol.InvalidResponseError(
+                'Expected one of %s, got %s' % (expected_ids, command_id))
+
+        if not read_data:
+            return command_id, header[1:]
+
+        # Header is (ID, ..., size).
+        size = header[-1]
+        data = self._ReadBuffered(size)
+        return command_id, header[1:-1], data
+
+    def ReadUntil(self, expected_ids, *finish_ids):
+        """Useful wrapper around Read."""
+        while True:
+            cmd_id, header, data = self.Read(expected_ids + finish_ids)
+            yield cmd_id, header, data
+            if cmd_id in finish_ids:
+                break
+
+    def _CanAddToSendBuffer(self, data_len):
+        added_len = self.send_header_len + data_len
+        return self.send_idx + added_len < adb_protocol.MAX_ADB_DATA
+
+    def _Flush(self):
+        try:
+            self.adb.Write(self.send_buffer[:self.send_idx])
+        except libusb1.USBError as e:
+            raise adb_protocol.SendFailedError(
+                'Could not send data %s' % self.send_buffer, e)
+        self.send_idx = 0
+
+    def _ReadBuffered(self, size):
+        # Ensure recv buffer has enough data.
+        while len(self.recv_buffer) < size:
+            _, data = self.adb.ReadUntil(b'WRTE')
+            self.recv_buffer += data
+
+        result = self.recv_buffer[:size]
+        self.recv_buffer = self.recv_buffer[size:]
+        return result

+ 33 - 0
adb/sign_m2crypto.py

@@ -0,0 +1,33 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from M2Crypto import RSA
+
+from adb import adb_protocol
+
+
+class M2CryptoSigner(adb_protocol.AuthSigner):
+    """AuthSigner using M2Crypto."""
+
+    def __init__(self, rsa_key_path):
+        with open(rsa_key_path + '.pub') as rsa_pub_file:
+            self.public_key = rsa_pub_file.read()
+
+        self.rsa_key = RSA.load_key(rsa_key_path)
+
+    def Sign(self, data):
+        return self.rsa_key.sign(data, 'sha1')
+
+    def GetPublicKey(self):
+        return self.public_key

+ 25 - 0
adb/sign_pycryptodome.py

@@ -0,0 +1,25 @@
+from adb import adb_protocol
+
+from Crypto.Hash import SHA256
+from Crypto.PublicKey import RSA
+from Crypto.Signature import pkcs1_15
+
+
+class PycryptodomeAuthSigner(adb_protocol.AuthSigner):
+
+    def __init__(self, rsa_key_path=None):
+        super(PycryptodomeAuthSigner, self).__init__()
+
+        if rsa_key_path:
+            with open(rsa_key_path + '.pub', 'rb') as rsa_pub_file:
+                self.public_key = rsa_pub_file.read()
+
+            with open(rsa_key_path, 'rb') as rsa_priv_file:
+                self.rsa_key = RSA.import_key(rsa_priv_file.read())
+
+    def Sign(self, data):
+        h = SHA256.new(data)
+        return pkcs1_15.new(self.rsa_key).sign(h)
+
+    def GetPublicKey(self):
+        return self.public_key

+ 77 - 0
adb/sign_pythonrsa.py

@@ -0,0 +1,77 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import rsa
+
+from pyasn1.codec.der import decoder
+from pyasn1.type import univ
+from rsa import pkcs1
+
+
+# python-rsa lib hashes all messages it signs. ADB does it already, we just
+# need to slap a signature on top of already hashed message. Introduce "fake"
+# hashing algo for this.
+class _Accum(object):
+    def __init__(self):
+        self._buf = b''
+
+    def update(self, msg):
+        self._buf += msg
+
+    def digest(self):
+        return self._buf
+
+
+pkcs1.HASH_METHODS['SHA-1-PREHASHED'] = _Accum
+pkcs1.HASH_ASN1['SHA-1-PREHASHED'] = pkcs1.HASH_ASN1['SHA-1']
+
+
+def _load_rsa_private_key(pem):
+    """PEM encoded PKCS#8 private key -> rsa.PrivateKey."""
+    # ADB uses private RSA keys in pkcs#8 format. 'rsa' library doesn't support
+    # them natively. Do some ASN unwrapping to extract naked RSA key
+    # (in der-encoded form). See https://www.ietf.org/rfc/rfc2313.txt.
+    # Also http://superuser.com/a/606266.
+    try:
+        der = rsa.pem.load_pem(pem, 'PRIVATE KEY')
+        keyinfo, _ = decoder.decode(der)
+        if keyinfo[1][0] != univ.ObjectIdentifier(
+                '1.2.840.113549.1.1.1'):  # pragma: no cover
+            raise ValueError('Not a DER-encoded OpenSSL private RSA key')
+        private_key_der = keyinfo[2].asOctets()
+    except IndexError:  # pragma: no cover
+        raise ValueError('Not a DER-encoded OpenSSL private RSA key')
+    return rsa.PrivateKey.load_pkcs1(private_key_der, format='DER')
+
+
+class PythonRSASigner(object):
+    """Implements adb_protocol.AuthSigner using http://stuvel.eu/rsa."""
+
+    @classmethod
+    def FromRSAKeyPath(cls, rsa_key_path):
+        with open(rsa_key_path + '.pub') as f:
+            pub = f.read()
+        with open(rsa_key_path) as f:
+            priv = f.read()
+        return cls(pub, priv)
+
+    def __init__(self, pub=None, priv=None):
+        self.priv_key = _load_rsa_private_key(priv)
+        self.pub_key = pub
+
+    def Sign(self, data):
+        return rsa.sign(data, self.priv_key, 'SHA-1-PREHASHED')
+
+    def GetPublicKey(self):
+        return self.pub_key

+ 76 - 0
adb/usb_exceptions.py

@@ -0,0 +1,76 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Common exceptions for ADB and Fastboot."""
+
+
+class CommonUsbError(Exception):
+    """Base class for usb communication errors."""
+
+
+class FormatMessageWithArgumentsException(CommonUsbError):
+    """Exception that both looks good and is functional.
+
+    Okay, not that kind of functional, it's still a class.
+
+    This interpolates the message with the given arguments to make it
+    human-readable, but keeps the arguments in case other code try-excepts it.
+    """
+
+    def __init__(self, message, *args):
+        message %= args
+        super(FormatMessageWithArgumentsException, self).__init__(message, *args)
+
+
+class DeviceNotFoundError(FormatMessageWithArgumentsException):
+    """Device isn't on USB."""
+
+
+class DeviceAuthError(FormatMessageWithArgumentsException):
+    """Device authentication failed."""
+
+
+class LibusbWrappingError(CommonUsbError):
+    """Wraps libusb1 errors while keeping its original usefulness.
+
+    Attributes:
+      usb_error: Instance of libusb1.USBError
+    """
+
+    def __init__(self, msg, usb_error):
+        super(LibusbWrappingError, self).__init__(msg)
+        self.usb_error = usb_error
+
+    def __str__(self):
+        return '%s: %s' % (
+            super(LibusbWrappingError, self).__str__(), str(self.usb_error))
+
+
+class WriteFailedError(LibusbWrappingError):
+    """Raised when the device doesn't accept our command."""
+
+
+class ReadFailedError(LibusbWrappingError):
+    """Raised when the device doesn't respond to our commands."""
+
+
+class AdbCommandFailureException(Exception):
+    """ADB Command returned a FAIL."""
+
+
+class AdbOperationException(Exception):
+    """Failed to communicate over adb with device after multiple retries."""
+
+
+class TcpTimeoutException(FormatMessageWithArgumentsException):
+    """TCP connection timed out in the time out given."""

+ 25 - 0
client.py

@@ -0,0 +1,25 @@
+from adb import adb_commands
+from adb import sign_m2crypto
+import sys
+
+queue = list()
+
+# Connect to the device
+device = adb_commands.AdbCommands()
+device.ConnectDevice(port_path=None, serial="192.168.43.168:5555")
+
+if sys.argv[1] == 'ls':
+	root = device.List(sys.argv[2])
+
+	for i in root:
+		print(i[0].decode('utf-8') + ' Perm: ' + str(oct(i[1])) + ' Size: ' + str(i[2]))
+
+if sys.argv[1] == 'download':
+	root = device.Pull(sys.argv[2], sys.argv[2].replace('/', '_'))
+	print(root)
+
+if sys.argv[1] == 'upload':
+	root = device.Push(sys.argv[2], sys.argv[3])
+	#root = device.Push(sys.argv[2], "/opt/testbin")
+
+	print(root)

BIN
original/xcb


BIN
original/xcb_user_manual_v1.0.0_.pdf