Source code for adb.filesync_protocol

# Copyright 2014 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""ADB protocol implementation.

Implements the ADB protocol as seen in android's adb/adbd binaries, but only the
host side.


.. rubric:: Contents

* :class:`FileSyncConnection`

    * :meth:`FileSyncConnection.Read`
    * :meth:`FileSyncConnection.ReadUntil`
    * :meth:`FileSyncConnection.Send`
    * :meth:`FileSyncConnection._CanAddToSendBuffer`
    * :meth:`FileSyncConnection._Flush`
    * :meth:`FileSyncConnection._ReadBuffered`

* :class:`FilesyncProtocol`

    * :meth:`FilesyncProtocol._HandleProgress`
    * :meth:`FilesyncProtocol.List`
    * :meth:`FilesyncProtocol.Pull`
    * :meth:`FilesyncProtocol.Push`
    * :meth:`FilesyncProtocol.Stat`

* :class:`InterleavedDataError`
* :class:`InvalidChecksumError`
* :class:`PullFailedError`
* :class:`PushFailedError`

"""

import collections
import io
import os
import stat
import struct
import time

import libusb1

from adb import adb_protocol
from adb import usb_exceptions


try:
    file_types = (file, io.IOBase)
except NameError:
    file_types = (io.IOBase,)

#: 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


[docs]class InvalidChecksumError(Exception): """Checksum of data didn't match expected checksum. .. image:: _static/adb.filesync_protocol.InvalidChecksumError.CALL_GRAPH.svg """
[docs]class InterleavedDataError(Exception): """We only support command sent serially. .. image:: _static/adb.filesync_protocol.InterleavedDataError.CALL_GRAPH.svg """
[docs]class PushFailedError(Exception): """Pushing a file failed for some reason. .. image:: _static/adb.filesync_protocol.PushFailedError.CALL_GRAPH.svg """
[docs]class PullFailedError(Exception): """Pulling a file failed for some reason. .. image:: _static/adb.filesync_protocol.PullFailedError.CALL_GRAPH.svg """
DeviceFile = collections.namedtuple('DeviceFile', ['filename', 'mode', 'size', 'mtime'])
[docs]class FilesyncProtocol(object): """Implements the FileSync protocol as described in sync.txt."""
[docs] @staticmethod def Stat(connection, filename): """Get file status (mode, size, and mtime). .. image:: _static/adb.filesync_protocol.FilesyncProtocol.Stat.CALLER_GRAPH.svg Parameters ---------- connection : adb.adb_protocol._AdbConnection ADB connection filename : str, bytes The file for which we are getting info Returns ------- mode : int The mode of the file size : int The size of the file mtime : int The time of last modification for the file Raises ------ adb.adb_protocol.InvalidResponseError Expected STAT response to STAT, got something else """ 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
[docs] @classmethod def List(cls, connection, path): """Get a list of the files in ``path``. Parameters ---------- connection : adb.adb_protocol._AdbConnection ADB connection path : str, bytes The path for which we are getting a list of files Returns ------- files : list[DeviceFile] Information about the files in ``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
[docs] @classmethod def Pull(cls, connection, filename, dest_file, progress_callback): """Pull a file from the device into the file-like ``dest_file``. .. image:: _static/adb.filesync_protocol.FilesyncProtocol.Pull.CALL_GRAPH.svg Parameters ---------- connection : adb.adb_protocol._AdbConnection ADB connection filename : str The file to be pulled dest_file : _io.BytesIO File-like object for writing to progress_callback : function, None Callback method that accepts ``filename``, ``bytes_written``, and ``total_bytes`` Raises ------ PullFailedError Unable to pull file """ if progress_callback: total_bytes = cls.Stat(connection, filename)[1] progress = cls._HandleProgress(lambda current: progress_callback(filename, current, total_bytes)) next(progress) cnxn = FileSyncConnection(connection, b'<2I') try: cnxn.Send(b'RECV', filename) for cmd_id, _, data in cnxn.ReadUntil((b'DATA',), b'DONE'): if cmd_id == b'DONE': break dest_file.write(data) if progress_callback: progress.send(len(data)) except usb_exceptions.CommonUsbError as e: raise PullFailedError('Unable to pull file %s due to: %s' % (filename, e))
[docs] @classmethod def _HandleProgress(cls, progress_callback): """Calls the callback with the current progress and total bytes written/received. .. image:: _static/adb.filesync_protocol.FilesyncProtocol._HandleProgress.CALL_GRAPH.svg .. image:: _static/adb.filesync_protocol.FilesyncProtocol._HandleProgress.CALLER_GRAPH.svg Parameters ---------- progress_callback : function Callback method that accepts ``filename``, ``bytes_written``, and ``total_bytes``; total_bytes will be -1 for file-like objects. """ current = 0 while True: current += yield try: progress_callback(current) except Exception: # pylint: disable=broad-except continue
[docs] @classmethod def Push(cls, connection, datafile, filename, st_mode=DEFAULT_PUSH_MODE, mtime=0, progress_callback=None): """Push a file-like object to the device. .. image:: _static/adb.filesync_protocol.FilesyncProtocol.Push.CALL_GRAPH.svg .. image:: _static/adb.filesync_protocol.FilesyncProtocol.Push.CALLER_GRAPH.svg Parameters ---------- connection : adb.adb_protocol._AdbConnection ADB connection datafile : _io.BytesIO File-like object for reading from filename : str Filename to push to st_mode : int Stat mode for filename mtime : int Modification time progress_callback : function, None Callback method that accepts ``filename``, ``bytes_written``, and ``total_bytes`` Raises ------ PushFailedError Raised on push failure. """ fileinfo = ('{},{}'.format(filename, int(st_mode))).encode('utf-8') cnxn = FileSyncConnection(connection, b'<2I') cnxn.Send(b'SEND', fileinfo) if progress_callback: total_bytes = os.fstat(datafile.fileno()).st_size if isinstance(datafile, file_types) else -1 progress = cls._HandleProgress(lambda current: progress_callback(filename, current, total_bytes)) next(progress) while True: data = datafile.read(MAX_PUSH_DATA) if data: cnxn.Send(b'DATA', data) if progress_callback: progress.send(len(data)) else: break if mtime == 0: mtime = int(time.time()) # DONE doesn't send data, but it hides the last bit of data in the size # field. cnxn.Send(b'DONE', size=mtime) for cmd_id, _, data in cnxn.ReadUntil((), b'OKAY', b'FAIL'): if cmd_id == b'OKAY': return raise PushFailedError(data)
[docs]class FileSyncConnection(object): """Encapsulate a FileSync service connection. Parameters ---------- adb_connection : adb.adb_protocol._AdbConnection ADB connection recv_header_format : bytes TODO Attributes ---------- adb : adb.adb_protocol._AdbConnection ADB connection send_buffer : byte_array ``bytearray(adb_protocol.MAX_ADB_DATA)`` (see :const:`adb.adb_protocol.MAX_ADB_DATA`) send_idx : int TODO send_header_len : int ``struct.calcsize(b'<2I')`` recv_buffer : bytearray TODO recv_header_format : bytes TODO recv_header_len : int ``struct.calcsize(recv_header_format)`` """ 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)
[docs] 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. .. image:: _static/adb.filesync_protocol.FileSyncConnection.Send.CALL_GRAPH.svg Parameters ---------- command_id : bytes Command to send. data : str, bytes Optional data to send, must set data or size. size : int 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)
[docs] def Read(self, expected_ids, read_data=True): """Read ADB messages and return FileSync packets. .. image:: _static/adb.filesync_protocol.FileSyncConnection.Read.CALL_GRAPH.svg .. image:: _static/adb.filesync_protocol.FileSyncConnection.Read.CALLER_GRAPH.svg Parameters ---------- expected_ids : tuple[bytes] If the received header ID is not in ``expected_ids``, an exception will be raised read_data : bool Whether to read the received data Returns ------- command_id : bytes The received header ID tuple TODO data : bytearray The received data Raises ------ adb.usb_exceptions.AdbCommandFailureException Command failed adb.adb_protocol.InvalidResponseError Received response was not in ``expected_ids`` """ if self.send_idx: self._Flush() # Read one filesync packet off the recv buffer. header_data = self._ReadBuffered(self.recv_header_len) header = struct.unpack(self.recv_header_format, header_data) # Header is (ID, ...). command_id = self.wire_to_id[header[0]] if command_id not in expected_ids: if command_id == b'FAIL': reason = '' if self.recv_buffer: reason = self.recv_buffer.decode('utf-8', errors='ignore') raise usb_exceptions.AdbCommandFailureException('Command failed: {}'.format(reason)) raise adb_protocol.InvalidResponseError('Expected one of %s, got %s' % (expected_ids, command_id)) if not read_data: return command_id, header[1:] # Header is (ID, ..., size). size = header[-1] data = self._ReadBuffered(size) return command_id, header[1:-1], data
[docs] def ReadUntil(self, expected_ids, *finish_ids): """Useful wrapper around :meth:`FileSyncConnection.Read`. .. image:: _static/adb.filesync_protocol.FileSyncConnection.ReadUntil.CALL_GRAPH.svg Parameters ---------- expected_ids : tuple[bytes] If the received header ID is not in ``expected_ids``, an exception will be raised finish_ids : tuple[bytes] We will read until we find a header ID that is in ``finish_ids`` Yields ------ cmd_id : bytes The received header ID header : tuple TODO data : bytearray The received data """ while True: cmd_id, header, data = self.Read(expected_ids + finish_ids) yield cmd_id, header, data if cmd_id in finish_ids: break
[docs] def _CanAddToSendBuffer(self, data_len): """Determine whether ``data_len`` bytes of data can be added to the send buffer without exceeding :const:`adb.adb_protocol.MAX_ADB_DATA`. .. image:: _static/adb.filesync_protocol.FileSyncConnection._CanAddToSendBuffer.CALLER_GRAPH.svg Parameters ---------- data_len : int The length of the data to be potentially added to the send buffer (not including the length of its header) Returns ------- bool Whether ``data_len`` bytes of data can be added to the send buffer without exceeding :const:`adb.adb_protocol.MAX_ADB_DATA` """ added_len = self.send_header_len + data_len return self.send_idx + added_len < adb_protocol.MAX_ADB_DATA
[docs] def _Flush(self): """TODO .. image:: _static/adb.filesync_protocol.FileSyncConnection._Flush.CALLER_GRAPH.svg Raises ------ adb.usb_exceptions.WriteFailedError Could not send data """ try: self.adb.Write(self.send_buffer[:self.send_idx]) except libusb1.USBError as e: raise usb_exceptions.WriteFailedError('Could not send data %s' % self.send_buffer, e) self.send_idx = 0
[docs] def _ReadBuffered(self, size): """Read ``size`` bytes of data from ``self.recv_buffer``. .. image:: _static/adb.filesync_protocol.FileSyncConnection._ReadBuffered.CALLER_GRAPH.svg Parameters ---------- size : int The amount of data to read Returns ------- result : bytearray The read data """ # 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