From 70fd2dd131ac80677821135299a4136ab0516b6a Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Wed, 31 Jan 2018 13:26:54 -0500 Subject: [PATCH 01/38] Added pycharm .idea/ folder to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 3f03a81..e7a1366 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ adb.egg-info/ .tox/ /adb.zip /fastboot.zip +.idea/ \ No newline at end of file From 6e4e466dac400cd79690bcdedf78d5024bd518be Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Wed, 31 Jan 2018 13:27:52 -0500 Subject: [PATCH 02/38] Added authsigner for pycryptodome --- adb/sign_pycryptodome.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 adb/sign_pycryptodome.py diff --git a/adb/sign_pycryptodome.py b/adb/sign_pycryptodome.py new file mode 100644 index 0000000..1a56f6a --- /dev/null +++ b/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 From 54b3ed8cb9c595a4dd07e5739cf2667106d7e444 Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Wed, 31 Jan 2018 13:32:54 -0500 Subject: [PATCH 03/38] Added support for retrieving the reason for a command failure --- adb/filesync_protocol.py | 359 ++++++++++++++++++++------------------- 1 file changed, 183 insertions(+), 176 deletions(-) diff --git a/adb/filesync_protocol.py b/adb/filesync_protocol.py index 97e2c75..57eb0ee 100644 --- a/adb/filesync_protocol.py +++ b/adb/filesync_protocol.py @@ -22,7 +22,10 @@ import struct import time -import libusb1 +try: + import libusb1 +except: + pass from adb import adb_protocol from adb import usb_exceptions @@ -30,19 +33,19 @@ # 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 +MAX_PUSH_DATA = 2 * 1024 class InvalidChecksumError(Exception): - """Checksum of data didn't match expected checksum.""" + """Checksum of data didn't match expected checksum.""" class InterleavedDataError(Exception): - """We only support command sent serially.""" + """We only support command sent serially.""" class PushFailedError(Exception): - """Pushing a file failed for some reason.""" + """Pushing a file failed for some reason.""" DeviceFile = collections.namedtuple('DeviceFile', [ @@ -50,177 +53,181 @@ class PushFailedError(Exception): 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): - """Pull a file from the device into the file-like dest_file.""" - cnxn = FileSyncConnection(connection, b'<2I') - 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) - - @classmethod - def Push(cls, connection, datafile, filename, - st_mode=DEFAULT_PUSH_MODE, mtime=0): - """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 - - Raises: - PushFailedError: Raised on push failure. - """ - if not isinstance(filename, bytes): - filename = filename.encode('utf8') - fileinfo = b'%s,%d' % (filename, st_mode) - - cnxn = FileSyncConnection(connection, b'<2I') - cnxn.Send(b'SEND', fileinfo) - - while True: - data = datafile.read(MAX_PUSH_DATA) - if not data: - break - cnxn.Send(b'DATA', data) - - 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) + """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): + """Pull a file from the device into the file-like dest_file.""" + cnxn = FileSyncConnection(connection, b'<2I') + 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) + + @classmethod + def Push(cls, connection, datafile, filename, + st_mode=DEFAULT_PUSH_MODE, mtime=0): + """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 + + 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) + + while True: + data = datafile.read(MAX_PUSH_DATA) + if not data: + break + cnxn.Send(b'DATA', data) + + 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': - raise usb_exceptions.AdbCommandFailureException('Command failed.') - 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 - + """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 isinstance(data, str): + 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: + try: + reason = self.recv_buffer.decode('utf-8') + except: + pass + 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 From fd3026560392d0851d6ac954a81d0eab3267fedd Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Wed, 31 Jan 2018 13:56:38 -0500 Subject: [PATCH 04/38] Revised AdbCommands model to be an instantiated class. Fixed misc bugs and python3 compatibility issues --- adb/adb_commands.py | 501 ++++++++++++++----------- adb/adb_debug.py | 299 ++++++++------- adb/adb_protocol.py | 872 ++++++++++++++++++++++++++------------------ adb/common.py | 593 +++++++++++++++--------------- adb/common_cli.py | 15 +- 5 files changed, 1297 insertions(+), 983 deletions(-) mode change 100755 => 100644 adb/adb_debug.py diff --git a/adb/adb_commands.py b/adb/adb_commands.py index de1516b..35246fb 100644 --- a/adb/adb_commands.py +++ b/adb/adb_commands.py @@ -30,7 +30,7 @@ from adb import common from adb import filesync_protocol -# From adb.h +# from adb.h CLASS = 0xFF SUBCLASS = 0x42 PROTOCOL = 0x01 @@ -38,210 +38,299 @@ 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): + protocol_handler = adb_protocol.AdbMessage + filesync_handler = filesync_protocol.FilesyncProtocol -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 - - @classmethod - def ConnectDevice( - cls, port_path=None, serial=None, default_timeout_ms=None, **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. - - If serial specifies a TCP address:port, then a TCP connection is - used instead of a USB connection. - """ - if serial and b':' in serial: - handle = common.TcpHandle(serial, timeout_ms=default_timeout_ms) - else: - handle = common.UsbHandle.FindAndOpen( - DeviceIsAvailable, port_path=port_path, serial=serial, - timeout_ms=default_timeout_ms) - return cls.Connect(handle, **kwargs) - - def __init__(self, handle, device_state): - self.handle = handle - self._device_state = device_state - - def Close(self): - self.handle.Close() - - @classmethod - def Connect(cls, usb, banner=None, **kwargs): - """Connect to the device. - - Args: - usb: UsbHandle or TcpHandle instance to use. - banner: See protocol_handler.Connect. - **kwargs: See protocol_handler.Connect for kwargs. Includes rsa_keys, - and auth_timeout_ms. - Returns: - An instance of this class if the device connected successfully. - """ - if not banner: - banner = socket.gethostname().encode() - device_state = cls.protocol_handler.Connect(usb, banner=banner, **kwargs) - # Remove banner and colons after device state (state::banner) - device_state = device_state.split(b':')[0] - return cls(usb, device_state) - - @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='', timeout_ms=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. - timeout_ms: Expected timeout for pushing and installing. - - Returns: - The pm install output. - """ - if not destination_dir: - destination_dir = '/data/local/tmp/' - basename = os.path.basename(apk_path) - destination_path = destination_dir + basename - self.Push(apk_path, destination_path, timeout_ms=timeout_ms) - return self.Shell('pm install -r "%s"' % destination_path, - timeout_ms=timeout_ms) - - def Push(self, source_file, device_filename, mtime='0', timeout_ms=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. - """ - 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) - return - source_file = open(source_file) - - connection = self.protocol_handler.Open( - self.handle, destination=b'sync:', timeout_ms=timeout_ms) - self.filesync_handler.Push(connection, source_file, device_filename, - mtime=int(mtime)) - connection.Close() - - def Pull(self, device_filename, dest_file='', timeout_ms=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. - - Returns: - The file data if dest_file is not set. - """ - if not dest_file: - dest_file = io.BytesIO() - elif isinstance(dest_file, str): - dest_file = open(dest_file, 'wb') - connection = self.protocol_handler.Open( - self.handle, destination=b'sync:', - timeout_ms=timeout_ms) - self.filesync_handler.Pull(connection, device_filename, dest_file) - connection.Close() - if isinstance(dest_file, io.BytesIO): - return dest_file.getvalue() - - def Stat(self, device_filename): - """Get a file's stat() information.""" - connection = self.protocol_handler.Open(self.handle, destination=b'sync:') - 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'sync:') - 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 Shell(self, command, timeout_ms=None): - """Run command on the device, returning the output.""" - 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'. - """ - return self.StreamingShell('logcat %s' % options, timeout_ms) + def __init__(self): + + self.__reset() + + def __reset(self): + self.handle = None + self._device_state = None + + # Connection table tracks each open AdbConnection objects per service type + # By default, the only service connections that make sense to hold open are (interactive) shell and sync + self._service_connections = { + b'shell:': None, + #b'sync': None + } + + 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 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. + + If serial specifies a TCP address:port, then a TCP connection is + used instead of a USB connection. + """ + if serial and b':' 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 + + self.handle.Close() + self.__reset() + + def __Connect(self, banner=None, **kwargs): + """Connect to the device. + + Args: + usb: UsbHandle or TcpHandle instance to use. + banner: See protocol_handler.Connect. + **kwargs: See protocol_handler.Connect for kwargs. Includes 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'::') + + device_state = parts[0] + + # Break out the build prop info + build_props = str(parts[1].split(b';')) + #print("Device build props: {}".format(build_props)) + + self._device_state = device_state + + 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='', timeout_ms=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. + timeout_ms: Expected timeout for pushing and installing. + + Returns: + The pm install output. + """ + if not destination_dir: + destination_dir = '/data/local/tmp/' + basename = os.path.basename(apk_path) + destination_path = destination_dir + basename + self.Push(apk_path, destination_path, timeout_ms=timeout_ms) + + return self.Shell(command='pm install -r "%s"' % destination_path) + + def Push(self, source_file, device_filename, mtime='0', timeout_ms=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. + """ + + 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) + return + source_file = open(source_file, 'rb') + + with source_file: + connection = self.protocol_handler.Open( + self.handle, destination=b'sync:', timeout_ms=timeout_ms) + self.filesync_handler.Push(connection, source_file, device_filename, + mtime=int(mtime)) + connection.Close() + + def Pull(self, device_filename, dest_file=None, timeout_ms=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. + + 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, 'w') + + else: + raise ValueError("destfile is of unknown type") + + #conn = self._get_service_connection(b'sync:') + conn = self.protocol_handler.Open(self.handle, destination=b'sync:') + + self.filesync_handler.Pull(conn, device_filename, dest_file) + + conn.Close() + + if isinstance(dest_file, io.BytesIO): + return dest_file.getvalue() + else: + dest_file.close() + return os.path.exists(dest_file) + + def Stat(self, device_filename): + """Get a file's stat() information.""" + connection = self.protocol_handler.Open(self.handle, destination=b'sync:') + 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'sync:') + 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'remount') + + 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, command=None, strip_command=True, delimiter=None, strip_delimiter=True): + """Get stdout from the currently open interactive shell and optionally run a command + on the device, returning all output. + + Args: + command: Optional. Command to run on the target. + strip_command: Optional (default True). Strip command name from stdout. + delimiter: Optional. Delimiter to look for in the output to know when to stop expecting more output + (usually the shell prompt) + strip_delimiter: 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, command=command, strip_command=strip_command, + delimiter=delimiter, strip_delimiter=strip_delimiter) diff --git a/adb/adb_debug.py b/adb/adb_debug.py old mode 100755 new mode 100644 index 7eb0970..ec88c70 --- a/adb/adb_debug.py +++ b/adb/adb_debug.py @@ -27,152 +27,185 @@ from adb import common_cli try: - from adb import sign_m2crypto - rsa_signer = sign_m2crypto.M2CryptoSigner + 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: - rsa_signer = None + 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(self, device_path): - """Prints a directory listing. - - Args: - device_path: Directory to list. - """ - files = adb_commands.AdbCommands.List(self, 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) + """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(self, *options): - return adb_commands.AdbCommands.Logcat(self, ' '.join(options)) +def Logcat(device, *options): + return device.Logcat(' '.join(options)) -def Shell(self, *command): - """Runs a command on the device and prints the stdout. +def Shell(device, *command): + """Runs a command on the device and prints the stdout. - Args: - command: Command to run on the target. - """ - return adb_commands.AdbCommands.StreamingShell(self, ' '.join(command)) + Args: + command: Command to run on the target. + """ + if command: -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, 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, 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 or python-rsa') - # 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 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: - return common_cli.StartCli( - args, - adb_commands.AdbCommands.ConnectDevice, - auth_timeout_ms=args.auth_timeout_s * 1000, - rsa_keys=[rsa_signer(path) for path in args.rsa_key_path]) + cmd = input('> ') + if not cmd: + continue + elif cmd == 'exit': + break + else: + print(device.InteractiveShell(cmd, strip_command=True, delimiter=terminal_prompt, strip_delimiter=True)) + print(terminal_prompt.decode('utf-8')) + + 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, 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, 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()) + sys.exit(main()) diff --git a/adb/adb_protocol.py b/adb/adb_protocol.py index 3dc92d6..b06fd7a 100644 --- a/adb/adb_protocol.py +++ b/adb/adb_protocol.py @@ -19,10 +19,11 @@ import struct import time - +from future.utils import iteritems +from io import BytesIO +from future.utils import iteritems from adb import usb_exceptions - # Maximum amount of data in an ADB packet. MAX_ADB_DATA = 4096 # ADB protocol version. @@ -34,378 +35,555 @@ 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.""" + """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) + 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.""" + """Got an invalid response to our command.""" class InvalidChecksumError(Exception): - """Checksum of data didn't match expected checksum.""" + """Checksum of data didn't match expected checksum.""" class InterleavedDataError(Exception): - """We only support command sent serially.""" + """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 + 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 iteritems(id_to_wire)} + return id_to_wire, wire_to_id class AuthSigner(object): - """Signer for use with authenticated ADB, introduced in 4.4.x/KitKat.""" + """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 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() + 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': + """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) - 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) + 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) - 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. - """ - 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)) - - signed_token = rsa_key.Sign(str(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 %s, got %s' % (local_id, their_local_id)) - if cmd == b'CLSE': - # Device doesn't support this service. - return None - if cmd != b'OKAY': - raise InvalidCommandError('Expected a ready response, got %s' % 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. + """ADB Protocol and message class. - 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. + Protocol Notes - 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. + 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: - Raises: - InterleavedDataError: Multiple streams running over usb. - InvalidCommandError: Got an unexpected response command. - - Yields: - The responses from the service. + OPEN(host_id, 0, 'shell:XXX') + READY/OKAY(device_id, host_id, '') + WRITE(0, host_id, 'data') + CLOSE(device_id, host_id, '') """ - if isinstance(command, str): - 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') + + 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. + """ + 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)) + + # MODIFIED: 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 %s, got %s' % (local_id, their_local_id)) + if cmd == b'CLSE': + # Device doesn't support this service. + return None + if cmd != b'OKAY': + raise InvalidCommandError('Expected a ready response, got %s' % 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 isinstance(command, str): + 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, connection, command=None, strip_command=True, delimiter=None, strip_delimiter=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: + connection: Instance of AdbConnection + command: Optional. Command to run on the target. + strip_command: Optional (default True). Strip command name from stdout. + delimiter: Optional. Delimiter to look for in the output to know when to stop expecting more output + (usually the shell prompt) + strip_delimiter: 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 isinstance(delimiter, str): + delimiter = delimiter.encode('utf-8') + + stdout = '' + stdout_stream = BytesIO() + original_command = '' + + try: + + if command: + original_command = str(command) + command += '\r' # Required. Send a carriage return right after the command + command = command.encode('utf8') + + # Send the command raw + bytes_written = connection.Write(command) + + if delimiter: + # Expect multiple WRTE commands until the delimiter (usually terminal prompt) is detected + + data = b'' + while delimiter not in data: + + cmd, data = connection.ReadUntil(b'WRTE') + stdout_stream.write(data) + + else: + # Otherwise, expect only a single WRTE + cmd, data = connection.ReadUntil(b'WRTE') + + # WRTE command from device will follow with stdout data + stdout_stream.write(data) + + else: + + # No command provided means we should just expect a single line from the terminal. Use this sparingly + cmd, data = connection.ReadUntil(b'WRTE') + if cmd == b'WRTE': + # WRTE command from device will follow with stdout data + stdout_stream.write(data) + else: + print("Unhandled command 2") + + # print("DATA") + # print(data) + # print(binascii.hexlify(data)) + # print("=================") + + cleaned_stdout_stream = BytesIO() + if clean_stdout: + stdout_bytes = stdout_stream.getvalue() + + bsruns = {} + 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:]) + + # first_backspace_pos = stdout_bytes.find(b'\x08') + # if first_backspace_pos == -1: + # cleaned_stdout_stream.write(stdout_bytes) + # else: + # end_backspaces_pos = first_backspace_pos + # while True: + # if chr(stdout_bytes[end_backspaces_pos]) == '\b': + # end_backspaces_pos += 1 + # else: + # break + # + # num_backspaces = end_backspaces_pos - first_backspace_pos + # + # # Ignore num_backspaces number of characters prior to first_backspace_pos + # cleaned_stdout_stream.write(stdout_bytes[:(first_backspace_pos-num_backspaces)]) + # cleaned_stdout_stream.write(stdout_bytes[end_backspaces_pos:]) + + else: + cleaned_stdout_stream.write(stdout_stream.getvalue()) + + stdout = cleaned_stdout_stream.getvalue() + + # print("STDOUT") + # print(stdout) + # print(binascii.hexlify(stdout)) + # print("=================") + + # Strip original command that will come back in stdout + + if original_command and strip_command: + findstr = original_command.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 delimiter if requested + if delimiter and strip_delimiter: + # Sometimes, a number preceeds the terminal delimiter which we need to check for an strip out + # eg. "123|dreamlte:/ $" + + # number_delim = r'(?P\d{1,3}\|' + delimiter.decode('utf-8') + ")" + # + # reg = re.compile(number_delim) + # m = reg.match(stdout.decode('utf-8')) + # if m: + # stdout = stdout.replace(m.group('new_delim'), b'') + # else: + # stdout = stdout.replace(delimiter, b'') + + stdout = stdout.replace(delimiter, b'') + + stdout = stdout.rstrip() + + except Exception as e: + print("InteractiveShell exception (most likely timeout): {}".format(e)) + + return stdout diff --git a/adb/common.py b/adb/common.py index afc8f47..bae8ddd 100644 --- a/adb/common.py +++ b/adb/common.py @@ -21,314 +21,327 @@ import weakref import select -import libusb1 -import usb1 +try: + import libusb1 + import usb1 +except: + libusb1 = usb1 = None from adb import usb_exceptions -DEFAULT_TIMEOUT_MS = 1000 +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()) + """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 + """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 -class UsbHandle(object): - """USB communication object. Not thread-safe. - - Handles reading and writing over USB with the proper endpoints, exceptions, - and interface claiming. + return Matcher - Important methods: - FlushBuffers() - BulkRead(int length) - BulkWrite(bytes data) - """ - _HANDLE_CACHE = weakref.WeakValueDictionary() - _HANDLE_CACHE_LOCK = threading.Lock() +class UsbHandle(object): + """USB communication object. Not thread-safe. - def __init__(self, device, setting, usb_info=None, timeout_ms=None): - """Initialize USB Handle. + Handles reading and writing over USB with the proper endpoints, exceptions, + and interface claiming. - 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. + Important methods: + FlushBuffers() + BulkRead(int length) + BulkWrite(bytes data) """ - self._setting = setting - self._device = device - self._handle = None - - self._usb_info = usb_info or '' - self._timeout_ms = timeout_ms or DEFAULT_TIMEOUT_MS - - @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 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) - - @classmethod - def PortPathMatcher(cls, port_path): - """Returns a device matcher for the given port path.""" - if isinstance(port_path, basestring): - # 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 + _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 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 b':' in serial: - (host, port) = serial.split(b':') - else: - host = serial - port = 5555 - self._serial_number = '%s:%s' % (host, port) - self._timeout_ms = float(timeout_ms) if timeout_ms else None - timeout = self.TimeoutSeconds(self._timeout_ms) - self._connection = socket.create_connection((host, 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() +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 b':' in serial: + (host, port) = serial.split(b':') + else: + host = serial + port = 5555 + self._serial_number = '%s:%s' % (host, port) + self._timeout_ms = float(timeout_ms) if timeout_ms else None + timeout = self.TimeoutSeconds(self._timeout_ms) + self._connection = socket.create_connection((host, port), timeout=timeout) + if timeout: + self._connection.setblocking(0) + + self._connection = socket.create_connection((host, port)) + + @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() diff --git a/adb/common_cli.py b/adb/common_cli.py index 22dfdd8..322492a 100644 --- a/adb/common_cli.py +++ b/adb/common_cli.py @@ -25,8 +25,8 @@ import logging import re import sys -import textwrap import types +from future.builtins import range from adb import usb_exceptions @@ -142,17 +142,18 @@ def _RunMethod(dev, args, extra): return 0 -def StartCli(args, device_factory, extra=None, **device_kwargs): +def StartCli(args, adb_commands, extra=None, **device_kwargs): """Starts a common CLI interface for this usb path and protocol.""" try: - dev = device_factory( - port_path=args.port_path, serial=args.serial, - default_timeout_ms=args.timeout_ms, **device_kwargs) + + 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 >> sys.stderr, 'No device found: %s' % e + print('No device found: {}'.format(e), file=sys.stderr) return 1 except usb_exceptions.CommonUsbError as e: - print >> sys.stderr, 'Could not connect to device: %s' % e + print('Could not connect to device: {}'.format(e), file=sys.stderr) return 1 try: return _RunMethod(dev, args, extra or {}) From 3f7eefb0e672f0c1aefc2301264107b6e0ccfe74 Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Sat, 3 Feb 2018 15:08:12 -0500 Subject: [PATCH 05/38] Merge adb_test and CONTRIBUTORS from master --- CONTRIBUTORS | 1 + test/adb_test.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index c873f04..ac85816 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -4,3 +4,4 @@ Simon Ye Jamey Hicks Marc-Antoine Ruel Max Borghino +Mohammad Abu-Garbeyyeh diff --git a/test/adb_test.py b/test/adb_test.py index 06cdce9..1b8800c 100755 --- a/test/adb_test.py +++ b/test/adb_test.py @@ -116,6 +116,15 @@ def testBigResponseShell(self): self.assertEqual(b''.join(responses).decode('utf8'), adb_commands.Shell(command)) + def testUninstall(self): + package_name = "com.test.package" + response = 'Success' + + usb = self._ExpectCommand(b'shell', ('pm uninstall "%s"' % package_name).encode('utf8'), response) + + adb_commands = self._Connect(usb) + self.assertEquals(response, adb_commands.Uninstall(package_name)) + def testStreamingResponseShell(self): command = b'keepin it real big' # expect multiple lines @@ -151,6 +160,15 @@ def testRoot(self): adb_commands = self._Connect(usb) adb_commands.Root() + def testEnableVerity(self): + usb = self._ExpectCommand(b'enable-verity', b'', b'') + adb_commands = self._Connect(usb) + adb_commands.EnableVerity() + + def testDisableVerity(self): + usb = self._ExpectCommand(b'disable-verity', b'', b'') + adb_commands = self._Connect(usb) + adb_commands.DisableVerity() class FilesyncAdbTest(BaseAdbTest): From 0eca1c7742fad86c20b4f7c3ab8cdb5c9bb3a55e Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Sat, 3 Feb 2018 15:10:17 -0500 Subject: [PATCH 06/38] Additional formatting cleanup for common_cli --- adb/common_cli.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/adb/common_cli.py b/adb/common_cli.py index 322492a..e2f217e 100644 --- a/adb/common_cli.py +++ b/adb/common_cli.py @@ -26,7 +26,6 @@ import re import sys import types -from future.builtins import range from adb import usb_exceptions @@ -145,10 +144,8 @@ def _RunMethod(dev, args, extra): 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 = 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 From a3b6c225fa3e1a0cba39cf18a47bcf027fbb495a Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Sat, 3 Feb 2018 15:15:53 -0500 Subject: [PATCH 07/38] Fixed formatting in filesync_protocol --- adb/filesync_protocol.py | 361 +++++++++++++++++++-------------------- 1 file changed, 179 insertions(+), 182 deletions(-) diff --git a/adb/filesync_protocol.py b/adb/filesync_protocol.py index 57eb0ee..3754a8a 100644 --- a/adb/filesync_protocol.py +++ b/adb/filesync_protocol.py @@ -22,10 +22,7 @@ import struct import time -try: - import libusb1 -except: - pass +import libusb1 from adb import adb_protocol from adb import usb_exceptions @@ -37,15 +34,15 @@ class InvalidChecksumError(Exception): - """Checksum of data didn't match expected checksum.""" + """Checksum of data didn't match expected checksum.""" class InterleavedDataError(Exception): - """We only support command sent serially.""" + """We only support command sent serially.""" class PushFailedError(Exception): - """Pushing a file failed for some reason.""" + """Pushing a file failed for some reason.""" DeviceFile = collections.namedtuple('DeviceFile', [ @@ -53,181 +50,181 @@ class PushFailedError(Exception): 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): - """Pull a file from the device into the file-like dest_file.""" - cnxn = FileSyncConnection(connection, b'<2I') - 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) - - @classmethod - def Push(cls, connection, datafile, filename, - st_mode=DEFAULT_PUSH_MODE, mtime=0): - """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 - - 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) - - while True: - data = datafile.read(MAX_PUSH_DATA) - if not data: - break - cnxn.Send(b'DATA', data) - - 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) + """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): + """Pull a file from the device into the file-like dest_file.""" + cnxn = FileSyncConnection(connection, b'<2I') + 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) + + @classmethod + def Push(cls, connection, datafile, filename, + st_mode=DEFAULT_PUSH_MODE, mtime=0): + """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 + + 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) + + while True: + data = datafile.read(MAX_PUSH_DATA) + if not data: + break + cnxn.Send(b'DATA', data) + + 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 isinstance(data, str): - 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: - try: - reason = self.recv_buffer.decode('utf-8') - except: - pass - 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 + """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 isinstance(data, str): + 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: + try: + reason = self.recv_buffer.decode('utf-8') + except: + pass + 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 From 7b1c1159e59f79c07e180ea1571fe89f596c2cfb Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Sat, 3 Feb 2018 15:23:27 -0500 Subject: [PATCH 08/38] common.py merges with master and spacing fixes --- adb/common.py | 597 +++++++++++++++++++++++++------------------------- 1 file changed, 295 insertions(+), 302 deletions(-) diff --git a/adb/common.py b/adb/common.py index bae8ddd..2c95f8e 100644 --- a/adb/common.py +++ b/adb/common.py @@ -16,16 +16,14 @@ Common usb browsing, and usb communication. """ import logging +import platform import socket import threading import weakref import select -try: - import libusb1 - import usb1 -except: - libusb1 = usb1 = None +import libusb1 +import usb1 from adb import usb_exceptions @@ -35,313 +33,308 @@ def GetInterface(setting): - """Get the class, subclass, and protocol for the given USB setting.""" - return (setting.getClass(), setting.getSubClass(), setting.getProtocol()) + """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) + """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 - 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. -class UsbHandle(object): - """USB communication object. Not thread-safe. + Important methods: + FlushBuffers() + BulkRead(int length) + BulkWrite(bytes data) + """ - Handles reading and writing over USB with the proper endpoints, exceptions, - and interface claiming. + _HANDLE_CACHE = weakref.WeakValueDictionary() + _HANDLE_CACHE_LOCK = threading.Lock() - Important methods: - FlushBuffers() - BulkRead(int length) - BulkWrite(bytes data) - """ + def __init__(self, device, setting, usb_info=None, timeout_ms=None): + """Initialize USB Handle. - _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 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 + 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 b':' in serial: - (host, port) = serial.split(b':') - else: - host = serial - port = 5555 - self._serial_number = '%s:%s' % (host, port) - self._timeout_ms = float(timeout_ms) if timeout_ms else None - timeout = self.TimeoutSeconds(self._timeout_ms) - self._connection = socket.create_connection((host, port), timeout=timeout) - if timeout: - self._connection.setblocking(0) - - self._connection = socket.create_connection((host, port)) - - @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() + """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 b':' in serial: + (host, port) = serial.split(b':') + else: + host = serial + port = 5555 + self._serial_number = '%s:%s' % (host, port) + self._timeout_ms = float(timeout_ms) if timeout_ms else None + timeout = self.TimeoutSeconds(self._timeout_ms) + self._connection = socket.create_connection((host, 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() From 7b39bc3c23249165057704dd1fbf65fd37b0951e Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Sat, 3 Feb 2018 15:45:20 -0500 Subject: [PATCH 09/38] Fixed spacing issues in adb_protocol. Removed unused code --- adb/adb_protocol.py | 934 +++++++++++++++++++++----------------------- 1 file changed, 444 insertions(+), 490 deletions(-) diff --git a/adb/adb_protocol.py b/adb/adb_protocol.py index b06fd7a..0ccecbf 100644 --- a/adb/adb_protocol.py +++ b/adb/adb_protocol.py @@ -19,7 +19,6 @@ import struct import time -from future.utils import iteritems from io import BytesIO from future.utils import iteritems from adb import usb_exceptions @@ -37,553 +36,508 @@ 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 + first_backspace_pos = stdout_bytes[start_pos:].find(b'\x08') + if first_backspace_pos == -1: + return -1, 0 - num_backspaces = end_backspace_pos - (start_pos + first_backspace_pos) + 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 - return (start_pos + first_backspace_pos), num_backspaces + 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.""" + """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) + 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.""" + """Got an invalid response to our command.""" class InvalidChecksumError(Exception): - """Checksum of data didn't match expected checksum.""" + """Checksum of data didn't match expected checksum.""" class InterleavedDataError(Exception): - """We only support command sent serially.""" + """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 iteritems(id_to_wire)} - return id_to_wire, wire_to_id + 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 iteritems(id_to_wire)} + return id_to_wire, wire_to_id class AuthSigner(object): - """Signer for use with authenticated ADB, introduced in 4.4.x/KitKat.""" + """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 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() + 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): + """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) - 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) + 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. + """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. + """ + 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': + # Device doesn't support this service. + 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)) - Protocol Notes + @classmethod + def StreamingCommand(cls, usb, service, command='', timeout_ms=None): + """One complete set of USB packets for a single command. - 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: + 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. - OPEN(host_id, 0, 'shell:XXX') - READY/OKAY(device_id, host_id, '') - WRITE(0, host_id, 'data') - CLOSE(device_id, host_id, '') - """ + 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. - 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)) + Raises: + InterleavedDataError: Multiple streams running over usb. + InvalidCommandError: Got an unexpected response command. - 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. - """ - 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)) - - # MODIFIED: 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 %s, got %s' % (local_id, their_local_id)) - if cmd == b'CLSE': - # Device doesn't support this service. - return None - if cmd != b'OKAY': - raise InvalidCommandError('Expected a ready response, got %s' % 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 isinstance(command, str): - 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, connection, command=None, strip_command=True, delimiter=None, strip_delimiter=True, + Yields: + The responses from the service. + """ + if isinstance(command, str): + 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, connection, command=None, strip_command=True, delimiter=None, strip_delimiter=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? + """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: + connection: Instance of AdbConnection + command: Optional. Command to run on the target. + strip_command: Optional (default True). Strip command name from stdout. + delimiter: Optional. Delimiter to look for in the output to know when to stop expecting more output + (usually the shell prompt) + strip_delimiter: 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 isinstance(delimiter, str): + delimiter = delimiter.encode('utf-8') + + stdout = '' + stdout_stream = BytesIO() + original_command = '' + + try: - Args: - connection: Instance of AdbConnection - command: Optional. Command to run on the target. - strip_command: Optional (default True). Strip command name from stdout. - delimiter: Optional. Delimiter to look for in the output to know when to stop expecting more output - (usually the shell prompt) - strip_delimiter: 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 command: + original_command = str(command) + command += '\r' # Required. Send a carriage return right after the command + command = command.encode('utf8') - if isinstance(delimiter, str): - delimiter = delimiter.encode('utf-8') + # Send the command raw + bytes_written = connection.Write(command) - stdout = '' - stdout_stream = BytesIO() - original_command = '' + if delimiter: + # Expect multiple WRTE commands until the delimiter (usually terminal prompt) is detected - try: + data = b'' + while delimiter not in data: - if command: - original_command = str(command) - command += '\r' # Required. Send a carriage return right after the command - command = command.encode('utf8') + cmd, data = connection.ReadUntil(b'WRTE') + stdout_stream.write(data) - # Send the command raw - bytes_written = connection.Write(command) + else: + # Otherwise, expect only a single WRTE + cmd, data = connection.ReadUntil(b'WRTE') - if delimiter: - # Expect multiple WRTE commands until the delimiter (usually terminal prompt) is detected + # WRTE command from device will follow with stdout data + stdout_stream.write(data) - data = b'' - while delimiter not in data: + else: - cmd, data = connection.ReadUntil(b'WRTE') - stdout_stream.write(data) + # No command provided means we should just expect a single line from the terminal. Use this sparingly + cmd, data = connection.ReadUntil(b'WRTE') + if cmd == b'WRTE': + # WRTE command from device will follow with stdout data + stdout_stream.write(data) + else: + print("Unhandled command: {}".format(cmd)) - else: - # Otherwise, expect only a single WRTE - cmd, data = connection.ReadUntil(b'WRTE') + cleaned_stdout_stream = BytesIO() + if clean_stdout: + stdout_bytes = stdout_stream.getvalue() - # WRTE command from device will follow with stdout data - stdout_stream.write(data) + bsruns = {} # Backspace runs tracking + next_start_pos = 0 + last_run_pos, last_run_len = find_backspace_runs(stdout_bytes, next_start_pos) - else: + 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 - # No command provided means we should just expect a single line from the terminal. Use this sparingly - cmd, data = connection.ReadUntil(b'WRTE') - if cmd == b'WRTE': - # WRTE command from device will follow with stdout data - stdout_stream.write(data) - else: - print("Unhandled command 2") - - # print("DATA") - # print(data) - # print(binascii.hexlify(data)) - # print("=================") - - cleaned_stdout_stream = BytesIO() - if clean_stdout: - stdout_bytes = stdout_stream.getvalue() - - bsruns = {} - 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:]) - - # first_backspace_pos = stdout_bytes.find(b'\x08') - # if first_backspace_pos == -1: - # cleaned_stdout_stream.write(stdout_bytes) - # else: - # end_backspaces_pos = first_backspace_pos - # while True: - # if chr(stdout_bytes[end_backspaces_pos]) == '\b': - # end_backspaces_pos += 1 - # else: - # break - # - # num_backspaces = end_backspaces_pos - first_backspace_pos - # - # # Ignore num_backspaces number of characters prior to first_backspace_pos - # cleaned_stdout_stream.write(stdout_bytes[:(first_backspace_pos-num_backspaces)]) - # cleaned_stdout_stream.write(stdout_bytes[end_backspaces_pos:]) + while last_run_pos != -1: + last_run_pos, last_run_len = find_backspace_runs(stdout_bytes[next_start_pos:], next_start_pos) - else: - cleaned_stdout_stream.write(stdout_stream.getvalue()) + 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 - stdout = cleaned_stdout_stream.getvalue() + cleaned_stdout_stream.write(stdout_bytes[next_start_pos:]) - # print("STDOUT") - # print(stdout) - # print(binascii.hexlify(stdout)) - # print("=================") + else: + cleaned_stdout_stream.write(stdout_stream.getvalue()) - # Strip original command that will come back in stdout + stdout = cleaned_stdout_stream.getvalue() - if original_command and strip_command: - findstr = original_command.encode('utf-8') + b'\r\r\n' + # Strip original command that will come back in stdout + if original_command and strip_command: + findstr = original_command.encode('utf-8') + b'\r\r\n' + pos = stdout.find(findstr) + while pos >= 0: + stdout = stdout.replace(findstr, b'') 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 delimiter if requested - if delimiter and strip_delimiter: - # Sometimes, a number preceeds the terminal delimiter which we need to check for an strip out - # eg. "123|dreamlte:/ $" + if b'\r\r\n' in stdout: + stdout = stdout.split(b'\r\r\n')[1] - # number_delim = r'(?P\d{1,3}\|' + delimiter.decode('utf-8') + ")" - # - # reg = re.compile(number_delim) - # m = reg.match(stdout.decode('utf-8')) - # if m: - # stdout = stdout.replace(m.group('new_delim'), b'') - # else: - # stdout = stdout.replace(delimiter, b'') + # Strip delimiter if requested + if delimiter and strip_delimiter: - stdout = stdout.replace(delimiter, b'') + stdout = stdout.replace(delimiter, b'') - stdout = stdout.rstrip() + stdout = stdout.rstrip() - except Exception as e: - print("InteractiveShell exception (most likely timeout): {}".format(e)) + except Exception as e: + print("InteractiveShell exception (most likely timeout): {}".format(e)) - return stdout + return stdout From 240279e42dfae51c20e4641698ad1e64b758da49 Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Sat, 3 Feb 2018 15:45:31 -0500 Subject: [PATCH 10/38] Added future to the requirements for py3 compatibility --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7fb1172..948910c 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,7 @@ keywords = ['android', 'adb', 'fastboot'], - install_requires = ['libusb1>=1.0.16', 'M2Crypto>=0.21.1,<=0.26.4'], + install_requires = ['future', 'libusb1>=1.0.16', 'M2Crypto>=0.21.1,<=0.26.4'], extra_requires = { 'fastboot': 'progressbar>=2.3' From 14a0f2f6e42979574106b2227279853daf00ef16 Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Sat, 3 Feb 2018 15:46:16 -0500 Subject: [PATCH 11/38] Fixed spacing issues in adb_debug --- adb/adb_debug.py | 308 +++++++++++++++++++++++------------------------ 1 file changed, 151 insertions(+), 157 deletions(-) diff --git a/adb/adb_debug.py b/adb/adb_debug.py index ec88c70..412da7c 100644 --- a/adb/adb_debug.py +++ b/adb/adb_debug.py @@ -27,185 +27,179 @@ from adb import common_cli try: - from adb import sign_m2crypto - - rsa_signer = sign_m2crypto.M2CryptoSigner + 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_pythonrsa + from adb import sign_pycryptodome - rsa_signer = sign_pythonrsa.PythonRSASigner.FromRSAKeyPath + rsa_signer = sign_pycryptodome.PycryptodomeAuthSigner except ImportError: - - try: - from adb import sign_pycryptodome - - rsa_signer = sign_pycryptodome.PycryptodomeAuthSigner - except ImportError: - rsa_signer = None + 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 + """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) + """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(' '.join(options)) + return device.Logcat(' '.join(options)) 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() + """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: + print(device.InteractiveShell(cmd, strip_command=True, delimiter=terminal_prompt, strip_delimiter=True)) 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: - print(device.InteractiveShell(cmd, strip_command=True, delimiter=terminal_prompt, strip_delimiter=True)) - print(terminal_prompt.decode('utf-8')) - - device.Close() + 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, 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, 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]) + 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()) + sys.exit(main()) From 13d75f861ded8772f32c021b521b2c29e3eb5126 Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Sat, 3 Feb 2018 15:54:50 -0500 Subject: [PATCH 12/38] Fixed up adb_commands spacing --- adb/adb_commands.py | 604 +++++++++++++++++++++++--------------------- 1 file changed, 318 insertions(+), 286 deletions(-) diff --git a/adb/adb_commands.py b/adb/adb_commands.py index 35246fb..59a980b 100644 --- a/adb/adb_commands.py +++ b/adb/adb_commands.py @@ -30,7 +30,7 @@ from adb import common from adb import filesync_protocol -# from adb.h +# From adb.h CLASS = 0xFF SUBCLASS = 0x42 PROTOCOL = 0x01 @@ -38,299 +38,331 @@ DeviceIsAvailable = common.InterfaceMatcher(CLASS, SUBCLASS, PROTOCOL) -class AdbCommands(object): - - protocol_handler = adb_protocol.AdbMessage - filesync_handler = filesync_protocol.FilesyncProtocol - - def __init__(self): - - self.__reset() - - def __reset(self): - self.handle = None - self._device_state = None - - # Connection table tracks each open AdbConnection objects per service type - # By default, the only service connections that make sense to hold open are (interactive) shell and sync - self._service_connections = { - b'shell:': None, - #b'sync': None - } - - 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 +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 - 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 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. - - If serial specifies a TCP address:port, then a TCP connection is - used instead of a USB connection. - """ - if serial and b':' 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 - - self.handle.Close() - self.__reset() - - def __Connect(self, banner=None, **kwargs): - """Connect to the device. - - Args: - usb: UsbHandle or TcpHandle instance to use. - banner: See protocol_handler.Connect. - **kwargs: See protocol_handler.Connect for kwargs. Includes 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'::') - - device_state = parts[0] - - # Break out the build prop info - build_props = str(parts[1].split(b';')) - #print("Device build props: {}".format(build_props)) - - self._device_state = device_state - - 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='', timeout_ms=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. - timeout_ms: Expected timeout for pushing and installing. - - Returns: - The pm install output. - """ - if not destination_dir: - destination_dir = '/data/local/tmp/' - basename = os.path.basename(apk_path) - destination_path = destination_dir + basename - self.Push(apk_path, destination_path, timeout_ms=timeout_ms) - - return self.Shell(command='pm install -r "%s"' % destination_path) - - def Push(self, source_file, device_filename, mtime='0', timeout_ms=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. - """ - - 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) - return - source_file = open(source_file, 'rb') - - with source_file: - connection = self.protocol_handler.Open( - self.handle, destination=b'sync:', timeout_ms=timeout_ms) - self.filesync_handler.Push(connection, source_file, device_filename, - mtime=int(mtime)) - connection.Close() - - def Pull(self, device_filename, dest_file=None, timeout_ms=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. - - 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, 'w') - - else: - raise ValueError("destfile is of unknown type") - - #conn = self._get_service_connection(b'sync:') - conn = self.protocol_handler.Open(self.handle, destination=b'sync:') - - self.filesync_handler.Pull(conn, device_filename, dest_file) - - conn.Close() - - if isinstance(dest_file, io.BytesIO): - return dest_file.getvalue() - else: - dest_file.close() - return os.path.exists(dest_file) - - def Stat(self, device_filename): - """Get a file's stat() information.""" - connection = self.protocol_handler.Open(self.handle, destination=b'sync:') - 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'sync:') - 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'remount') - - 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. +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() - Args: - command: Command to run on the target. - timeout_ms: Maximum time to allow the command to run. + def __reset(self): + self.handle = None + self._device_state = None - Yields: - The responses from the shell command. - """ - return self.protocol_handler.StreamingCommand( - self.handle, service=b'shell', command=command, - timeout_ms=timeout_ms) + # Connection table tracks each open AdbConnection objects per service type + # By default, the only service connections that make sense to hold open is (interactive) shell + self._service_connections = { + b'shell:': None + } - def Logcat(self, options, timeout_ms=None): - """Run 'shell logcat' and stream the output to stdout. + 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 - 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) + :param service: + :param service_command: Additional service parameters to append + :param create: If False, dont create a connection if it does not exist + :return: + """ - def InteractiveShell(self, command=None, strip_command=True, delimiter=None, strip_delimiter=True): - """Get stdout from the currently open interactive shell and optionally run a command + 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 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. + + If serial specifies a TCP address:port, then a TCP connection is + used instead of a USB connection. + """ + if serial and b':' 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 + + self.handle.Close() + self.__reset() + + def __Connect(self, banner=None, **kwargs): + """Connect to the device. + + Args: + usb: UsbHandle or TcpHandle instance to use. + banner: See protocol_handler.Connect. + **kwargs: See protocol_handler.Connect for kwargs. Includes 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'::') + device_state = parts[0] + + # Break out the build prop info + build_props = str(parts[1].split(b';')) + # print("Device build props: {}".format(build_props)) + + self._device_state = device_state + + 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, timeout_ms=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 + timeout_ms: Expected timeout for pushing and installing. + + Returns: + The pm install output. + """ + if not destination_dir: + destination_dir = '/data/local/tmp/' + basename = os.path.basename(apk_path) + destination_path = os.path.join(destination_dir, basename) + self.Push(apk_path, destination_path, timeout_ms=timeout_ms) + + cmd = ['pm install'] + if replace_existing: + cmd.append('-r') + cmd.append('"{}"'.format(destination_path)) + + return self.Shell(' '.join(cmd), timeout_ms=timeout_ms) + + 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): + """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. + """ + 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) + return + source_file = open(source_file, 'rb') + + with source_file: + connection = self.protocol_handler.Open( + self.handle, destination=b'sync:', timeout_ms=timeout_ms) + self.filesync_handler.Push(connection, source_file, device_filename, + mtime=int(mtime)) + connection.Close() + + def Pull(self, device_filename, dest_file=None, timeout_ms=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. + + 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, 'w') + else: + raise ValueError("destfile is of unknown type") + + #conn = self._get_service_connection(b'sync:') + conn = self.protocol_handler.Open( + self.handle, destination=b'sync:') + + self.filesync_handler.Pull(conn, device_filename, dest_file) + + conn.Close() + if isinstance(dest_file, io.BytesIO): + return dest_file.getvalue() + else: + dest_file.close() + return os.path.exists(dest_file) + + def Stat(self, device_filename): + """Get a file's stat() information.""" + connection = self.protocol_handler.Open(self.handle, destination=b'sync:') + 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'sync:') + 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, command=None, strip_command=True, delimiter=None, strip_delimiter=True): + """Get stdout from the currently open interactive shell and optionally run a command on the device, returning all output. - Args: - command: Optional. Command to run on the target. - strip_command: Optional (default True). Strip command name from stdout. - delimiter: Optional. Delimiter to look for in the output to know when to stop expecting more output - (usually the shell prompt) - strip_delimiter: Optional (default True): Strip the provided delimiter from the output - - Returns: - The stdout from the shell command. - """ + Args: + command: Optional. Command to run on the target. + strip_command: Optional (default True). Strip command name from stdout. + delimiter: Optional. Delimiter to look for in the output to know when to stop expecting more output + (usually the shell prompt) + strip_delimiter: Optional (default True): Strip the provided delimiter from the output - conn = self._get_service_connection(b'shell:') + Returns: + The stdout from the shell command. + """ + conn = self._get_service_connection(b'shell:') - return self.protocol_handler.InteractiveShellCommand(conn, command=command, strip_command=strip_command, - delimiter=delimiter, strip_delimiter=strip_delimiter) + return self.protocol_handler.InteractiveShellCommand(conn, command=command, strip_command=strip_command, + delimiter=delimiter, strip_delimiter=strip_delimiter) \ No newline at end of file From 60ecb1cd3287f509b4eca1dd9b42fad52037fc15 Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Sat, 3 Feb 2018 15:55:04 -0500 Subject: [PATCH 13/38] Minor spacing change to requirements list --- setup.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 948910c..695e117 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,11 @@ keywords = ['android', 'adb', 'fastboot'], - install_requires = ['future', 'libusb1>=1.0.16', 'M2Crypto>=0.21.1,<=0.26.4'], + install_requires = [ + 'future', + 'libusb1>=1.0.16', + #'M2Crypto>=0.21.1,<=0.26.4' + ], extra_requires = { 'fastboot': 'progressbar>=2.3' From 71aa542d9f4719a817c7b1f4280c28df68bae8c1 Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Sat, 3 Feb 2018 16:21:50 -0500 Subject: [PATCH 14/38] Updated InteractiveShellCommand to handle changing delimiters, still a bit more work to do --- adb/adb_commands.py | 3 +- adb/adb_protocol.py | 142 ++++++++++++++++++++++++-------------------- 2 files changed, 79 insertions(+), 66 deletions(-) diff --git a/adb/adb_commands.py b/adb/adb_commands.py index 59a980b..07afbe8 100644 --- a/adb/adb_commands.py +++ b/adb/adb_commands.py @@ -364,5 +364,4 @@ def InteractiveShell(self, command=None, strip_command=True, delimiter=None, str """ conn = self._get_service_connection(b'shell:') - return self.protocol_handler.InteractiveShellCommand(conn, command=command, strip_command=strip_command, - delimiter=delimiter, strip_delimiter=strip_delimiter) \ No newline at end of file + return self.protocol_handler.InteractiveShellCommand(conn, command=command, strip_command=strip_command, delimiter=delimiter, strip_delimiter=strip_delimiter) \ No newline at end of file diff --git a/adb/adb_protocol.py b/adb/adb_protocol.py index 0ccecbf..568a7a5 100644 --- a/adb/adb_protocol.py +++ b/adb/adb_protocol.py @@ -451,93 +451,107 @@ def InteractiveShellCommand(cls, connection, command=None, strip_command=True, d if isinstance(delimiter, str): delimiter = delimiter.encode('utf-8') - stdout = '' - stdout_stream = BytesIO() - original_command = '' - - try: + # 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 delimiter: + user_pos = delimiter.find(b'@') + dir_pos = delimiter.rfind(b':/') + if user_pos != -1 and dir_pos != -1: + partial_delimiter = delimiter[user_pos:dir_pos+1] # e.g. @hammerhead: + else: + partial_delimiter = delimiter + else: + partial_delimiter = None - if command: - original_command = str(command) - command += '\r' # Required. Send a carriage return right after the command - command = command.encode('utf8') + stdout = '' + stdout_stream = BytesIO() + original_command = '' - # Send the command raw - bytes_written = connection.Write(command) + try: - if delimiter: - # Expect multiple WRTE commands until the delimiter (usually terminal prompt) is detected + if command: + original_command = str(command) + command += '\r' # Required. Send a carriage return right after the command + command = command.encode('utf8') - data = b'' - while delimiter not in data: + # Send the command raw + bytes_written = connection.Write(command) - cmd, data = connection.ReadUntil(b'WRTE') - stdout_stream.write(data) + if delimiter: + # Expect multiple WRTE commands until the delimiter (usually terminal prompt) is detected - else: - # Otherwise, expect only a single WRTE - cmd, data = connection.ReadUntil(b'WRTE') + data = b'' + while partial_delimiter not in data: - # WRTE command from device will follow with stdout data - stdout_stream.write(data) + cmd, data = connection.ReadUntil(b'WRTE') + stdout_stream.write(data) else: - - # No command provided means we should just expect a single line from the terminal. Use this sparingly + # Otherwise, expect only a single WRTE cmd, data = connection.ReadUntil(b'WRTE') - if cmd == b'WRTE': - # WRTE command from device will follow with stdout data - stdout_stream.write(data) - else: - print("Unhandled command: {}".format(cmd)) - cleaned_stdout_stream = BytesIO() - if clean_stdout: - stdout_bytes = stdout_stream.getvalue() + # WRTE command from device will follow with stdout data + stdout_stream.write(data) - bsruns = {} # Backspace runs tracking - next_start_pos = 0 - last_run_pos, last_run_len = find_backspace_runs(stdout_bytes, next_start_pos) + else: + + # No command provided means we should just expect a single line from the terminal. Use this sparingly + cmd, data = connection.ReadUntil(b'WRTE') + if cmd == b'WRTE': + # WRTE command from device will follow with stdout data + stdout_stream.write(data) + else: + print("Unhandled command: {}".format(cmd)) - 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 + cleaned_stdout_stream = BytesIO() + if clean_stdout: + stdout_bytes = stdout_stream.getvalue() - while last_run_pos != -1: - last_run_pos, last_run_len = find_backspace_runs(stdout_bytes[next_start_pos:], next_start_pos) + 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: - 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 + 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 - cleaned_stdout_stream.write(stdout_bytes[next_start_pos:]) + while last_run_pos != -1: + last_run_pos, last_run_len = find_backspace_runs(stdout_bytes[next_start_pos:], next_start_pos) - else: - cleaned_stdout_stream.write(stdout_stream.getvalue()) + 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() + stdout = cleaned_stdout_stream.getvalue() - # Strip original command that will come back in stdout - if original_command and strip_command: - findstr = original_command.encode('utf-8') + b'\r\r\n' + # Strip original command that will come back in stdout + if original_command and strip_command: + findstr = original_command.encode('utf-8') + b'\r\r\n' + pos = stdout.find(findstr) + while pos >= 0: + stdout = stdout.replace(findstr, b'') 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] + if b'\r\r\n' in stdout: + stdout = stdout.split(b'\r\r\n')[1] - # Strip delimiter if requested - if delimiter and strip_delimiter: + # Strip delimiter if requested + # TODO: Handling stripping partial delimiters here + if delimiter and strip_delimiter: - stdout = stdout.replace(delimiter, b'') + stdout = stdout.replace(delimiter, b'') - stdout = stdout.rstrip() + stdout = stdout.rstrip() - except Exception as e: - print("InteractiveShell exception (most likely timeout): {}".format(e)) + except Exception as e: + print("InteractiveShell exception (most likely timeout): {}".format(e)) - return stdout + return stdout From dc7bafdae0450a90a26a854ad788641f94a5e1d4 Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Sat, 3 Feb 2018 16:29:30 -0500 Subject: [PATCH 15/38] Dynamic detection of rsa library in setup.py --- setup.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 695e117..7516425 100644 --- a/setup.py +++ b/setup.py @@ -14,6 +14,23 @@ from setuptools import setup +# Figure out if the system already has a supported Crypto library +rsa_signer_library = 'M2Crypto>=0.21.1,<=0.26.4' +try: + import rsa + + rsa_signer_library = 'python-rsa' +except ImportError: + try: + from Crypto.Hash import SHA256 + from Crypto.PublicKey import RSA + from Crypto.Signature import pkcs1_15 + + rsa_signer_library = 'pycryptodome' + except ImportError: + rsa_signer_library = 'M2Crypto>=0.21.1,<=0.26.4' + + setup( name = 'adb', packages = ['adb'], @@ -46,7 +63,7 @@ install_requires = [ 'future', 'libusb1>=1.0.16', - #'M2Crypto>=0.21.1,<=0.26.4' + rsa_signer_library ], extra_requires = { From c4009e4310223c7d1d45e38d1ee289168081b285 Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Sat, 3 Feb 2018 20:30:45 -0500 Subject: [PATCH 16/38] Improved user experience for adb_debug --- adb/adb_debug.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/adb/adb_debug.py b/adb/adb_debug.py index 412da7c..39e0144 100644 --- a/adb/adb_debug.py +++ b/adb/adb_debug.py @@ -113,8 +113,12 @@ def Shell(device, *command): elif cmd == 'exit': break else: - print(device.InteractiveShell(cmd, strip_command=True, delimiter=terminal_prompt, strip_delimiter=True)) - print(terminal_prompt.decode('utf-8')) + stdout = device.InteractiveShell(cmd, strip_command=True, delimiter=terminal_prompt, strip_delimiter=True) + if stdout: + if isinstance(stdout, bytes): + stdout = stdout.decode('utf-8') + print(stdout) + device.Close() From 04bf05ade89152e57bb6648f4a3974cea68b2456 Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Sat, 3 Feb 2018 20:40:37 -0500 Subject: [PATCH 17/38] Shortened InteractiveShell function and parameters to less than 80 characters. Minor fix to handling partial delimiter --- adb/adb_commands.py | 10 +++--- adb/adb_debug.py | 2 +- adb/adb_protocol.py | 77 ++++++++++++++++++++++----------------------- 3 files changed, 44 insertions(+), 45 deletions(-) diff --git a/adb/adb_commands.py b/adb/adb_commands.py index 07afbe8..e598894 100644 --- a/adb/adb_commands.py +++ b/adb/adb_commands.py @@ -348,20 +348,20 @@ def Logcat(self, options, timeout_ms=None): """ return self.StreamingShell('logcat %s' % options, timeout_ms) - def InteractiveShell(self, command=None, strip_command=True, delimiter=None, strip_delimiter=True): + 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: command: Optional. Command to run on the target. - strip_command: Optional (default True). Strip command name from stdout. - delimiter: Optional. Delimiter to look for in the output to know when to stop expecting more output + 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_delimiter: Optional (default True): Strip the provided delimiter from the output + 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, command=command, strip_command=strip_command, delimiter=delimiter, strip_delimiter=strip_delimiter) \ No newline at end of file + return self.protocol_handler.InteractiveShellCommand(conn, cmd=cmd, strip_cmd=strip_cmd, delim=delim, strip_delim=strip_delim) \ No newline at end of file diff --git a/adb/adb_debug.py b/adb/adb_debug.py index 39e0144..0cecdcc 100644 --- a/adb/adb_debug.py +++ b/adb/adb_debug.py @@ -113,7 +113,7 @@ def Shell(device, *command): elif cmd == 'exit': break else: - stdout = device.InteractiveShell(cmd, strip_command=True, delimiter=terminal_prompt, strip_delimiter=True) + stdout = device.InteractiveShell(cmd, strip_cmd=True, delim=terminal_prompt, strip_delim=True) if stdout: if isinstance(stdout, bytes): stdout = stdout.decode('utf-8') diff --git a/adb/adb_protocol.py b/adb/adb_protocol.py index 568a7a5..4fd8a6c 100644 --- a/adb/adb_protocol.py +++ b/adb/adb_protocol.py @@ -431,78 +431,77 @@ def StreamingCommand(cls, usb, service, command='', timeout_ms=None): yield data.decode('utf8') @classmethod - def InteractiveShellCommand(cls, connection, command=None, strip_command=True, delimiter=None, strip_delimiter=True, - clean_stdout=True): + 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: - connection: Instance of AdbConnection - command: Optional. Command to run on the target. - strip_command: Optional (default True). Strip command name from stdout. - delimiter: Optional. Delimiter to look for in the output to know when to stop expecting more output + 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_delimiter: Optional (default True): Strip the provided delimiter from the output + 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 isinstance(delimiter, str): - delimiter = delimiter.encode('utf-8') + if isinstance(delim, str): + delimiter = 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 delimiter: - user_pos = delimiter.find(b'@') - dir_pos = delimiter.rfind(b':/') + if delim: + user_pos = delim.find(b'@') + dir_pos = delim.rfind(b':/') if user_pos != -1 and dir_pos != -1: - partial_delimiter = delimiter[user_pos:dir_pos+1] # e.g. @hammerhead: + partial_delim = delim[user_pos:dir_pos+1] # e.g. @hammerhead: else: - partial_delimiter = delimiter + partial_delim = delim else: - partial_delimiter = None + partial_delim = None stdout = '' stdout_stream = BytesIO() - original_command = '' + original_cmd = '' try: - if command: - original_command = str(command) - command += '\r' # Required. Send a carriage return right after the command - command = command.encode('utf8') + if cmd: + original_cmd = str(cmd) + cmd += '\r' # Required. Send a carriage return right after the cmd + cmd = cmd.encode('utf8') - # Send the command raw - bytes_written = connection.Write(command) + # Send the cmd raw + bytes_written = conn.Write(cmd) - if delimiter: - # Expect multiple WRTE commands until the delimiter (usually terminal prompt) is detected + if delim: + # Expect multiple WRTE cmds until the delim (usually terminal prompt) is detected data = b'' - while partial_delimiter not in data: + while partial_delim not in data: - cmd, data = connection.ReadUntil(b'WRTE') + cmd, data = conn.ReadUntil(b'WRTE') stdout_stream.write(data) else: # Otherwise, expect only a single WRTE - cmd, data = connection.ReadUntil(b'WRTE') + cmd, data = conn.ReadUntil(b'WRTE') - # WRTE command from device will follow with stdout data + # WRTE cmd from device will follow with stdout data stdout_stream.write(data) else: - # No command provided means we should just expect a single line from the terminal. Use this sparingly - cmd, data = connection.ReadUntil(b'WRTE') + # 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 command from device will follow with stdout data + # WRTE cmd from device will follow with stdout data stdout_stream.write(data) else: - print("Unhandled command: {}".format(cmd)) + print("Unhandled cmd: {}".format(cmd)) cleaned_stdout_stream = BytesIO() if clean_stdout: @@ -532,9 +531,9 @@ def InteractiveShellCommand(cls, connection, command=None, strip_command=True, d stdout = cleaned_stdout_stream.getvalue() - # Strip original command that will come back in stdout - if original_command and strip_command: - findstr = original_command.encode('utf-8') + b'\r\r\n' + # 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'') @@ -543,11 +542,11 @@ def InteractiveShellCommand(cls, connection, command=None, strip_command=True, d if b'\r\r\n' in stdout: stdout = stdout.split(b'\r\r\n')[1] - # Strip delimiter if requested - # TODO: Handling stripping partial delimiters here - if delimiter and strip_delimiter: + # 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(delimiter, b'') + stdout = stdout.replace(delim, b'') stdout = stdout.rstrip() From a4113e5b6f6156b5c67d7ba484090c31db07c28c Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Sat, 3 Feb 2018 21:22:37 -0500 Subject: [PATCH 18/38] adb_commands: Exposing handle paramter to allow caller to specify their own device handle if desired (used in tests) --- adb/adb_commands.py | 28 +++++++++++++++++++--------- adb/common_cli.py | 2 +- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/adb/adb_commands.py b/adb/adb_commands.py index 5a4327c..1272c2a 100644 --- a/adb/adb_commands.py +++ b/adb/adb_commands.py @@ -109,12 +109,16 @@ def ConnectDevice(self, port_path=None, serial=None, default_timeout_ms=None, ** If serial specifies a TCP address:port, then a TCP connection is used instead of a USB connection. """ - if serial and b':' 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) + + # If there isnt a handle override (used by tests), build one here + if not kwargs.get('handle', None): + + if serial and b':' 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) @@ -131,20 +135,26 @@ def Close(self): self.handle.Close() self.__reset() - def __Connect(self, banner=None, **kwargs): + def __Connect(self, handle=None, banner=None, **kwargs): """Connect to the device. Args: - usb: UsbHandle or TcpHandle instance to use. + handle: UsbHandle or TcpHandle instance to use (usually self.handle) banner: See protocol_handler.Connect. **kwargs: See protocol_handler.Connect for kwargs. Includes rsa_keys, and auth_timeout_ms. Returns: An instance of this class if the device connected successfully. """ + + if not handle: + handle = self.handle + if not banner: banner = socket.gethostname().encode() - conn_str = self.protocol_handler.Connect(self.handle, banner=banner, **kwargs) + + conn_str = self.protocol_handler.Connect(handle, banner=banner, **kwargs) + # Remove banner and colons after device state (state::banner) parts = conn_str.split(b'::') device_state = parts[0] diff --git a/adb/common_cli.py b/adb/common_cli.py index e2f217e..9f9147a 100644 --- a/adb/common_cli.py +++ b/adb/common_cli.py @@ -144,7 +144,7 @@ def _RunMethod(dev, args, extra): 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 = 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) From 32f2532159b8146c188690f3e1f30ca9153490d6 Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Sat, 3 Feb 2018 21:23:27 -0500 Subject: [PATCH 19/38] Migrated fastboot to use object oriented interface. Updated fastboot tests. Consolidated interface to mimic object adbcommands --- adb/fastboot.py | 51 +++++++++++++++++++++++++------------------ adb/fastboot_debug.py | 7 +++--- test/fastboot_test.py | 49 +++++++++++++++++++++++------------------ 3 files changed, 62 insertions(+), 45 deletions(-) diff --git a/adb/fastboot.py b/adb/fastboot.py index 5642a3e..4d1e73d 100644 --- a/adb/fastboot.py +++ b/adb/fastboot.py @@ -206,30 +206,38 @@ def _Write(self, data, length, progress_callback=None): class FastbootCommands(object): """Encapsulates the fastboot commands.""" - def __init__(self, usb, chunk_kb=1024): + def __init__(self): """Constructs a FastbootCommands instance. Args: usb: UsbHandle instance. """ - self._usb = usb - self._protocol = FastbootProtocol(usb, chunk_kb) + self.__reset() + + def __reset(self): + self._handle = None + self._protocol = None @property def usb_handle(self): - return self._usb + return self._handle def Close(self): - self._usb.Close() + self._handle.Close() - @classmethod - def ConnectDevice( - cls, port_path=None, serial=None, default_timeout_ms=None, chunk_kb=1024): + 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.""" - usb = common.UsbHandle.FindAndOpen( - DeviceIsAvailable, port_path=port_path, serial=serial, - timeout_ms=default_timeout_ms) - return cls(usb, chunk_kb=chunk_kb) + + if not kwargs.get('handle', None): + self._handle = common.UsbHandle.FindAndOpen( + DeviceIsAvailable, port_path=port_path, serial=serial, + timeout_ms=default_timeout_ms) + else: + self._handle = kwargs['handle'] + + self._protocol = FastbootProtocol(self._handle, chunk_kb) + + return self @classmethod def Devices(cls): @@ -285,15 +293,16 @@ def Download(self, source_file, source_len=0, source_len = os.stat(source_file).st_size source_file = open(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) + 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. diff --git a/adb/fastboot_debug.py b/adb/fastboot_debug.py index 7f25c44..59713d8 100755 --- a/adb/fastboot_debug.py +++ b/adb/fastboot_debug.py @@ -114,9 +114,10 @@ def SetProgress(current, total): bar.finish() kwargs['progress_callback'] = SetProgress - return common_cli.StartCli( - args, fastboot.FastbootCommands.ConnectDevice, chunk_kb=args.chunk_kb, - extra=kwargs) + dev = fastboot.FastbootCommands() + dev.ConnectDevice(chunk_kb=args.chunk_kb) + + return common_cli.StartCli(args, dev, extra=kwargs) if __name__ == '__main__': diff --git a/test/fastboot_test.py b/test/fastboot_test.py index 6862c2e..32c96fa 100755 --- a/test/fastboot_test.py +++ b/test/fastboot_test.py @@ -61,9 +61,10 @@ def testDownload(self): data = io.StringIO(raw) self.ExpectDownload([raw]) - commands = fastboot.FastbootCommands(self.usb) + dev = fastboot.FastbootCommands() + dev.ConnectDevice(handle=self.usb) - response = commands.Download(data) + response = dev.Download(data) self.assertEqual(b'Result', response) def testDownloadFail(self): @@ -71,26 +72,28 @@ def testDownloadFail(self): data = io.StringIO(raw) self.ExpectDownload([raw], succeed=False) - commands = fastboot.FastbootCommands(self.usb) + dev = fastboot.FastbootCommands() + dev.ConnectDevice(handle=self.usb) with self.assertRaises(fastboot.FastbootRemoteFailure): - commands.Download(data) + dev.Download(data) data = io.StringIO(raw) self.ExpectDownload([raw], accept_data=False) with self.assertRaises(fastboot.FastbootTransferError): - commands.Download(data) + dev.Download(data) def testFlash(self): partition = b'yarr' self.ExpectFlash(partition) - commands = fastboot.FastbootCommands(self.usb) + dev = fastboot.FastbootCommands() + dev.ConnectDevice(handle=self.usb) output = io.BytesIO() def InfoCb(message): if message.header == b'INFO': output.write(message.message) - response = commands.Flash(partition, info_cb=InfoCb) + response = dev.Flash(partition, info_cb=InfoCb) self.assertEqual(b'Done', response) self.assertEqual(b'Random info from the bootloader', output.getvalue()) @@ -98,10 +101,11 @@ def testFlashFail(self): partition = b'matey' self.ExpectFlash(partition, succeed=False) - commands = fastboot.FastbootCommands(self.usb) + dev = fastboot.FastbootCommands() + dev.ConnectDevice(handle=self.usb) with self.assertRaises(fastboot.FastbootRemoteFailure): - commands.Flash(partition) + dev.Flash(partition) def testFlashFromFile(self): partition = b'somewhere' @@ -122,51 +126,54 @@ def testFlashFromFile(self): cb = lambda progress, total: progresses.append((progress, total)) - commands = fastboot.FastbootCommands(self.usb) - commands.FlashFromFile( + dev = fastboot.FastbootCommands() + dev.ConnectDevice(handle=self.usb) + dev.FlashFromFile( partition, tmp.name, progress_callback=cb) self.assertEqual(len(pieces), len(progresses)) os.remove(tmp.name) def testSimplerCommands(self): - commands = fastboot.FastbootCommands(self.usb) + dev = fastboot.FastbootCommands() + dev.ConnectDevice(handle=self.usb) self.usb.ExpectWrite(b'erase:vector') self.usb.ExpectRead(b'OKAY') - commands.Erase('vector') + dev.Erase('vector') self.usb.ExpectWrite(b'getvar:variable') self.usb.ExpectRead(b'OKAYstuff') - self.assertEqual(b'stuff', commands.Getvar('variable')) + self.assertEqual(b'stuff', dev.Getvar('variable')) self.usb.ExpectWrite(b'continue') self.usb.ExpectRead(b'OKAY') - commands.Continue() + dev.Continue() self.usb.ExpectWrite(b'reboot') self.usb.ExpectRead(b'OKAY') - commands.Reboot() + dev.Reboot() self.usb.ExpectWrite(b'reboot-bootloader') self.usb.ExpectRead(b'OKAY') - commands.RebootBootloader() + dev.RebootBootloader() self.usb.ExpectWrite(b'oem a little somethin') self.usb.ExpectRead(b'OKAYsomethin') - self.assertEqual(b'somethin', commands.Oem('a little somethin')) + self.assertEqual(b'somethin', dev.Oem('a little somethin')) def testVariousFailures(self): - commands = fastboot.FastbootCommands(self.usb) + dev = fastboot.FastbootCommands() + dev.ConnectDevice(handle=self.usb) self.usb.ExpectWrite(b'continue') self.usb.ExpectRead(b'BLEH') with self.assertRaises(fastboot.FastbootInvalidResponse): - commands.Continue() + dev.Continue() self.usb.ExpectWrite(b'continue') self.usb.ExpectRead(b'DATA000000') with self.assertRaises(fastboot.FastbootStateMismatch): - commands.Continue() + dev.Continue() if __name__ == '__main__': From e014cdc6ff24a6c8e3aa89e19778b1f022be9cee Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Sat, 3 Feb 2018 21:39:56 -0500 Subject: [PATCH 20/38] AdbCommands: Improved handle interface and passing to internal functions --- adb/adb_commands.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/adb/adb_commands.py b/adb/adb_commands.py index 1272c2a..b80d6a6 100644 --- a/adb/adb_commands.py +++ b/adb/adb_commands.py @@ -120,6 +120,9 @@ def ConnectDevice(self, port_path=None, serial=None, default_timeout_ms=None, ** DeviceIsAvailable, port_path=port_path, serial=serial, timeout_ms=default_timeout_ms) + else: + self.handle = kwargs.pop('handle') + self.__Connect(**kwargs) return self @@ -135,11 +138,10 @@ def Close(self): self.handle.Close() self.__reset() - def __Connect(self, handle=None, banner=None, **kwargs): + def __Connect(self, banner=None, **kwargs): """Connect to the device. Args: - handle: UsbHandle or TcpHandle instance to use (usually self.handle) banner: See protocol_handler.Connect. **kwargs: See protocol_handler.Connect for kwargs. Includes rsa_keys, and auth_timeout_ms. @@ -147,13 +149,10 @@ def __Connect(self, handle=None, banner=None, **kwargs): An instance of this class if the device connected successfully. """ - if not handle: - handle = self.handle - if not banner: banner = socket.gethostname().encode() - conn_str = self.protocol_handler.Connect(handle, banner=banner, **kwargs) + 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'::') From 71825c5756a2e6d720795e7df65fe962386fe593 Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Sat, 3 Feb 2018 21:40:19 -0500 Subject: [PATCH 21/38] Updated adb_test.py to conform to new AdbCommand object model --- test/adb_test.py | 71 +++++++++++++++++++++++++++++------------------- 1 file changed, 43 insertions(+), 28 deletions(-) diff --git a/test/adb_test.py b/test/adb_test.py index 1b8800c..faceb0d 100755 --- a/test/adb_test.py +++ b/test/adb_test.py @@ -94,15 +94,17 @@ def testConnect(self): usb = common_stub.StubUsb() self._ExpectConnection(usb) - adb_commands.AdbCommands.Connect(usb, BANNER) + dev = adb_commands.AdbCommands() + dev.ConnectDevice(handle=usb, banner=BANNER) def testSmallResponseShell(self): command = b'keepin it real' response = 'word.' usb = self._ExpectCommand(b'shell', command, response) - adb_commands = self._Connect(usb) - self.assertEqual(response, adb_commands.Shell(command)) + dev = adb_commands.AdbCommands() + dev.ConnectDevice(handle=usb, banner=BANNER) + self.assertEqual(response, dev.Shell(command)) def testBigResponseShell(self): command = b'keepin it real big' @@ -112,9 +114,10 @@ def testBigResponseShell(self): usb = self._ExpectCommand(b'shell', command, *responses) - adb_commands = self._Connect(usb) + dev = adb_commands.AdbCommands() + dev.ConnectDevice(handle=usb, banner=BANNER) self.assertEqual(b''.join(responses).decode('utf8'), - adb_commands.Shell(command)) + dev.Shell(command)) def testUninstall(self): package_name = "com.test.package" @@ -122,8 +125,9 @@ def testUninstall(self): usb = self._ExpectCommand(b'shell', ('pm uninstall "%s"' % package_name).encode('utf8'), response) - adb_commands = self._Connect(usb) - self.assertEquals(response, adb_commands.Uninstall(package_name)) + dev = adb_commands.AdbCommands() + dev.ConnectDevice(handle=usb, banner=BANNER) + self.assertEqual(response, dev.Uninstall(package_name)) def testStreamingResponseShell(self): command = b'keepin it real big' @@ -133,42 +137,49 @@ def testStreamingResponseShell(self): usb = self._ExpectCommand(b'shell', command, *responses) - adb_commands = self._Connect(usb) + dev = adb_commands.AdbCommands() + dev.ConnectDevice(handle=usb, banner=BANNER) response_count = 0 - for (expected,actual) in zip(responses, adb_commands.StreamingShell(command)): + for (expected,actual) in zip(responses, dev.StreamingShell(command)): self.assertEqual(expected, actual) response_count = response_count + 1 self.assertEqual(len(responses), response_count) def testReboot(self): usb = self._ExpectCommand(b'reboot', b'', b'') - adb_commands = self._Connect(usb) - adb_commands.Reboot() + dev = adb_commands.AdbCommands() + dev.ConnectDevice(handle=usb, banner=BANNER) + dev.Reboot() def testRebootBootloader(self): usb = self._ExpectCommand(b'reboot', b'bootloader', b'') - adb_commands = self._Connect(usb) - adb_commands.RebootBootloader() + dev = adb_commands.AdbCommands() + dev.ConnectDevice(handle=usb, banner=BANNER) + dev.RebootBootloader() def testRemount(self): usb = self._ExpectCommand(b'remount', b'', b'') - adb_commands = self._Connect(usb) - adb_commands.Remount() + dev = adb_commands.AdbCommands() + dev.ConnectDevice(handle=usb, banner=BANNER) + dev.Remount() def testRoot(self): usb = self._ExpectCommand(b'root', b'', b'') - adb_commands = self._Connect(usb) - adb_commands.Root() + dev = adb_commands.AdbCommands() + dev.ConnectDevice(handle=usb, banner=BANNER) + dev.Root() def testEnableVerity(self): usb = self._ExpectCommand(b'enable-verity', b'', b'') - adb_commands = self._Connect(usb) - adb_commands.EnableVerity() + dev = adb_commands.AdbCommands() + dev.ConnectDevice(handle=usb, banner=BANNER) + dev.EnableVerity() def testDisableVerity(self): usb = self._ExpectCommand(b'disable-verity', b'', b'') - adb_commands = self._Connect(usb) - adb_commands.DisableVerity() + dev = adb_commands.AdbCommands() + dev.ConnectDevice(handle=usb, banner=BANNER) + dev.DisableVerity() class FilesyncAdbTest(BaseAdbTest): @@ -213,8 +224,9 @@ def testPush(self): data = b'OKAY\0\0\0\0' usb = self._ExpectSyncCommand([b''.join(send)], [data]) - adb_commands = self._Connect(usb) - adb_commands.Push(io.StringIO(filedata), '/data', mtime=mtime) + dev = adb_commands.AdbCommands() + dev.ConnectDevice(handle=usb, banner=BANNER) + dev.Push(io.StringIO(filedata), '/data', mtime=mtime) def testPull(self): filedata = b"g'ddayta, govnah" @@ -225,8 +237,9 @@ def testPull(self): self._MakeWriteSyncPacket(b'DONE'), ] usb = self._ExpectSyncCommand([recv], [b''.join(data)]) - adb_commands = self._Connect(usb) - self.assertEqual(filedata, adb_commands.Pull('/data')) + dev = adb_commands.AdbCommands() + dev.ConnectDevice(handle=usb, banner=BANNER) + self.assertEqual(filedata, dev.Pull('/data')) class TcpTimeoutAdbTest(BaseAdbTest): @@ -244,13 +257,15 @@ def _ExpectCommand(cls, service, command, *responses): def _run_shell(self, cmd, timeout_ms=None): tcp = self._ExpectCommand(b'shell', cmd) - adb_commands = self._Connect(tcp) - adb_commands.Shell(cmd, timeout_ms=timeout_ms) + dev = adb_commands.AdbCommands() + dev.ConnectDevice(handle=tcp, banner=BANNER) + dev.Shell(cmd, timeout_ms=timeout_ms) def testConnect(self): tcp = common_stub.StubTcp() self._ExpectConnection(tcp) - adb_commands.AdbCommands.Connect(tcp, BANNER) + dev = adb_commands.AdbCommands() + dev.ConnectDevice(handle=tcp, banner=BANNER) def testTcpTimeout(self): timeout_ms = 1 From 8420ba3dc27b2c4233579f4f9f3b235349d41637 Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Sat, 3 Feb 2018 21:47:57 -0500 Subject: [PATCH 22/38] Updated adb_test to support py2/3. Fixed exit bug in adb_commands --- adb/adb_commands.py | 4 +++- test/adb_test.py | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/adb/adb_commands.py b/adb/adb_commands.py index b80d6a6..3da5195 100644 --- a/adb/adb_commands.py +++ b/adb/adb_commands.py @@ -135,7 +135,9 @@ def Close(self): except: pass - self.handle.Close() + if self.handle: + self.handle.Close() + self.__reset() def __Connect(self, banner=None, **kwargs): diff --git a/test/adb_test.py b/test/adb_test.py index faceb0d..bdbfce5 100755 --- a/test/adb_test.py +++ b/test/adb_test.py @@ -14,7 +14,7 @@ # limitations under the License. """Tests for adb.""" -import io +from io import BytesIO import struct import unittest @@ -213,7 +213,7 @@ def _ExpectSyncCommand(cls, write_commands, read_commands): return usb def testPush(self): - filedata = u'alo there, govnah' + filedata = b'alo there, govnah' mtime = 100 send = [ @@ -226,7 +226,7 @@ def testPush(self): dev = adb_commands.AdbCommands() dev.ConnectDevice(handle=usb, banner=BANNER) - dev.Push(io.StringIO(filedata), '/data', mtime=mtime) + dev.Push(BytesIO(filedata), '/data', mtime=mtime) def testPull(self): filedata = b"g'ddayta, govnah" From 855b33db18cc6c3935850d4063799e0cd0ff24a6 Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Sat, 17 Feb 2018 14:35:47 -0500 Subject: [PATCH 23/38] added .DS_Store files to gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index e7a1366..e92aa58 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ adb.egg-info/ .tox/ /adb.zip /fastboot.zip -.idea/ \ No newline at end of file +.idea/ +*.DS_Store* \ No newline at end of file From 821d9622c648073ef3b681a127bf68c3a7b475d3 Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Sat, 17 Feb 2018 14:54:16 -0500 Subject: [PATCH 24/38] Fixed minor syntax issue in function declaration --- adb/adb_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adb/adb_commands.py b/adb/adb_commands.py index 77cc864..96d5953 100644 --- a/adb/adb_commands.py +++ b/adb/adb_commands.py @@ -249,7 +249,7 @@ def Push(self, source_file, device_filename, mtime='0', timeout_ms=None, progres mtime=int(mtime), progress_callback=progress_callback) connection.Close() - def Pull(self, device_filename, dest_file=None, timeout_ms=None, progress_callback=None,): + def Pull(self, device_filename, dest_file=None, timeout_ms=None, progress_callback=None): """Pull a file from the device. Args: From 550b39e7c429188f5284da8096b96e68ceec1461 Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Sat, 17 Feb 2018 16:15:20 -0500 Subject: [PATCH 25/38] Fixed merge issues --- adb/adb_commands.py | 2 +- adb/fastboot_debug.py | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/adb/adb_commands.py b/adb/adb_commands.py index 96d5953..26b5aba 100644 --- a/adb/adb_commands.py +++ b/adb/adb_commands.py @@ -271,7 +271,7 @@ def Pull(self, device_filename, dest_file=None, timeout_ms=None, progress_callba conn = self.protocol_handler.Open( self.handle, destination=b'sync:', timeout_ms=timeout_ms) - self.filesync_handler.Pull(conn, device_filename, dest_file, timeout_ms, progress_callback) + self.filesync_handler.Pull(conn, device_filename, dest_file, progress_callback) conn.Close() if isinstance(dest_file, io.BytesIO): diff --git a/adb/fastboot_debug.py b/adb/fastboot_debug.py index 59713d8..e9791a4 100755 --- a/adb/fastboot_debug.py +++ b/adb/fastboot_debug.py @@ -114,10 +114,7 @@ def SetProgress(current, total): bar.finish() kwargs['progress_callback'] = SetProgress - dev = fastboot.FastbootCommands() - dev.ConnectDevice(chunk_kb=args.chunk_kb) - - return common_cli.StartCli(args, dev, extra=kwargs) + return common_cli.StartCli(args, fastboot.FastbootCommands, extra=kwargs) if __name__ == '__main__': From fd09a4f1e28d69d98a1c42493bdce0115cefd959 Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Mon, 26 Feb 2018 18:04:37 -0500 Subject: [PATCH 26/38] removed reliance on future library --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 7516425..89fb47d 100644 --- a/setup.py +++ b/setup.py @@ -61,7 +61,6 @@ keywords = ['android', 'adb', 'fastboot'], install_requires = [ - 'future', 'libusb1>=1.0.16', rsa_signer_library ], From 06babd6410ab9501cb0e1ff64cd5bb6b77f4ca73 Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Mon, 26 Feb 2018 18:05:31 -0500 Subject: [PATCH 27/38] Improved FastbootCommands.ConnectDevice documentation. Fixed bug in fastboot_debug where chunk_kb was not being passed through --- adb/fastboot.py | 25 ++++++++++++++++++++++++- adb/fastboot_debug.py | 6 +++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/adb/fastboot.py b/adb/fastboot.py index 4d1e73d..7d69bea 100644 --- a/adb/fastboot.py +++ b/adb/fastboot.py @@ -226,7 +226,30 @@ 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.""" + """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 not kwargs.get('handle', None): self._handle = common.UsbHandle.FindAndOpen( diff --git a/adb/fastboot_debug.py b/adb/fastboot_debug.py index e9791a4..f904b90 100755 --- a/adb/fastboot_debug.py +++ b/adb/fastboot_debug.py @@ -114,7 +114,11 @@ def SetProgress(current, total): bar.finish() kwargs['progress_callback'] = SetProgress - return common_cli.StartCli(args, fastboot.FastbootCommands, extra=kwargs) + return common_cli.StartCli( + args, + fastboot.FastbootCommands, + chunk_kb=args.chunk_kb, + extra=kwargs) if __name__ == '__main__': From e5554962e6107b04fbcc2388e5403977f7f15544 Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Mon, 26 Feb 2018 18:05:54 -0500 Subject: [PATCH 28/38] FilesyncProtocol: Simplified handling of utf-8 decoding errors --- adb/filesync_protocol.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/adb/filesync_protocol.py b/adb/filesync_protocol.py index ea8ffb2..ef114a9 100644 --- a/adb/filesync_protocol.py +++ b/adb/filesync_protocol.py @@ -212,10 +212,7 @@ def Read(self, expected_ids, read_data=True): if command_id == b'FAIL': reason = '' if self.recv_buffer: - try: - reason = self.recv_buffer.decode('utf-8') - except: - pass + 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)) From b6284bf60d415d227649b5ed36f0ed9ec09fba9a Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Mon, 26 Feb 2018 18:06:23 -0500 Subject: [PATCH 29/38] Imported __future__.print_function for python2/3 compatible printing to stderr --- adb/common_cli.py | 1 + 1 file changed, 1 insertion(+) diff --git a/adb/common_cli.py b/adb/common_cli.py index 9f9147a..f3d2be6 100644 --- a/adb/common_cli.py +++ b/adb/common_cli.py @@ -19,6 +19,7 @@ outputting the results. """ +from __future__ import print_function import argparse import io import inspect From 257b3ad7e3323a2f3fd7f302cdcb1e05d839d687 Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Mon, 26 Feb 2018 18:06:47 -0500 Subject: [PATCH 30/38] AdbProtocol: Reverted reliance on future.iteritems --- adb/adb_protocol.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/adb/adb_protocol.py b/adb/adb_protocol.py index 4fd8a6c..0cf421e 100644 --- a/adb/adb_protocol.py +++ b/adb/adb_protocol.py @@ -20,7 +20,6 @@ import struct import time from io import BytesIO -from future.utils import iteritems from adb import usb_exceptions # Maximum amount of data in an ADB packet. @@ -78,7 +77,7 @@ def MakeWireIDs(ids): 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 iteritems(id_to_wire)} + wire_to_id = {wire: cmd_id for cmd_id, wire in id_to_wire.items()} return id_to_wire, wire_to_id From 636ceba6dcd5601e2bb6f14953b63a9983f347a5 Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Mon, 26 Feb 2018 18:08:04 -0500 Subject: [PATCH 31/38] Made handle a protected member, removed unused code, fixed line spacing, made build_prop a public property, improved documentation for ConnectDevice --- adb/adb_commands.py | 78 +++++++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 34 deletions(-) diff --git a/adb/adb_commands.py b/adb/adb_commands.py index 26b5aba..e0bf0a3 100644 --- a/adb/adb_commands.py +++ b/adb/adb_commands.py @@ -25,6 +25,7 @@ import io import os import socket +import posixpath from adb import adb_protocol from adb import common @@ -59,7 +60,8 @@ def __init__(self): self.__reset() def __reset(self): - self.handle = None + self.build_props = None + self._handle = None self._device_state = None # Connection table tracks each open AdbConnection objects per service type @@ -92,7 +94,7 @@ def _get_service_connection(self, service, service_command=None, create=True, ti destination_str = service connection = self.protocol_handler.Open( - self.handle, destination=destination_str, timeout_ms=timeout_ms) + self._handle, destination=destination_str, timeout_ms=timeout_ms) self._service_connections.update({service: connection}) @@ -105,23 +107,33 @@ def ConnectDevice(self, port_path=None, serial=None, default_timeout_ms=None, ** 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 not kwargs.get('handle', None): - - if serial and b':' 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) - + if 'handle' in kwargs: + self._handle = kwargs.pop('handle') + elif serial and b':' in serial: + self._handle = common.TcpHandle(serial, timeout_ms=default_timeout_ms) else: - self.handle = kwargs.pop('handle') + self._handle = common.UsbHandle.FindAndOpen( + DeviceIsAvailable, port_path=port_path, serial=serial, + timeout_ms=default_timeout_ms) self.__Connect(**kwargs) @@ -135,8 +147,8 @@ def Close(self): except: pass - if self.handle: - self.handle.Close() + if self._handle: + self._handle.Close() self.__reset() @@ -154,17 +166,14 @@ def __Connect(self, banner=None, **kwargs): if not banner: banner = socket.gethostname().encode() - conn_str = self.protocol_handler.Connect(self.handle, banner=banner, **kwargs) + 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'::') - device_state = parts[0] + self._device_state = parts[0] # Break out the build prop info - build_props = str(parts[1].split(b';')) - # print("Device build props: {}".format(build_props)) - - self._device_state = device_state + self.build_props = str(parts[1].split(b';')) return True @@ -196,7 +205,7 @@ def Install(self, apk_path, destination_dir='', timeout_ms=None, replace_existin if not destination_dir: destination_dir = '/data/local/tmp/' basename = os.path.basename(apk_path) - destination_path = os.path.join(destination_dir, basename) + 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'] @@ -244,7 +253,7 @@ def Push(self, source_file, device_filename, mtime='0', timeout_ms=None, progres with source_file: connection = self.protocol_handler.Open( - self.handle, destination=b'sync:', timeout_ms=timeout_ms) + self._handle, destination=b'sync:', timeout_ms=timeout_ms) self.filesync_handler.Push(connection, source_file, device_filename, mtime=int(mtime), progress_callback=progress_callback) connection.Close() @@ -267,9 +276,8 @@ def Pull(self, device_filename, dest_file=None, timeout_ms=None, progress_callba else: raise ValueError("destfile is of unknown type") - #conn = self._get_service_connection(b'sync:') conn = self.protocol_handler.Open( - self.handle, destination=b'sync:', timeout_ms=timeout_ms) + self._handle, destination=b'sync:', timeout_ms=timeout_ms) self.filesync_handler.Pull(conn, device_filename, dest_file, progress_callback) @@ -282,7 +290,7 @@ def Pull(self, device_filename, dest_file=None, timeout_ms=None, progress_callba def Stat(self, device_filename): """Get a file's stat() information.""" - connection = self.protocol_handler.Open(self.handle, destination=b'sync:') + connection = self.protocol_handler.Open(self._handle, destination=b'sync:') mode, size, mtime = self.filesync_handler.Stat( connection, device_filename) connection.Close() @@ -294,7 +302,7 @@ def List(self, device_path): Args: device_path: Directory to list. """ - connection = self.protocol_handler.Open(self.handle, destination=b'sync:') + connection = self.protocol_handler.Open(self._handle, destination=b'sync:') listing = self.filesync_handler.List(connection, device_path) connection.Close() return listing @@ -305,7 +313,7 @@ def Reboot(self, destination=b''): Args: destination: Specify 'bootloader' for fastboot. """ - self.protocol_handler.Open(self.handle, b'reboot:%s' % destination) + self.protocol_handler.Open(self._handle, b'reboot:%s' % destination) def RebootBootloader(self): """Reboot device into fastboot.""" @@ -313,19 +321,19 @@ def RebootBootloader(self): def Remount(self): """Remount / as read-write.""" - return self.protocol_handler.Command(self.handle, service=b'remount') + 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') + 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') + 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') + 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. @@ -335,7 +343,7 @@ def Shell(self, command, timeout_ms=None): timeout_ms: Maximum time to allow the command to run. """ return self.protocol_handler.Command( - self.handle, service=b'shell', command=command, + self._handle, service=b'shell', command=command, timeout_ms=timeout_ms) def StreamingShell(self, command, timeout_ms=None): @@ -349,7 +357,7 @@ def StreamingShell(self, command, timeout_ms=None): The responses from the shell command. """ return self.protocol_handler.StreamingCommand( - self.handle, service=b'shell', command=command, + self._handle, service=b'shell', command=command, timeout_ms=timeout_ms) def Logcat(self, options, timeout_ms=None): @@ -377,4 +385,6 @@ def InteractiveShell(self, cmd=None, strip_cmd=True, delim=None, strip_delim=Tru """ 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) \ No newline at end of file + return self.protocol_handler.InteractiveShellCommand( + conn, cmd=cmd, strip_cmd=strip_cmd, + delim=delim, strip_delim=strip_delim) From a308704cdcb197297f03446bcd5af85e1ce3cd95 Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Mon, 26 Feb 2018 18:16:48 -0500 Subject: [PATCH 32/38] Simplified logic in fastboot.ConnectDevice --- adb/fastboot.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/adb/fastboot.py b/adb/fastboot.py index 7d69bea..e9028a7 100644 --- a/adb/fastboot.py +++ b/adb/fastboot.py @@ -251,12 +251,13 @@ def ConnectDevice(self, port_path=None, serial=None, default_timeout_ms=None, ch used instead of a USB connection. """ - if not kwargs.get('handle', None): + 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) - else: - self._handle = kwargs['handle'] self._protocol = FastbootProtocol(self._handle, chunk_kb) From 6d7404ef89da37f24099f792c4f68dd4c6de60c0 Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Mon, 26 Feb 2018 18:26:28 -0500 Subject: [PATCH 33/38] setup.py: Removed duplicate version string for M2Crypto requirements --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f573aeb..3cd715b 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ rsa_signer_library = 'pycryptodome' except ImportError: - rsa_signer_library = 'M2Crypto>=0.21.1,<=0.26.4' + pass setup( From 7a1a1c7c86e152951ffa022dfe1f47c57771f6b0 Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Mon, 26 Feb 2018 18:34:56 -0500 Subject: [PATCH 34/38] Fixed check for bytes in filesync_protocol --- adb/filesync_protocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adb/filesync_protocol.py b/adb/filesync_protocol.py index ef114a9..e046dd0 100644 --- a/adb/filesync_protocol.py +++ b/adb/filesync_protocol.py @@ -187,7 +187,7 @@ def Send(self, command_id, data=b'', size=0): size: Optionally override size from len(data). """ if data: - if isinstance(data, str): + if not isinstance(data, bytes): data = data.encode('utf8') size = len(data) From 45f0f0a9ab6596dcffd402227b8123bf10c25f88 Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Mon, 26 Feb 2018 18:49:09 -0500 Subject: [PATCH 35/38] Updated documentation around self._service_connections --- adb/adb_commands.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/adb/adb_commands.py b/adb/adb_commands.py index e0bf0a3..9a53d5a 100644 --- a/adb/adb_commands.py +++ b/adb/adb_commands.py @@ -64,11 +64,10 @@ def __reset(self): self._handle = None self._device_state = None - # Connection table tracks each open AdbConnection objects per service type - # By default, the only service connections that make sense to hold open is (interactive) shell - self._service_connections = { - b'shell:': 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): """ From c8fcbc481ca6b6c31351a04dfff387f8443cd7d5 Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Mon, 26 Feb 2018 19:21:03 -0500 Subject: [PATCH 36/38] Additional docstrings for progress_callback parameter --- adb/adb_commands.py | 2 ++ adb/filesync_protocol.py | 9 +++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/adb/adb_commands.py b/adb/adb_commands.py index 9a53d5a..abd057c 100644 --- a/adb/adb_commands.py +++ b/adb/adb_commands.py @@ -264,6 +264,8 @@ def Pull(self, device_filename, dest_file=None, timeout_ms=None, progress_callba 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 diff --git a/adb/filesync_protocol.py b/adb/filesync_protocol.py index e046dd0..fdecd4a 100644 --- a/adb/filesync_protocol.py +++ b/adb/filesync_protocol.py @@ -95,7 +95,12 @@ def Pull(cls, connection, filename, dest_file, progress_callback): @classmethod def _HandleProgress(cls, progress_callback): - """Calls the callback with the current progress and total .""" + """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 @@ -115,7 +120,7 @@ def Push(cls, connection, datafile, filename, 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, + progress_callback: callback method that accepts filename, bytes_written and total_bytes Raises: PushFailedError: Raised on push failure. From 2eea6b9c8c659cc2f4e67df57ef798a801dffe73 Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Sat, 3 Mar 2018 15:42:40 -0500 Subject: [PATCH 37/38] Updated adb_commands._Connect to be a protected member that could be called externally. Fixed documentation around the method and its relationship to ConnectDevice() --- adb/adb_commands.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/adb/adb_commands.py b/adb/adb_commands.py index abd057c..c9ca631 100644 --- a/adb/adb_commands.py +++ b/adb/adb_commands.py @@ -100,7 +100,8 @@ def _get_service_connection(self, service, service_command=None, create=True, ti return connection def ConnectDevice(self, port_path=None, serial=None, default_timeout_ms=None, **kwargs): - """Convenience function to get an adb device from usb path or serial. + """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. @@ -134,7 +135,7 @@ def ConnectDevice(self, port_path=None, serial=None, default_timeout_ms=None, ** DeviceIsAvailable, port_path=port_path, serial=serial, timeout_ms=default_timeout_ms) - self.__Connect(**kwargs) + self._Connect(**kwargs) return self @@ -151,13 +152,13 @@ def Close(self): self.__reset() - def __Connect(self, banner=None, **kwargs): + def _Connect(self, banner=None, **kwargs): """Connect to the device. Args: banner: See protocol_handler.Connect. - **kwargs: See protocol_handler.Connect for kwargs. Includes rsa_keys, - and auth_timeout_ms. + **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. """ From 5badca3eadc75132b805c19929edfc7f4149e0e6 Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Sat, 3 Mar 2018 15:42:51 -0500 Subject: [PATCH 38/38] Updated contributors --- CONTRIBUTORS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 2fc5d46..ddb520b 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -6,3 +6,5 @@ Marc-Antoine Ruel Max Borghino Mohammad Abu-Garbeyyeh Josip Delic +Greg E. +