diff --git a/.travis.yml b/.travis.yml index e1a97ae..298cdb7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,10 @@ language: python python: - "2.7" + - "3.5" + - "3.6" - "3.7" + - "3.8" addons: apt: packages: @@ -9,9 +12,12 @@ addons: - libusb-1.0-0-dev install: - pip install . - - pip install flake8 pylint coveralls cryptography libusb1>=1.0.16 + - pip install flake8 pylint coveralls cryptography libusb1>=1.0.16 pycryptodome - python --version 2>&1 | grep -q "Python 2" && pip install mock || true script: -- flake8 adb_shell/ && pylint adb_shell/ && coverage run --source adb_shell setup.py test && coverage report -m + - if python --version 2>&1 | grep -q "Python 2" || python --version 2>&1 | grep -q "Python 3.5"; then flake8 adb_shell/ --exclude="adb_shell/adb_device_async.py,adb_shell/handle/base_handle_async.py,adb_shell/handle/tcp_handle_async.py" && pylint --ignore="adb_device_async.py,base_handle_async.py,tcp_handle_async.py" adb_shell/; fi + - if python --version 2>&1 | grep -q "Python 3.6" || python --version 2>&1 | grep -q "Python 3.7" || python --version 2>&1 | grep -q "Python 3.8"; then flake8 adb_shell/ && pylint adb_shell/; fi + - if python --version 2>&1 | grep -q "Python 2" || python --version 2>&1 | grep -q "Python 3.5"; then for synctest in $(cd tests && ls test*.py | grep -v async); do python -m unittest discover -s tests/ -t . -p "$synctest" || exit 1; done; fi + - if python --version 2>&1 | grep -q "Python 3.6" || python --version 2>&1 | grep -q "Python 3.7" || python --version 2>&1 | grep -q "Python 3.8"; then coverage run --source adb_shell -m unittest discover -s tests/ -t . && coverage report -m; fi after_success: -- coveralls + - if python --version 2>&1 | grep -q "Python 3.6" || python --version 2>&1 | grep -q "Python 3.7" || python --version 2>&1 | grep -q "Python 3.8"; then coveralls; fi diff --git a/Makefile b/Makefile index f249ac6..0b8e622 100644 --- a/Makefile +++ b/Makefile @@ -11,27 +11,25 @@ docs: @cd docs && sphinx-apidoc -f -e -o source/ ../adb_shell/ @cd docs && make html && make html -.PHONY: doxygen -doxygen: - rm -rf docs/html - doxygen Doxyfile +SYNCTESTS := $(shell cd tests && ls test*.py | grep -v async) .PHONY: test test: - python setup.py test + python --version 2>&1 | grep -q "Python 2" && (for synctest in $(SYNCTESTS); do python -m unittest discover -s tests/ -t . -p "$$synctest"; done) || true + python --version 2>&1 | grep -q "Python 3" && python -m unittest discover -s tests/ -t . || true .PHONY: coverage coverage: - coverage run --source adb_shell setup.py test && coverage html && coverage report -m + coverage run --source adb_shell -m unittest discover -s tests/ -t . && coverage html && coverage report -m .PHONY: tdd tdd: - coverage run --source adb_shell setup.py test && coverage report -m + coverage run --source adb_shell -m unittest discover -s tests/ -t . && coverage report -m .PHONY: lint lint: - flake8 adb_shell/ && pylint adb_shell/ + python --version 2>&1 | grep -q "Python 2" && (flake8 adb_shell/ --exclude="adb_shell/adb_device_async.py,adb_shell/handle/base_handle_async.py,adb_shell/handle/tcp_handle_async.py" && pylint --ignore="adb_device_async.py,base_handle_async.py,tcp_handle_async.py" adb_shell/) || (flake8 adb_shell/ && pylint adb_shell/) .PHONY: alltests alltests: - flake8 adb_shell/ && pylint adb_shell/ && coverage run --source adb_shell setup.py test && coverage report -m + flake8 adb_shell/ && pylint adb_shell/ && coverage run --source adb_shell -m unittest discover -s tests/ -t . && coverage report -m diff --git a/adb_shell/adb_device.py b/adb_shell/adb_device.py index d3b2590..7334f72 100644 --- a/adb_shell/adb_device.py +++ b/adb_shell/adb_device.py @@ -22,11 +22,6 @@ .. rubric:: Contents -* :class:`_AdbTransactionInfo` -* :class:`_FileSyncTransactionInfo` - - * :meth:`_FileSyncTransactionInfo.can_add_to_send_buffer` - * :class:`AdbDevice` * :meth:`AdbDevice._close` @@ -65,8 +60,6 @@ """ -from collections import namedtuple -from contextlib import contextmanager import io import logging import os @@ -79,6 +72,7 @@ from .adb_message import AdbMessage, checksum, unpack from .handle.base_handle import BaseHandle from .handle.tcp_handle import TcpHandle +from .hidden_helpers import FILE_TYPES, DeviceFile, _AdbTransactionInfo, _FileSyncTransactionInfo, _open try: from .handle.usb_handle import UsbHandle @@ -86,128 +80,8 @@ UsbHandle = None -try: - FILE_TYPES = (file, io.IOBase) -except NameError: # pragma: no cover - FILE_TYPES = (io.IOBase,) - _LOGGER = logging.getLogger(__name__) -DeviceFile = namedtuple('DeviceFile', ['filename', 'mode', 'size', 'mtime']) - - -@contextmanager -def _open(name, mode='r'): - """Handle opening and closing of files and IO streams. - - Parameters - ---------- - name : str, io.IOBase - The name of the file *or* an IO stream - mode : str - The mode for opening the file - - Yields - ------ - io.IOBase - The opened file *or* the IO stream - - """ - try: - opened = open(name, mode) if isinstance(name, str) else None - if isinstance(name, str): - yield opened - else: - yield name - finally: - if isinstance(name, str): - opened.close() - else: - name.close() - - -class _AdbTransactionInfo(object): # pylint: disable=too-few-public-methods - """A class for storing info and settings used during a single ADB "transaction." - - Parameters - ---------- - local_id : int - The ID for the sender (i.e., the device running this code) - remote_id : int - The ID for the recipient - timeout_s : float, None - Timeout in seconds for sending and receiving packets, or ``None``; see :meth:`BaseHandle.bulk_read() ` - and :meth:`BaseHandle.bulk_write() ` - total_timeout_s : float - The total time in seconds to wait for a command in ``expected_cmds`` in :meth:`AdbDevice._read` - - Attributes - ---------- - local_id : int - The ID for the sender (i.e., the device running this code) - remote_id : int - The ID for the recipient - timeout_s : float, None - Timeout in seconds for sending and receiving packets, or ``None``; see :meth:`BaseHandle.bulk_read() ` - and :meth:`BaseHandle.bulk_write() ` - total_timeout_s : float - The total time in seconds to wait for a command in ``expected_cmds`` in :meth:`AdbDevice._read` - - """ - def __init__(self, local_id, remote_id, timeout_s=None, total_timeout_s=constants.DEFAULT_TOTAL_TIMEOUT_S): - self.local_id = local_id - self.remote_id = remote_id - self.timeout_s = timeout_s - self.total_timeout_s = total_timeout_s - - -class _FileSyncTransactionInfo(object): # pylint: disable=too-few-public-methods - """A class for storing info used during a single FileSync "transaction." - - Parameters - ---------- - recv_message_format : bytes - The FileSync message format - - Attributes - ---------- - recv_buffer : bytearray - A buffer for storing received data - recv_message_format : bytes - The FileSync message format - recv_message_size : int - The FileSync message size - send_buffer : bytearray - A buffer for storing data to be sent - send_idx : int - The index in ``recv_buffer`` that will be the start of the next data packet sent - - """ - def __init__(self, recv_message_format): - self.send_buffer = bytearray(constants.MAX_ADB_DATA) - self.send_idx = 0 - - self.recv_buffer = bytearray() - self.recv_message_format = recv_message_format - self.recv_message_size = struct.calcsize(recv_message_format) - - def can_add_to_send_buffer(self, data_len): - """Determine whether ``data_len`` bytes of data can be added to the send buffer without exceeding :const:`constants.MAX_ADB_DATA`. - - 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:`constants.MAX_ADB_DATA` - - """ - added_len = self.recv_message_size + data_len - return self.send_idx + added_len < constants.MAX_ADB_DATA - class AdbDevice(object): """A class with methods for connecting to a device and executing ADB commands. diff --git a/adb_shell/adb_device_async.py b/adb_shell/adb_device_async.py new file mode 100644 index 0000000..6b8ceb5 --- /dev/null +++ b/adb_shell/adb_device_async.py @@ -0,0 +1,1162 @@ +# Copyright (c) 2020 Jeff Irion and contributors +# +# This file is part of the adb-shell package. It incorporates work +# covered by the following license notice: +# +# +# 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. + +"""Implement the :class:`AdbDeviceAsync` class, which can connect to a device and run ADB shell commands. + +* :class:`AdbDeviceAsync` + + * :meth:`AdbDeviceAsync._close` + * :meth:`AdbDeviceAsync._filesync_flush` + * :meth:`AdbDeviceAsync._filesync_read` + * :meth:`AdbDeviceAsync._filesync_read_buffered` + * :meth:`AdbDeviceAsync._filesync_read_until` + * :meth:`AdbDeviceAsync._filesync_send` + * :meth:`AdbDeviceAsync._handle_progress` + * :meth:`AdbDeviceAsync._okay` + * :meth:`AdbDeviceAsync._open` + * :meth:`AdbDeviceAsync._pull` + * :meth:`AdbDeviceAsync._push` + * :meth:`AdbDeviceAsync._read` + * :meth:`AdbDeviceAsync._read_until` + * :meth:`AdbDeviceAsync._read_until_close` + * :meth:`AdbDeviceAsync._send` + * :meth:`AdbDeviceAsync._service` + * :meth:`AdbDeviceAsync._streaming_command` + * :meth:`AdbDeviceAsync._streaming_service` + * :meth:`AdbDeviceAsync._write` + * :attr:`AdbDeviceAsync.available` + * :meth:`AdbDeviceAsync.close` + * :meth:`AdbDeviceAsync.connect` + * :meth:`AdbDeviceAsync.list` + * :meth:`AdbDeviceAsync.pull` + * :meth:`AdbDeviceAsync.push` + * :meth:`AdbDeviceAsync.root` + * :meth:`AdbDeviceAsync.shell` + * :meth:`AdbDeviceAsync.stat` + * :meth:`AdbDeviceAsync.streaming_shell` + +* :class:`AdbDeviceTcpAsync` + +""" + + +import io +import logging +import os +import socket +import struct +import time + +from . import constants +from . import exceptions +from .adb_message import AdbMessage, checksum, unpack +from .handle.base_handle_async import BaseHandleAsync +from .handle.tcp_handle_async import TcpHandleAsync +from .hidden_helpers import FILE_TYPES, DeviceFile, _AdbTransactionInfo, _FileSyncTransactionInfo, _open + + +_LOGGER = logging.getLogger(__name__) + + +class AdbDeviceAsync(object): + """A class with methods for connecting to a device and executing ADB commands. + + Parameters + ---------- + handle : BaseHandleAsync + A user-provided handle for communicating with the device; must be an instance of a subclass of :class:`~adb_shell.handle.base_handle_async.BaseHandleAsync` + banner : str, bytes, None + The hostname of the machine where the Python interpreter is currently running; if + it is not provided, it will be determined via ``socket.gethostname()`` + + Raises + ------ + adb_shell.exceptions.InvalidHandleError + The passed ``handle`` is not an instance of a subclass of :class:`~adb_shell.handle.base_handle_async.BaseHandleAsync` + + Attributes + ---------- + _available : bool + Whether an ADB connection to the device has been established + _banner : bytearray, bytes + The hostname of the machine where the Python interpreter is currently running + _handle : BaseHandleAsync + The handle that is used to connect to the device; must be a subclass of :class:`~adb_shell.handle.base_handle_async.BaseHandleAsync` + + """ + + def __init__(self, handle, banner=None): + if banner: + if not isinstance(banner, (bytes, bytearray)): + self._banner = bytearray(banner, 'utf-8') + else: + self._banner = banner + else: + try: + # TODO: make this async / don't do I/O + self._banner = bytearray(socket.gethostname(), 'utf-8') + except: # noqa pylint: disable=bare-except + self._banner = bytearray('unknown', 'utf-8') + + if not isinstance(handle, BaseHandleAsync): + raise exceptions.InvalidHandleError("`handle` must be an instance of a subclass of `BaseHandleAsync`") + + self._handle = handle + + self._available = False + + @property + def available(self): + """Whether or not an ADB connection to the device has been established. + + Returns + ------- + bool + ``self._available`` + + """ + return self._available + + async def close(self): + """Close the connection via the provided handle's ``close()`` method. + + """ + self._available = False + await self._handle.close() + + async def connect(self, rsa_keys=None, timeout_s=None, auth_timeout_s=constants.DEFAULT_AUTH_TIMEOUT_S, total_timeout_s=constants.DEFAULT_TOTAL_TIMEOUT_S, auth_callback=None): + """Establish an ADB connection to the device. + + 1. Use the handle to establish a connection + 2. Send a ``b'CNXN'`` message + 3. Unpack the ``cmd``, ``arg0``, ``arg1``, and ``banner`` fields from the response + 4. If ``cmd`` is not ``b'AUTH'``, then authentication is not necesary and so we are done + 5. If no ``rsa_keys`` are provided, raise an exception + 6. Loop through our keys, signing the last ``banner`` that we received + + 1. If the last ``arg0`` was not :const:`adb_shell.constants.AUTH_TOKEN`, raise an exception + 2. Sign the last ``banner`` and send it in an ``b'AUTH'`` message + 3. Unpack the ``cmd``, ``arg0``, and ``banner`` fields from the response via :func:`adb_shell.adb_message.unpack` + 4. If ``cmd`` is ``b'CNXN'``, return ``banner`` + + 7. None of the keys worked, so send ``rsa_keys[0]``'s public key; if the response does not time out, we must have connected successfully + + + Parameters + ---------- + rsa_keys : list, None + A list of signers of type :class:`~adb_shell.auth.sign_cryptography.CryptographySigner`, + :class:`~adb_shell.auth.sign_pycryptodome.PycryptodomeAuthSigner`, or :class:`~adb_shell.auth.sign_pythonrsa.PythonRSASigner` + timeout_s : float, None + Timeout in seconds for sending and receiving packets, or ``None``; see :meth:`BaseHandleAsync.bulk_read() ` + and :meth:`BaseHandleAsync.bulk_write() ` + auth_timeout_s : float, None + The time in seconds to wait for a ``b'CNXN'`` authentication response + total_timeout_s : float + The total time in seconds to wait for expected commands in :meth:`AdbDeviceAsync._read` + auth_callback : function, None + Function callback invoked when the connection needs to be accepted on the device + + Returns + ------- + bool + Whether the connection was established (:attr:`AdbDeviceAsync.available`) + + Raises + ------ + adb_shell.exceptions.DeviceAuthError + Device authentication required, no keys available + adb_shell.exceptions.InvalidResponseError + Invalid auth response from the device + + """ + # 1. Use the handle to establish a connection + await self._handle.close() + await self._handle.connect(timeout_s) + + # 2. Send a ``b'CNXN'`` message + msg = AdbMessage(constants.CNXN, constants.VERSION, constants.MAX_ADB_DATA, b'host::%s\0' % self._banner) + adb_info = _AdbTransactionInfo(None, None, timeout_s, total_timeout_s) + await self._send(msg, adb_info) + + # 3. Unpack the ``cmd``, ``arg0``, ``arg1``, and ``banner`` fields from the response + cmd, arg0, arg1, banner = await self._read([constants.AUTH, constants.CNXN], adb_info) + + # 4. If ``cmd`` is not ``b'AUTH'``, then authentication is not necesary and so we are done + if cmd != constants.AUTH: + self._available = True + return True # return banner + + # 5. If no ``rsa_keys`` are provided, raise an exception + if not rsa_keys: + await self._handle.close() + raise exceptions.DeviceAuthError('Device authentication required, no keys available.') + + # 6. Loop through our keys, signing the last ``banner`` that we received + for rsa_key in rsa_keys: + # 6.1. If the last ``arg0`` was not :const:`adb_shell.constants.AUTH_TOKEN`, raise an exception + if arg0 != constants.AUTH_TOKEN: + await self._handle.close() + raise exceptions.InvalidResponseError('Unknown AUTH response: %s %s %s' % (arg0, arg1, banner)) + + # 6.2. Sign the last ``banner`` and send it in an ``b'AUTH'`` message + signed_token = rsa_key.Sign(banner) + msg = AdbMessage(constants.AUTH, constants.AUTH_SIGNATURE, 0, signed_token) + await self._send(msg, adb_info) + + # 6.3. Unpack the ``cmd``, ``arg0``, and ``banner`` fields from the response via :func:`adb_shell.adb_message.unpack` + cmd, arg0, _, banner = await self._read([constants.CNXN, constants.AUTH], adb_info) + + # 6.4. If ``cmd`` is ``b'CNXN'``, return ``banner`` + if cmd == constants.CNXN: + self._available = True + return True # return banner + + # 7. None of the keys worked, so send ``rsa_keys[0]``'s public key; if the response does not time out, we must have connected successfully + pubkey = rsa_keys[0].GetPublicKey() + if not isinstance(pubkey, (bytes, bytearray)): + pubkey = bytearray(pubkey, 'utf-8') + + if auth_callback is not None: + auth_callback(self) + + msg = AdbMessage(constants.AUTH, constants.AUTH_RSAPUBLICKEY, 0, pubkey + b'\0') + await self._send(msg, adb_info) + + adb_info.timeout_s = auth_timeout_s + cmd, arg0, _, banner = await self._read([constants.CNXN], adb_info) + self._available = True + return True # return banner + + # ======================================================================= # + # # + # Services # + # # + # ======================================================================= # + async def _service(self, service, command, timeout_s=None, total_timeout_s=constants.DEFAULT_TOTAL_TIMEOUT_S, decode=True): + """Send an ADB command to the device. + + Parameters + ---------- + service : bytes + The ADB service to talk to (e.g., ``b'shell'``) + command : bytes + The command that will be sent + timeout_s : float, None + Timeout in seconds for sending and receiving packets, or ``None``; see :meth:`BaseHandleAsync.bulk_read() ` + and :meth:`BaseHandleAsync.bulk_write() ` + total_timeout_s : float + The total time in seconds to wait for a ``b'CLSE'`` or ``b'OKAY'`` command in :meth:`AdbDeviceAsync._read` + decode : bool + Whether to decode the output to utf8 before returning + + Returns + ------- + bytes, str + The output of the ADB command as a string if ``decode`` is True, otherwise as bytes. + + """ + adb_info = _AdbTransactionInfo(None, None, timeout_s, total_timeout_s) + if decode: + return b''.join([x async for x in self._streaming_command(service, command, adb_info)]).decode('utf8') + return b''.join([x async for x in self._streaming_command(service, command, adb_info)]) + + async def _streaming_service(self, service, command, timeout_s=None, total_timeout_s=constants.DEFAULT_TOTAL_TIMEOUT_S, decode=True): + """Send an ADB command to the device, yielding each line of output. + + Parameters + ---------- + service : bytes + The ADB service to talk to (e.g., ``b'shell'``) + command : bytes + The command that will be sent + timeout_s : float, None + Timeout in seconds for sending and receiving packets, or ``None``; see :meth:`BaseHandleAsync.bulk_read() ` + and :meth:`BaseHandleAsync.bulk_write() ` + total_timeout_s : float + The total time in seconds to wait for a ``b'CLSE'`` or ``b'OKAY'`` command in :meth:`AdbDeviceAsync._read` + decode : bool + Whether to decode the output to utf8 before returning + + Yields + ------- + bytes, str + The line-by-line output of the ADB command as a string if ``decode`` is True, otherwise as bytes. + + """ + adb_info = _AdbTransactionInfo(None, None, timeout_s, total_timeout_s) + stream = self._streaming_command(service, command, adb_info) + if decode: + async for line in (stream_line.decode('utf8') async for stream_line in stream): + yield line + else: + async for line in stream: + yield line + + async def root(self, timeout_s=None, total_timeout_s=constants.DEFAULT_TOTAL_TIMEOUT_S): + """Gain root access. + + The device must be rooted in order for this to work. + + Parameters + ---------- + timeout_s : float, None + Timeout in seconds for sending and receiving packets, or ``None``; see :meth:`BaseHandleAsync.bulk_read() ` + and :meth:`BaseHandleAsync.bulk_write() ` + total_timeout_s : float + The total time in seconds to wait for a ``b'CLSE'`` or ``b'OKAY'`` command in :meth:`AdbDeviceAsync._read` + + """ + if not self.available: + raise exceptions.AdbConnectionError("ADB command not sent because a connection to the device has not been established. (Did you call `AdbDeviceAsync.connect()`?)") + + await self._service(b'root', b'', timeout_s, total_timeout_s, False) + + async def shell(self, command, timeout_s=None, total_timeout_s=constants.DEFAULT_TOTAL_TIMEOUT_S, decode=True): + """Send an ADB shell command to the device. + + Parameters + ---------- + command : str + The shell command that will be sent + timeout_s : float, None + Timeout in seconds for sending and receiving packets, or ``None``; see :meth:`BaseHandleAsync.bulk_read() ` + and :meth:`BaseHandleAsync.bulk_write() ` + total_timeout_s : float + The total time in seconds to wait for a ``b'CLSE'`` or ``b'OKAY'`` command in :meth:`AdbDeviceAsync._read` + decode : bool + Whether to decode the output to utf8 before returning + + Returns + ------- + bytes, str + The output of the ADB shell command as a string if ``decode`` is True, otherwise as bytes. + + """ + if not self.available: + raise exceptions.AdbConnectionError("ADB command not sent because a connection to the device has not been established. (Did you call `AdbDeviceAsync.connect()`?)") + + return await self._service(b'shell', command.encode('utf8'), timeout_s, total_timeout_s, decode) + + async def streaming_shell(self, command, timeout_s=None, total_timeout_s=constants.DEFAULT_TOTAL_TIMEOUT_S, decode=True): + """Send an ADB shell command to the device, yielding each line of output. + + Parameters + ---------- + command : str + The shell command that will be sent + timeout_s : float, None + Timeout in seconds for sending and receiving packets, or ``None``; see :meth:`BaseHandleAsync.bulk_read() ` + and :meth:`BaseHandleAsync.bulk_write() ` + total_timeout_s : float + The total time in seconds to wait for a ``b'CLSE'`` or ``b'OKAY'`` command in :meth:`AdbDeviceAsync._read` + decode : bool + Whether to decode the output to utf8 before returning + + Yields + ------- + bytes, str + The line-by-line output of the ADB shell command as a string if ``decode`` is True, otherwise as bytes. + + """ + if not self.available: + raise exceptions.AdbConnectionError("ADB command not sent because a connection to the device has not been established. (Did you call `AdbDeviceAsync.connect()`?)") + + async for line in self._streaming_service(b'shell', command.encode('utf8'), timeout_s, total_timeout_s, decode): + yield line + + # ======================================================================= # + # # + # FileSync # + # # + # ======================================================================= # + async def list(self, device_path, timeout_s=None, total_timeout_s=constants.DEFAULT_TOTAL_TIMEOUT_S): + """Return a directory listing of the given path. + + Parameters + ---------- + device_path : str + Directory to list. + timeout_s : float, None + Expected timeout for any part of the pull. + total_timeout_s : float + The total time in seconds to wait for a ``b'CLSE'`` or ``b'OKAY'`` command in :meth:`AdbDeviceAsync._read` + + Returns + ------- + files : list[DeviceFile] + Filename, mode, size, and mtime info for the files in the directory + + """ + if not self.available: + raise exceptions.AdbConnectionError("ADB command not sent because a connection to the device has not been established. (Did you call `AdbDeviceAsync.connect()`?)") + + adb_info = _AdbTransactionInfo(None, None, timeout_s, total_timeout_s) + filesync_info = _FileSyncTransactionInfo(constants.FILESYNC_LIST_FORMAT) + await self._open(b'sync:', adb_info) + + await self._filesync_send(constants.LIST, adb_info, filesync_info, data=device_path) + files = [] + + async for cmd_id, header, filename in self._filesync_read_until([constants.DENT], [constants.DONE], adb_info, filesync_info): + if cmd_id == constants.DONE: + break + + mode, size, mtime = header + files.append(DeviceFile(filename, mode, size, mtime)) + + await self._close(adb_info) + + return files + + async def pull(self, device_filename, dest_file=None, progress_callback=None, timeout_s=None, total_timeout_s=constants.DEFAULT_TOTAL_TIMEOUT_S): + """Pull a file from the device. + + Parameters + ---------- + device_filename : str + Filename on the device to pull. + dest_file : str, file, io.IOBase, None + If set, a filename or writable file-like object. + progress_callback : function, None + Callback method that accepts filename, bytes_written and total_bytes, total_bytes will be -1 for file-like + objects + timeout_s : float, None + Expected timeout for any part of the pull. + total_timeout_s : float + The total time in seconds to wait for a ``b'CLSE'`` or ``b'OKAY'`` command in :meth:`AdbDeviceAsync._read` + + Returns + ------- + bytes, bool + The file data if ``dest_file`` is not set. Otherwise, ``True`` if the destination file exists + + Raises + ------ + ValueError + If ``dest_file`` is of unknown type. + + """ + if not self.available: + raise exceptions.AdbConnectionError("ADB command not sent because a connection to the device has not been established. (Did you call `AdbDeviceAsync.connect()`?)") + + if not dest_file: + dest_file = io.BytesIO() + + if not isinstance(dest_file, FILE_TYPES + (str,)): + raise ValueError("dest_file is of unknown type") + + adb_info = _AdbTransactionInfo(None, None, timeout_s, total_timeout_s) + filesync_info = _FileSyncTransactionInfo(constants.FILESYNC_PULL_FORMAT) + + with _open(dest_file, 'wb') as dest: + await self._open(b'sync:', adb_info) + await self._pull(device_filename, dest, progress_callback, adb_info, filesync_info) + await self._close(adb_info) + + if isinstance(dest, io.BytesIO): + return dest.getvalue() + + if hasattr(dest, 'name'): + return os.path.exists(dest.name) + + # We don't know what the path is, so we just assume it exists. + return True + + async def _pull(self, filename, dest, progress_callback, adb_info, filesync_info): + """Pull a file from the device into the file-like ``dest_file``. + + Parameters + ---------- + filename : str + The file to be pulled + dest : _io.BytesIO + File-like object for writing to + progress_callback : function, None + Callback method that accepts ``filename``, ``bytes_written``, and ``total_bytes`` + adb_info : _AdbTransactionInfo + Info and settings for this ADB transaction + filesync_info : _FileSyncTransactionInfo + Data and storage for this FileSync transaction + + """ + if progress_callback: + total_bytes = await self.stat(filename)[1] + progress = self._handle_progress(lambda current: progress_callback(filename, current, total_bytes)) + next(progress) + + await self._filesync_send(constants.RECV, adb_info, filesync_info, data=filename) + async for cmd_id, _, data in self._filesync_read_until([constants.DATA], [constants.DONE], adb_info, filesync_info): + if cmd_id == constants.DONE: + break + + dest.write(data) + if progress_callback: + progress.send(len(data)) + + async def push(self, source_file, device_filename, st_mode=constants.DEFAULT_PUSH_MODE, mtime=0, progress_callback=None, timeout_s=None, total_timeout_s=constants.DEFAULT_TOTAL_TIMEOUT_S): + """Push a file or directory to the device. + + Parameters + ---------- + source_file : str + Either a filename, a directory or file-like object to push to the device. + device_filename : str + Destination on the device to write to. + st_mode : int + Stat mode for filename + mtime : int + Modification time to set on the file. + progress_callback : function, None + Callback method that accepts filename, bytes_written and total_bytes, total_bytes will be -1 for file-like + objects + timeout_s : float, None + Expected timeout for any part of the push. + total_timeout_s : float + The total time in seconds to wait for a ``b'CLSE'`` or ``b'OKAY'`` command in :meth:`AdbDeviceAsync._read` + + """ + if not self.available: + raise exceptions.AdbConnectionError("ADB command not sent because a connection to the device has not been established. (Did you call `AdbDeviceAsync.connect()`?)") + + if isinstance(source_file, str): + if os.path.isdir(source_file): + await self.shell("mkdir " + device_filename, timeout_s, total_timeout_s) + for f in os.listdir(source_file): + await self.push(os.path.join(source_file, f), device_filename + '/' + f, progress_callback=progress_callback) + return + + adb_info = _AdbTransactionInfo(None, None, timeout_s, total_timeout_s) + filesync_info = _FileSyncTransactionInfo(constants.FILESYNC_PUSH_FORMAT) + + with _open(source_file, 'rb') as source: + await self._open(b'sync:', adb_info) + await self._push(source, device_filename, st_mode, mtime, progress_callback, adb_info, filesync_info) + + await self._close(adb_info) + + async def _push(self, datafile, filename, st_mode, mtime, progress_callback, adb_info, filesync_info): + """Push a file-like object to the device. + + Parameters + ---------- + 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`` + adb_info : _AdbTransactionInfo + Info and settings for this ADB transaction + + Raises + ------ + PushFailedError + Raised on push failure. + + """ + fileinfo = ('{},{}'.format(filename, int(st_mode))).encode('utf-8') + + await self._filesync_send(constants.SEND, adb_info, filesync_info, data=fileinfo) + + if progress_callback: + total_bytes = os.fstat(datafile.fileno()).st_size if isinstance(datafile, FILE_TYPES) else -1 + progress = self._handle_progress(lambda current: progress_callback(filename, current, total_bytes)) + next(progress) + + while True: + data = datafile.read(constants.MAX_PUSH_DATA) + if data: + await self._filesync_send(constants.DATA, adb_info, filesync_info, 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. + await self._filesync_send(constants.DONE, adb_info, filesync_info, size=mtime) + async for cmd_id, _, data in self._filesync_read_until([], [constants.OKAY, constants.FAIL], adb_info, filesync_info): + if cmd_id == constants.OKAY: + return + + raise exceptions.PushFailedError(data) + + async def stat(self, device_filename, timeout_s=None, total_timeout_s=constants.DEFAULT_TOTAL_TIMEOUT_S): + """Get a file's ``stat()`` information. + + Parameters + ---------- + device_filename : str + The file on the device for which we will get information. + timeout_s : float, None + Expected timeout for any part of the pull. + total_timeout_s : float + The total time in seconds to wait for a ``b'CLSE'`` or ``b'OKAY'`` command in :meth:`AdbDeviceAsync._read` + + Returns + ------- + mode : int + The octal permissions for the file + size : int + The size of the file + mtime : int + The last modified time for the file + + """ + if not self.available: + raise exceptions.AdbConnectionError("ADB command not sent because a connection to the device has not been established. (Did you call `AdbDeviceAsync.connect()`?)") + + adb_info = _AdbTransactionInfo(None, None, timeout_s, total_timeout_s) + await self._open(b'sync:', adb_info) + + filesync_info = _FileSyncTransactionInfo(constants.FILESYNC_STAT_FORMAT) + await self._filesync_send(constants.STAT, adb_info, filesync_info, data=device_filename) + _, (mode, size, mtime), _ = await self._filesync_read([constants.STAT], adb_info, filesync_info, read_data=False) + await self._close(adb_info) + + return mode, size, mtime + + # ======================================================================= # + # # + # Hidden Methods # + # # + # ======================================================================= # + async def _close(self, adb_info): + """Send a ``b'CLSE'`` message. + + .. warning:: + + This is not to be confused with the :meth:`AdbDeviceAsync.close` method! + + + Parameters + ---------- + adb_info : _AdbTransactionInfo + Info and settings for this ADB transaction + + """ + msg = AdbMessage(constants.CLSE, adb_info.local_id, adb_info.remote_id) + await self._send(msg, adb_info) + await self._read_until([constants.CLSE], adb_info) + + async def _okay(self, adb_info): + """Send an ``b'OKAY'`` mesage. + + Parameters + ---------- + adb_info : _AdbTransactionInfo + Info and settings for this ADB transaction + + """ + msg = AdbMessage(constants.OKAY, adb_info.local_id, adb_info.remote_id) + await self._send(msg, adb_info) + + async def _open(self, destination, adb_info): + """Opens a new connection to the device via an ``b'OPEN'`` message. + + 1. :meth:`~AdbDeviceAsync._send` an ``b'OPEN'`` command to the device that specifies the ``local_id`` + 2. :meth:`~AdbDeviceAsync._read` a response from the device that includes a command, another local ID (``their_local_id``), and ``remote_id`` + + * If ``local_id`` and ``their_local_id`` do not match, raise an exception. + * If the received command is ``b'CLSE'``, :meth:`~AdbDeviceAsync._read` another response from the device + * If the received command is not ``b'OKAY'``, raise an exception + * Set the ``adb_info.local_id`` and ``adb_info.remote_id`` attributes + + + Parameters + ---------- + destination : bytes + ``b'SERVICE:COMMAND'`` + adb_info : _AdbTransactionInfo + Info and settings for this ADB transaction + + Raises + ------ + adb_shell.exceptions.InvalidResponseError + Wrong local_id sent to us. + + """ + adb_info.local_id = 1 + msg = AdbMessage(constants.OPEN, adb_info.local_id, 0, destination + b'\0') + await self._send(msg, adb_info) + _, adb_info.remote_id, their_local_id, _ = await self._read([constants.OKAY], adb_info) + + if adb_info.local_id != their_local_id: + raise exceptions.InvalidResponseError('Expected the local_id to be {}, got {}'.format(adb_info.local_id, their_local_id)) + + async def _read(self, expected_cmds, adb_info): + """Receive a response from the device. + + 1. Read a message from the device and unpack the ``cmd``, ``arg0``, ``arg1``, ``data_length``, and ``data_checksum`` fields + 2. If ``cmd`` is not a recognized command in :const:`adb_shell.constants.WIRE_TO_ID`, raise an exception + 3. If the time has exceeded ``total_timeout_s``, raise an exception + 4. Read ``data_length`` bytes from the device + 5. If the checksum of the read data does not match ``data_checksum``, raise an exception + 6. Return ``command``, ``arg0``, ``arg1``, and ``bytes(data)`` + + + Parameters + ---------- + expected_cmds : list[bytes] + We will read packets until we encounter one whose "command" field is in ``expected_cmds`` + adb_info : _AdbTransactionInfo + Info and settings for this ADB transaction + + Returns + ------- + command : bytes + The received command, which is in :const:`adb_shell.constants.WIRE_TO_ID` and must be in ``expected_cmds`` + arg0 : int + TODO + arg1 : int + TODO + bytes + The data that was read + + Raises + ------ + adb_shell.exceptions.InvalidCommandError + Unknown command *or* never got one of the expected responses. + adb_shell.exceptions.InvalidChecksumError + Received checksum does not match the expected checksum. + + """ + start = time.time() + + while True: + msg = await self._handle.bulk_read(constants.MESSAGE_SIZE, adb_info.timeout_s) + _LOGGER.debug("bulk_read(%d): %s", constants.MESSAGE_SIZE, repr(msg)) + cmd, arg0, arg1, data_length, data_checksum = unpack(msg) + command = constants.WIRE_TO_ID.get(cmd) + + if not command: + raise exceptions.InvalidCommandError('Unknown command: %x' % cmd, cmd, (arg0, arg1)) + + if command in expected_cmds: + break + + if time.time() - start > adb_info.total_timeout_s: + raise exceptions.InvalidCommandError('Never got one of the expected responses (%s)' % expected_cmds, cmd, (adb_info.timeout_s, adb_info.total_timeout_s)) + + if data_length > 0: + data = bytearray() + while data_length > 0: + temp = await self._handle.bulk_read(data_length, adb_info.timeout_s) + _LOGGER.debug("bulk_read(%d): %s", data_length, repr(temp)) + + data += temp + data_length -= len(temp) + + actual_checksum = checksum(data) + if actual_checksum != data_checksum: + raise exceptions.InvalidChecksumError('Received checksum {0} != {1}'.format(actual_checksum, data_checksum)) + + else: + data = bytearray() + + return command, arg0, arg1, bytes(data) + + async def _read_until(self, expected_cmds, adb_info): + """Read a packet, acknowledging any write packets. + + 1. Read data via :meth:`AdbDeviceAsync._read` + 2. If a ``b'WRTE'`` packet is received, send an ``b'OKAY'`` packet via :meth:`AdbDeviceAsync._okay` + 3. Return the ``cmd`` and ``data`` that were read by :meth:`AdbDeviceAsync._read` + + + Parameters + ---------- + expected_cmds : list[bytes] + :meth:`AdbDeviceAsync._read` with look for a packet whose command is in ``expected_cmds`` + adb_info : _AdbTransactionInfo + Info and settings for this ADB transaction + + Returns + ------- + cmd : bytes + The command that was received by :meth:`AdbDeviceAsync._read`, which is in :const:`adb_shell.constants.WIRE_TO_ID` and must be in ``expected_cmds`` + data : bytes + The data that was received by :meth:`AdbDeviceAsync._read` + + Raises + ------ + adb_shell.exceptions.InterleavedDataError + We don't support multiple streams... + adb_shell.exceptions.InvalidResponseError + Incorrect remote id. + adb_shell.exceptions.InvalidCommandError + Never got one of the expected responses. + + """ + start = time.time() + + while True: + cmd, remote_id2, local_id2, data = await self._read(expected_cmds, adb_info) + + if local_id2 not in (0, adb_info.local_id): + raise exceptions.InterleavedDataError("We don't support multiple streams...") + + if remote_id2 in (0, adb_info.remote_id): + break + + if time.time() - start > adb_info.total_timeout_s: + raise exceptions.InvalidCommandError('Never got one of the expected responses (%s)' % expected_cmds, cmd, (adb_info.timeout_s, adb_info.total_timeout_s)) + + # Ignore CLSE responses to previous commands + # https://github.com/JeffLIrion/adb_shell/pull/14 + if cmd != constants.CLSE: + raise exceptions.InvalidResponseError('Incorrect remote id, expected {0} got {1}'.format(adb_info.remote_id, remote_id2)) + + # Acknowledge write packets + if cmd == constants.WRTE: + await self._okay(adb_info) + + return cmd, data + + async def _read_until_close(self, adb_info): + """Yield packets until a ``b'CLSE'`` packet is received. + + 1. Read the ``cmd`` and ``data`` fields from a ``b'CLSE'`` or ``b'WRTE'`` packet via :meth:`AdbDeviceAsync._read_until` + 2. If ``cmd`` is ``b'CLSE'``, then send a ``b'CLSE'`` message and stop + 3. If ``cmd`` is not ``b'WRTE'``, raise an exception + + * If ``cmd`` is ``b'FAIL'``, raise :class:`~adb_shell.exceptions.AdbCommandFailureException` + * Otherwise, raise :class:`~~adb_shell.exceptions.InvalidCommandError` + + 4. Yield ``data`` and repeat + + + Parameters + ---------- + adb_info : _AdbTransactionInfo + Info and settings for this ADB transaction + + Yields + ------ + data : bytes + The data that was read by :meth:`AdbDeviceAsync._read_until` + + """ + while True: + cmd, data = await self._read_until([constants.CLSE, constants.WRTE], adb_info) + + if cmd == constants.CLSE: + msg = AdbMessage(constants.CLSE, adb_info.local_id, adb_info.remote_id) + await self._send(msg, adb_info) + break + + yield data + + async def _send(self, msg, adb_info): + """Send a message to the device. + + 1. Send the message header (:meth:`adb_shell.adb_message.AdbMessage.pack `) + 2. Send the message data + + + Parameters + ---------- + msg : AdbMessage + The data that will be sent + adb_info : _AdbTransactionInfo + Info and settings for this ADB transaction + + """ + _LOGGER.debug("bulk_write: %s", repr(msg.pack())) + await self._handle.bulk_write(msg.pack(), adb_info.timeout_s) + _LOGGER.debug("bulk_write: %s", repr(msg.data)) + await self._handle.bulk_write(msg.data, adb_info.timeout_s) + + async def _streaming_command(self, service, command, adb_info): + """One complete set of USB packets for a single command. + + 1. :meth:`~AdbDeviceAsync._open` a new connection to the device, where the ``destination`` parameter is ``service:command`` + 2. Read the response data via :meth:`AdbDeviceAsync._read_until_close` + + + .. note:: + + All the data is held in memory, and thus large responses will be slow and can fill up memory. + + + Parameters + ---------- + service : bytes + The ADB service (e.g., ``b'shell'``, as used by :meth:`AdbDeviceAsync.shell`) + command : bytes + The service command + adb_info : _AdbTransactionInfo + Info and settings for this ADB transaction + + Yields + ------ + bytes + The responses from the service. + + """ + await self._open(b'%s:%s' % (service, command), adb_info) + + async for data in self._read_until_close(adb_info): + yield data + + async def _write(self, data, adb_info): + """Write a packet and expect an Ack. + + Parameters + ---------- + data : bytes + The data that will be sent + adb_info : _AdbTransactionInfo + Info and settings for this ADB transaction + + """ + msg = AdbMessage(constants.WRTE, adb_info.local_id, adb_info.remote_id, data) + await self._send(msg, adb_info) + + # Expect an ack in response. + await self._read_until([constants.OKAY], adb_info) + + # ======================================================================= # + # # + # FileSync Hidden Methods # + # # + # ======================================================================= # + async def _filesync_flush(self, adb_info, filesync_info): + """Write the data in the buffer up to ``filesync_info.send_idx``, then set ``filesync_info.send_idx`` to 0. + + Parameters + ---------- + adb_info : _AdbTransactionInfo + Info and settings for this ADB transaction + filesync_info : _FileSyncTransactionInfo + Data and storage for this FileSync transaction + + """ + await self._write(filesync_info.send_buffer[:filesync_info.send_idx], adb_info) + filesync_info.send_idx = 0 + + async def _filesync_read(self, expected_ids, adb_info, filesync_info, read_data=True): + """Read ADB messages and return FileSync packets. + + Parameters + ---------- + expected_ids : tuple[bytes] + If the received header ID is not in ``expected_ids``, an exception will be raised + adb_info : _AdbTransactionInfo + Info and settings for this ADB transaction + filesync_info : _FileSyncTransactionInfo + Data and storage for this FileSync transaction + read_data : bool + Whether to read the received data + + Returns + ------- + command_id : bytes + The received header ID + tuple + The contents of the header + data : bytearray, None + The received data, or ``None`` if ``read_data`` is False + + Raises + ------ + adb_shell.exceptions.AdbCommandFailureException + Command failed + adb_shell.exceptions.InvalidResponseError + Received response was not in ``expected_ids`` + + """ + if filesync_info.send_idx: + await self._filesync_flush(adb_info, filesync_info) + + # Read one filesync packet off the recv buffer. + header_data = await self._filesync_read_buffered(filesync_info.recv_message_size, adb_info, filesync_info) + header = struct.unpack(filesync_info.recv_message_format, header_data) + # Header is (ID, ...). + command_id = constants.FILESYNC_WIRE_TO_ID[header[0]] + + if command_id not in expected_ids: + if command_id == constants.FAIL: + reason = '' + if filesync_info.recv_buffer: + reason = filesync_info.recv_buffer.decode('utf-8', errors='ignore') + + raise exceptions.AdbCommandFailureException('Command failed: {}'.format(reason)) + + raise exceptions.InvalidResponseError('Expected one of %s, got %s' % (expected_ids, command_id)) + + if not read_data: + return command_id, header[1:], None + + # Header is (ID, ..., size). + size = header[-1] + data = await self._filesync_read_buffered(size, adb_info, filesync_info) + + return command_id, header[1:-1], data + + async def _filesync_read_buffered(self, size, adb_info, filesync_info): + """Read ``size`` bytes of data from ``self.recv_buffer``. + + Parameters + ---------- + size : int + The amount of data to read + adb_info : _AdbTransactionInfo + Info and settings for this ADB transaction + filesync_info : _FileSyncTransactionInfo + Data and storage for this FileSync transaction + + Returns + ------- + result : bytearray + The read data + + """ + # Ensure recv buffer has enough data. + while len(filesync_info.recv_buffer) < size: + _, data = await self._read_until([constants.WRTE], adb_info) + filesync_info.recv_buffer += data + + result = filesync_info.recv_buffer[:size] + filesync_info.recv_buffer = filesync_info.recv_buffer[size:] + return result + + async def _filesync_read_until(self, expected_ids, finish_ids, adb_info, filesync_info): + """Useful wrapper around :meth:`AdbDeviceAsync._filesync_read`. + + 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`` + adb_info : _AdbTransactionInfo + Info and settings for this ADB transaction + filesync_info : _FileSyncTransactionInfo + Data and storage for this FileSync transaction + + Yields + ------ + cmd_id : bytes + The received header ID + header : tuple + TODO + data : bytearray + The received data + + """ + while True: + cmd_id, header, data = await self._filesync_read(expected_ids + finish_ids, adb_info, filesync_info) + yield cmd_id, header, data + + # These lines are not reachable because whenever this method is called and `cmd_id` is in `finish_ids`, the code + # either breaks (`list` and `_pull`), returns (`_push`), or raises an exception (`_push`) + if cmd_id in finish_ids: # pragma: no cover + break + + async def _filesync_send(self, command_id, adb_info, filesync_info, 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. + + Parameters + ---------- + command_id : bytes + Command to send. + adb_info : _AdbTransactionInfo + Info and settings for this ADB transaction + filesync_info : _FileSyncTransactionInfo + Data and storage for this FileSync transaction + 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 filesync_info.can_add_to_send_buffer(len(data)): + await self._filesync_flush(adb_info, filesync_info) + + buf = struct.pack(b'<2I', constants.FILESYNC_ID_TO_WIRE[command_id], size) + data + filesync_info.send_buffer[filesync_info.send_idx:filesync_info.send_idx + len(buf)] = buf + filesync_info.send_idx += len(buf) + + @staticmethod + def _handle_progress(progress_callback): + """Calls the callback with the current progress and total bytes written/received. + + 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 + + +class AdbDeviceTcpAsync(AdbDeviceAsync): + """A class with methods for connecting to a device via TCP and executing ADB commands. + + Parameters + ---------- + host : str + The address of the device; may be an IP address or a host name + port : int + The device port to which we are connecting (default is 5555) + default_timeout_s : float, None + Default timeout in seconds for TCP packets, or ``None`` + banner : str, bytes, None + The hostname of the machine where the Python interpreter is currently running; if + it is not provided, it will be determined via ``socket.gethostname()`` + + Attributes + ---------- + _available : bool + Whether an ADB connection to the device has been established + _banner : bytearray, bytes + The hostname of the machine where the Python interpreter is currently running + _handle : TcpHandleAsync + The handle that is used to connect to the device + + """ + + def __init__(self, host, port=5555, default_timeout_s=None, banner=None): + handle = TcpHandleAsync(host, port, default_timeout_s) + super(AdbDeviceTcpAsync, self).__init__(handle, banner) diff --git a/adb_shell/handle/base_handle_async.py b/adb_shell/handle/base_handle_async.py new file mode 100644 index 0000000..8381155 --- /dev/null +++ b/adb_shell/handle/base_handle_async.py @@ -0,0 +1,76 @@ +# Copyright (c) 2020 Jeff Irion and contributors +# +# This file is part of the adb-shell package. + +"""A base class for handles used to communicate with a device. + +* :class:`BaseHandleAsync` + + * :meth:`BaseHandleAsync.bulk_read` + * :meth:`BaseHandleAsync.bulk_write` + * :meth:`BaseHandleAsync.close` + * :meth:`BaseHandleAsync.connect` + +""" + + +from abc import ABC, abstractmethod + + +class BaseHandleAsync(ABC): + """A base handle class. + + """ + + @abstractmethod + async def close(self): + """Close the connection. + + """ + + @abstractmethod + async def connect(self, timeout_s=None): + """Create a connection to the device. + + Parameters + ---------- + timeout_s : float, None + A connection timeout + + """ + + @abstractmethod + async def bulk_read(self, numbytes, timeout_s=None): + """Read data from the device. + + Parameters + ---------- + numbytes : int + The maximum amount of data to be received + timeout_s : float, None + A timeout for the read operation + + Returns + ------- + bytes + The received data + + """ + + @abstractmethod + async def bulk_write(self, data, timeout_s=None): + """Send data to the device. + + Parameters + ---------- + data : bytes + The data to be sent + timeout_s : float, None + A timeout for the write operation + + Returns + ------- + int + The number of bytes sent + + """ diff --git a/adb_shell/handle/tcp_handle_async.py b/adb_shell/handle/tcp_handle_async.py new file mode 100644 index 0000000..067f1b8 --- /dev/null +++ b/adb_shell/handle/tcp_handle_async.py @@ -0,0 +1,146 @@ +# Copyright (c) 2020 Jeff Irion and contributors +# +# This file is part of the adb-shell package. + +"""A class for creating a socket connection with the device and sending and receiving data. + +* :class:`TcpHandleAsync` + + * :meth:`TcpHandleAsync.bulk_read` + * :meth:`TcpHandleAsync.bulk_write` + * :meth:`TcpHandleAsync.close` + * :meth:`TcpHandleAsync.connect` + +""" + + +import asyncio + +from .base_handle_async import BaseHandleAsync +from ..exceptions import TcpTimeoutException + + +class TcpHandleAsync(BaseHandleAsync): + """TCP connection object. + + Parameters + ---------- + host : str + The address of the device; may be an IP address or a host name + port : int + The device port to which we are connecting (default is 5555) + default_timeout_s : float, None + Default timeout in seconds for TCP packets, or ``None`` + + Attributes + ---------- + _default_timeout_s : float, None + Default timeout in seconds for TCP packets, or ``None`` + _host : str + The address of the device; may be an IP address or a host name + _port : int + The device port to which we are connecting (default is 5555) + _reader : StreamReader, None + TODO + _writer : StreamWriter, None + TODO + + """ + def __init__(self, host, port=5555, default_timeout_s=None): + self._host = host + self._port = port + self._default_timeout_s = default_timeout_s + + self._reader = None + self._writer = None + + async def close(self): + """Close the socket connection. + + """ + if self._writer: + try: + self._writer.close() + await self._writer.wait_closed() + except OSError: + pass + + self._reader = None + self._writer = None + + async def connect(self, timeout_s=None): + """Create a socket connection to the device. + + Parameters + ---------- + timeout_s : float, None + Set the timeout on the socket instance + + """ + timeout = self._default_timeout_s if timeout_s is None else timeout_s + + try: + self._reader, self._writer = await asyncio.wait_for(asyncio.open_connection(self._host, self._port), timeout) + except asyncio.TimeoutError: + msg = 'Connecting to {}:{} timed out ({} seconds)'.format(self._host, self._port, timeout) + raise TcpTimeoutException(msg) + + async def bulk_read(self, numbytes, timeout_s=None): + """Receive data from the socket. + + Parameters + ---------- + numbytes : int + The maximum amount of data to be received + timeout_s : float, None + When the timeout argument is omitted, ``select.select`` blocks until at least one file descriptor is ready. A time-out value of zero specifies a poll and never blocks. + + Returns + ------- + bytes + The received data + + Raises + ------ + TcpTimeoutException + Reading timed out. + + """ + timeout = self._default_timeout_s if timeout_s is None else timeout_s + + try: + return await asyncio.wait_for(self._reader.read(numbytes), timeout) + except asyncio.TimeoutError: + msg = 'Reading from {}:{} timed out ({} seconds)'.format(self._host, self._port, timeout) + raise TcpTimeoutException(msg) + + async def bulk_write(self, data, timeout_s=None): + """Send data to the socket. + + Parameters + ---------- + data : bytes + The data to be sent + timeout_s : float, None + When the timeout argument is omitted, ``select.select`` blocks until at least one file descriptor is ready. A time-out value of zero specifies a poll and never blocks. + + Returns + ------- + int + The number of bytes sent + + Raises + ------ + TcpTimeoutException + Sending data timed out. No data was sent. + + """ + timeout = self._default_timeout_s if timeout_s is None else timeout_s + + try: + self._writer.write(data) + await asyncio.wait_for(self._writer.drain(), timeout) + return len(data) + except asyncio.TimeoutError: + msg = 'Sending data to {}:{} timed out after {} seconds. No data was sent.'.format(self._host, self._port, timeout) + raise TcpTimeoutException(msg) diff --git a/adb_shell/hidden_helpers.py b/adb_shell/hidden_helpers.py new file mode 100644 index 0000000..0a4b510 --- /dev/null +++ b/adb_shell/hidden_helpers.py @@ -0,0 +1,161 @@ +# Copyright (c) 2020 Jeff Irion and contributors +# +# This file is part of the adb-shell package. It incorporates work +# covered by the following license notice: +# +# +# 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. + +"""Implement helpers for the :class:`~adb_shell.adb_device.AdbDevice` and :class:`~adb_shell.adb_device_async.AdbDeviceAsync` classes. + +.. rubric:: Contents + +* :class:`_AdbTransactionInfo` +* :class:`_FileSyncTransactionInfo` + + * :meth:`_FileSyncTransactionInfo.can_add_to_send_buffer` + +* :func:`_open` + +""" + + +from collections import namedtuple +from contextlib import contextmanager +import io +import struct + +from . import constants + + +try: + FILE_TYPES = (file, io.IOBase) +except NameError: # pragma: no cover + FILE_TYPES = (io.IOBase,) + +DeviceFile = namedtuple('DeviceFile', ['filename', 'mode', 'size', 'mtime']) + + +@contextmanager +def _open(name, mode='r'): + """Handle opening and closing of files and IO streams. + + Parameters + ---------- + name : str, io.IOBase + The name of the file *or* an IO stream + mode : str + The mode for opening the file + + Yields + ------ + io.IOBase + The opened file *or* the IO stream + + """ + try: + opened = open(name, mode) if isinstance(name, str) else None + if isinstance(name, str): + yield opened + else: + yield name + finally: + if isinstance(name, str): + opened.close() + else: + name.close() + + +class _AdbTransactionInfo(object): # pylint: disable=too-few-public-methods + """A class for storing info and settings used during a single ADB "transaction." + + Parameters + ---------- + local_id : int + The ID for the sender (i.e., the device running this code) + remote_id : int + The ID for the recipient + timeout_s : float, None + Timeout in seconds for sending and receiving packets, or ``None``; see :meth:`BaseHandle.bulk_read() ` + and :meth:`BaseHandle.bulk_write() ` + total_timeout_s : float + The total time in seconds to wait for a command in ``expected_cmds`` in :meth:`AdbDevice._read` + + Attributes + ---------- + local_id : int + The ID for the sender (i.e., the device running this code) + remote_id : int + The ID for the recipient + timeout_s : float, None + Timeout in seconds for sending and receiving packets, or ``None``; see :meth:`BaseHandle.bulk_read() ` + and :meth:`BaseHandle.bulk_write() ` + total_timeout_s : float + The total time in seconds to wait for a command in ``expected_cmds`` in :meth:`AdbDevice._read` + + """ + def __init__(self, local_id, remote_id, timeout_s=None, total_timeout_s=constants.DEFAULT_TOTAL_TIMEOUT_S): + self.local_id = local_id + self.remote_id = remote_id + self.timeout_s = timeout_s + self.total_timeout_s = total_timeout_s + + +class _FileSyncTransactionInfo(object): # pylint: disable=too-few-public-methods + """A class for storing info used during a single FileSync "transaction." + + Parameters + ---------- + recv_message_format : bytes + The FileSync message format + + Attributes + ---------- + recv_buffer : bytearray + A buffer for storing received data + recv_message_format : bytes + The FileSync message format + recv_message_size : int + The FileSync message size + send_buffer : bytearray + A buffer for storing data to be sent + send_idx : int + The index in ``recv_buffer`` that will be the start of the next data packet sent + + """ + def __init__(self, recv_message_format): + self.send_buffer = bytearray(constants.MAX_ADB_DATA) + self.send_idx = 0 + + self.recv_buffer = bytearray() + self.recv_message_format = recv_message_format + self.recv_message_size = struct.calcsize(recv_message_format) + + def can_add_to_send_buffer(self, data_len): + """Determine whether ``data_len`` bytes of data can be added to the send buffer without exceeding :const:`constants.MAX_ADB_DATA`. + + 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:`constants.MAX_ADB_DATA` + + """ + added_len = self.recv_message_size + data_len + return self.send_idx + added_len < constants.MAX_ADB_DATA diff --git a/docs/source/adb_shell.adb_device_async.rst b/docs/source/adb_shell.adb_device_async.rst new file mode 100644 index 0000000..e4886ad --- /dev/null +++ b/docs/source/adb_shell.adb_device_async.rst @@ -0,0 +1,7 @@ +adb\_shell.adb\_device\_async module +==================================== + +.. automodule:: adb_shell.adb_device_async + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/adb_shell.handle.base_handle_async.rst b/docs/source/adb_shell.handle.base_handle_async.rst new file mode 100644 index 0000000..65d8aef --- /dev/null +++ b/docs/source/adb_shell.handle.base_handle_async.rst @@ -0,0 +1,7 @@ +adb\_shell.handle.base\_handle\_async module +============================================ + +.. automodule:: adb_shell.handle.base_handle_async + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/adb_shell.handle.rst b/docs/source/adb_shell.handle.rst index 798a163..ff64626 100644 --- a/docs/source/adb_shell.handle.rst +++ b/docs/source/adb_shell.handle.rst @@ -7,7 +7,9 @@ Submodules .. toctree:: adb_shell.handle.base_handle + adb_shell.handle.base_handle_async adb_shell.handle.tcp_handle + adb_shell.handle.tcp_handle_async adb_shell.handle.usb_handle Module contents diff --git a/docs/source/adb_shell.handle.tcp_handle_async.rst b/docs/source/adb_shell.handle.tcp_handle_async.rst new file mode 100644 index 0000000..bd0817e --- /dev/null +++ b/docs/source/adb_shell.handle.tcp_handle_async.rst @@ -0,0 +1,7 @@ +adb\_shell.handle.tcp\_handle\_async module +=========================================== + +.. automodule:: adb_shell.handle.tcp_handle_async + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/adb_shell.hidden_helpers.rst b/docs/source/adb_shell.hidden_helpers.rst new file mode 100644 index 0000000..9163585 --- /dev/null +++ b/docs/source/adb_shell.hidden_helpers.rst @@ -0,0 +1,7 @@ +adb\_shell.hidden\_helpers module +================================= + +.. automodule:: adb_shell.hidden_helpers + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/adb_shell.rst b/docs/source/adb_shell.rst index 5a9edbf..4aeab58 100644 --- a/docs/source/adb_shell.rst +++ b/docs/source/adb_shell.rst @@ -15,9 +15,11 @@ Submodules .. toctree:: adb_shell.adb_device + adb_shell.adb_device_async adb_shell.adb_message adb_shell.constants adb_shell.exceptions + adb_shell.hidden_helpers Module contents --------------- diff --git a/tests/async_patchers.py b/tests/async_patchers.py new file mode 100644 index 0000000..3e72d9f --- /dev/null +++ b/tests/async_patchers.py @@ -0,0 +1,62 @@ +from unittest.mock import patch + +from adb_shell import constants +from adb_shell.adb_message import AdbMessage, unpack +from adb_shell.handle.tcp_handle_async import TcpHandleAsync + +try: + from unittest.mock import AsyncMock +except ImportError: + from unittest.mock import MagicMock + + class AsyncMock(MagicMock): + async def __call__(self, *args, **kwargs): + return super(AsyncMock, self).__call__(*args, **kwargs) + + +class FakeStreamWriter: + def close(self): + pass + + async def wait_closed(self): + pass + + def write(self, data): + pass + + async def drain(self): + pass + + +class FakeStreamReader: + async def read(self, numbytes): + return b'TEST' + + +class FakeTcpHandleAsync(TcpHandleAsync): + def __init__(self, *args, **kwargs): + TcpHandleAsync.__init__(self, *args, **kwargs) + self._bulk_read = b'' + self._bulk_write = b'' + + async def close(self): + self._reader = None + self._writer = None + + async def connect(self, auth_timeout_s=None): + self._reader = True + self._writer = True + + async def bulk_read(self, numbytes, timeout_s=None): + num = min(numbytes, constants.MAX_ADB_DATA) + ret = self._bulk_read[:num] + self._bulk_read = self._bulk_read[num:] + return ret + + async def bulk_write(self, data, timeout_s=None): + self._bulk_write += data + return len(data) + + +# `TcpHandle` patches +PATCH_TCP_HANDLE_ASYNC = patch('adb_shell.adb_device_async.TcpHandleAsync', FakeTcpHandleAsync) diff --git a/tests/async_wrapper.py b/tests/async_wrapper.py new file mode 100644 index 0000000..049feca --- /dev/null +++ b/tests/async_wrapper.py @@ -0,0 +1,12 @@ +import asyncio + + +def _await(coro): + return asyncio.get_event_loop().run_until_complete(coro) + + +def awaiter(func): + def sync_func(*args, **kwargs): + return _await(func(*args, **kwargs)) + + return sync_func diff --git a/tests/patchers.py b/tests/patchers.py index d06f033..2923794 100644 --- a/tests/patchers.py +++ b/tests/patchers.py @@ -1,10 +1,14 @@ from mock import patch +import sys +import unittest from adb_shell import constants from adb_shell.adb_message import AdbMessage from adb_shell.handle.tcp_handle import TcpHandle +ASYNC_SKIPPER=unittest.skipIf(sys.version_info.major < 3 or sys.version_info.minor < 6, "Async functionality requires Python 3.6+") + MSG_CONNECT = AdbMessage(command=constants.CNXN, arg0=0, arg1=0, data=b'host::unknown\0') MSG_CONNECT_WITH_AUTH_INVALID = AdbMessage(command=constants.AUTH, arg0=0, arg1=0, data=b'host::unknown\0') MSG_CONNECT_WITH_AUTH1 = AdbMessage(command=constants.AUTH, arg0=constants.AUTH_TOKEN, arg1=0, data=b'host::unknown\0') diff --git a/tests/test_adb_device.py b/tests/test_adb_device.py index 7baf7d5..348f75a 100644 --- a/tests/test_adb_device.py +++ b/tests/test_adb_device.py @@ -549,7 +549,7 @@ def test_push_file(self): AdbMessage(command=constants.OKAY, arg0=1, arg1=1), AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) - with patch('adb_shell.adb_device.open', mock_open(read_data=filedata)): + with patch('adb_shell.hidden_helpers.open', mock_open(read_data=filedata)): self.device.push('TEST_FILE', '/data', mtime=mtime) self.assertEqual(expected_bulk_write, self.device._handle._bulk_write) @@ -565,7 +565,7 @@ def test_push_fail(self): AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b''), AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=join_messages(FileSyncMessage(constants.FAIL, data=b'')))) - with self.assertRaises(exceptions.PushFailedError), patch('adb_shell.adb_device.open', mock_open(read_data=filedata)): + with self.assertRaises(exceptions.PushFailedError), patch('adb_shell.hidden_helpers.open', mock_open(read_data=filedata)): self.device.push('TEST_FILE', '/data', mtime=mtime) def test_push_big_file(self): @@ -619,7 +619,7 @@ def test_push_dir(self): # Expected `bulk_write` values #TODO - with patch('adb_shell.adb_device.open', mock_open(read_data=filedata)), patch('os.path.isdir', lambda x: x == 'TEST_DIR/'), patch('os.listdir', return_value=['TEST_FILE1', 'TEST_FILE2']): + with patch('adb_shell.hidden_helpers.open', mock_open(read_data=filedata)), patch('os.path.isdir', lambda x: x == 'TEST_DIR/'), patch('os.listdir', return_value=['TEST_FILE1', 'TEST_FILE2']): self.device.push('TEST_DIR/', '/data', mtime=mtime) def test_pull(self): @@ -663,7 +663,7 @@ def test_pull_file(self): AdbMessage(command=constants.OKAY, arg0=1, arg1=1), AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) - with patch('adb_shell.adb_device.open', mock_open()), patch('os.path.exists', return_value=True): + with patch('adb_shell.hidden_helpers.open', mock_open()), patch('os.path.exists', return_value=True): self.assertTrue(self.device.pull('/data', 'TEST_FILE')) self.assertEqual(expected_bulk_write, self.device._handle._bulk_write) @@ -686,7 +686,7 @@ def test_pull_file_return_true(self): AdbMessage(command=constants.OKAY, arg0=1, arg1=1), AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) - with patch('adb_shell.adb_device.open', mock_open()), patch('adb_shell.adb_device.hasattr', return_value=False): + with patch('adb_shell.hidden_helpers.open', mock_open()), patch('adb_shell.adb_device.hasattr', return_value=False): self.assertTrue(self.device.pull('/data', 'TEST_FILE')) self.assertEqual(expected_bulk_write, self.device._handle._bulk_write) @@ -710,7 +710,7 @@ def test_pull_big_file(self): AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b''), AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) - with patch('adb_shell.adb_device.open', mock_open()), patch('os.path.exists', return_value=True): + with patch('adb_shell.hidden_helpers.open', mock_open()), patch('os.path.exists', return_value=True): self.assertTrue(self.device.pull('/data', 'TEST_FILE')) self.assertEqual(expected_bulk_write, self.device._handle._bulk_write) diff --git a/tests/test_adb_device_async.py b/tests/test_adb_device_async.py new file mode 100644 index 0000000..01c99fe --- /dev/null +++ b/tests/test_adb_device_async.py @@ -0,0 +1,829 @@ +import asyncio +import logging +from io import BytesIO +import sys +import unittest +from unittest.mock import mock_open, patch + +from adb_shell import constants, exceptions +from adb_shell.adb_device_async import AdbDeviceAsync, AdbDeviceTcpAsync, DeviceFile +from adb_shell.adb_message import AdbMessage +from adb_shell.auth.keygen import keygen +from adb_shell.auth.sign_pythonrsa import PythonRSASigner + +from . import patchers +from .async_patchers import PATCH_TCP_HANDLE_ASYNC, AsyncMock, FakeTcpHandleAsync +from .async_wrapper import awaiter +from .filesync_helpers import FileSyncMessage, FileSyncListMessage, FileSyncStatMessage +from .keygen_stub import open_priv_pub + + +# https://stackoverflow.com/a/7483862 +_LOGGER = logging.getLogger('adb_shell.adb_device_async') +_LOGGER.setLevel(logging.DEBUG) +_LOGGER.addHandler(logging.StreamHandler(sys.stdout)) + + +def to_int(cmd): + return sum(c << (i * 8) for i, c in enumerate(bytearray(cmd))) + +def join_messages(*messages): + return b''.join([message.pack() + message.data for message in messages]) + + +class AdbMessageForTesting(AdbMessage): + def __init__(self, command, arg0=None, arg1=None, data=b''): + self.command = to_int(command) + self.magic = self.command ^ 0xFFFFFFFF + self.arg0 = arg0 + self.arg1 = arg1 + self.data = data + + +@patchers.ASYNC_SKIPPER +class TestAdbDeviceAsync(unittest.TestCase): + def setUp(self): + self.device = AdbDeviceAsync(handle=FakeTcpHandleAsync('host', 5555)) + self.device._handle._bulk_read = b''.join(patchers.BULK_READ_LIST) + + def tearDown(self): + self.assertFalse(self.device._handle._bulk_read) + + @awaiter + async def test_adb_connection_error(self): + with self.assertRaises(exceptions.AdbConnectionError): + await self.device.shell('FAIL') + + with self.assertRaises(exceptions.AdbConnectionError): + async_generator = self.device.streaming_shell('FAIL') + await async_generator.__anext__() + + with self.assertRaises(exceptions.AdbConnectionError): + await self.device.root() + + with self.assertRaises(exceptions.AdbConnectionError): + await self.device.list('FAIL') + + with self.assertRaises(exceptions.AdbConnectionError): + await self.device.push('FAIL', 'FAIL') + + with self.assertRaises(exceptions.AdbConnectionError): + await self.device.pull('FAIL', 'FAIL') + + with self.assertRaises(exceptions.AdbConnectionError): + await self.device.stat('FAIL') + + self.device._handle._bulk_read = b'' + + @awaiter + async def test_init_tcp(self): + with PATCH_TCP_HANDLE_ASYNC: + tcp_device = AdbDeviceTcpAsync('host') + tcp_device._handle._bulk_read = self.device._handle._bulk_read + + # Make sure that the `connect()` method works + self.assertTrue(await tcp_device.connect()) + self.assertTrue(tcp_device.available) + + # Clear the `_bulk_read` buffer so that `self.tearDown()` passes + self.device._handle._bulk_read = b'' + + + @awaiter + async def test_init_banner(self): + device_with_banner = AdbDeviceAsync(handle=FakeTcpHandleAsync('host', 5555), banner='banner') + self.assertEqual(device_with_banner._banner, b'banner') + + device_with_banner2 = AdbDeviceAsync(handle=FakeTcpHandleAsync('host', 5555), banner=bytearray('banner2', 'utf-8')) + self.assertEqual(device_with_banner2._banner, b'banner2') + + device_with_banner3 = AdbDeviceAsync(handle=FakeTcpHandleAsync('host', 5555), banner=u'banner3') + self.assertEqual(device_with_banner3._banner, b'banner3') + + with patch('socket.gethostname', side_effect=Exception): + device_banner_unknown = AdbDeviceAsync(handle=FakeTcpHandleAsync('host', 5555)) + self.assertEqual(device_banner_unknown._banner, b'unknown') + + # Clear the `_bulk_read` buffer so that `self.tearDown()` passes + self.device._handle._bulk_read = b'' + + @awaiter + async def test_init_invalid_handle(self): + with self.assertRaises(exceptions.InvalidHandleError): + device = AdbDeviceAsync(handle=123) + + # Clear the `_bulk_read` buffer so that `self.tearDown()` passes + self.device._handle._bulk_read = b'' + + @awaiter + async def test_available(self): + self.assertFalse(self.device.available) + + # Clear the `_bulk_read` buffer so that `self.tearDown()` passes + self.device._handle._bulk_read = b'' + + @awaiter + async def test_close(self): + self.assertFalse(await self.device.close()) + self.assertFalse(self.device.available) + + # Clear the `_bulk_read` buffer so that `self.tearDown()` passes + self.device._handle._bulk_read = b'' + + # ======================================================================= # + # # + # `connect` tests # + # # + # ======================================================================= # + @awaiter + async def test_connect(self): + self.assertTrue(await self.device.connect()) + self.assertTrue(self.device.available) + + @awaiter + async def test_connect_no_keys(self): + self.device._handle._bulk_read = b''.join(patchers.BULK_READ_LIST_WITH_AUTH[:2]) + with self.assertRaises(exceptions.DeviceAuthError): + await self.device.connect() + + self.assertFalse(self.device.available) + + @awaiter + async def test_connect_with_key_invalid_response(self): + with patch('adb_shell.auth.sign_pythonrsa.open', open_priv_pub), patch('adb_shell.auth.keygen.open', open_priv_pub): + keygen('tests/adbkey') + signer = PythonRSASigner.FromRSAKeyPath('tests/adbkey') + + self.device._handle._bulk_read = b''.join(patchers.BULK_READ_LIST_WITH_AUTH_INVALID) + + with self.assertRaises(exceptions.InvalidResponseError): + await self.device.connect([signer]) + + self.assertFalse(self.device.available) + + @awaiter + async def test_connect_with_key(self): + with patch('adb_shell.auth.sign_pythonrsa.open', open_priv_pub), patch('adb_shell.auth.keygen.open', open_priv_pub): + keygen('tests/adbkey') + signer = PythonRSASigner.FromRSAKeyPath('tests/adbkey') + + self.device._handle._bulk_read = b''.join(patchers.BULK_READ_LIST_WITH_AUTH) + + self.assertTrue(await self.device.connect([signer])) + + @awaiter + async def test_connect_with_new_key(self): + with patch('adb_shell.auth.sign_pythonrsa.open', open_priv_pub), patch('adb_shell.auth.keygen.open', open_priv_pub): + keygen('tests/adbkey') + signer = PythonRSASigner.FromRSAKeyPath('tests/adbkey') + signer.pub_key = u'' + + self.device._handle._bulk_read = b''.join(patchers.BULK_READ_LIST_WITH_AUTH_NEW_KEY) + + self.assertTrue(await self.device.connect([signer])) + + @awaiter + async def test_connect_with_new_key_and_callback(self): + with patch('adb_shell.auth.sign_pythonrsa.open', open_priv_pub), patch('adb_shell.auth.keygen.open', open_priv_pub): + keygen('tests/adbkey') + signer = PythonRSASigner.FromRSAKeyPath('tests/adbkey') + signer.pub_key = u'' + + self._callback_invoked = False + def auth_callback(device): + self._callback_invoked = True + + self.device._handle._bulk_read = b''.join(patchers.BULK_READ_LIST_WITH_AUTH_NEW_KEY) + + self.assertTrue(await self.device.connect([signer], auth_callback=auth_callback)) + self.assertTrue(self._callback_invoked) + + + # ======================================================================= # + # # + # `shell` tests # + # # + # ======================================================================= # + @awaiter + async def test_shell_no_return(self): + self.assertTrue(await self.device.connect()) + + # Provide the `bulk_read` return values + self.device._handle._bulk_read = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), + AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) + + self.assertEqual(await self.device.shell('TEST'), '') + + @awaiter + async def test_shell_return_pass(self): + self.assertTrue(await self.device.connect()) + + # Provide the `bulk_read` return values + self.device._handle._bulk_read = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), + AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=b'PA'), + AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=b'SS'), + AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) + + self.assertEqual(await self.device.shell('TEST'), 'PASS') + + @awaiter + async def test_shell_dont_decode(self): + self.assertTrue(await self.device.connect()) + + # Provide the `bulk_read` return values + self.device._handle._bulk_read = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), + AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=b'PA'), + AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=b'SS'), + AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) + + self.assertEqual(await self.device.shell('TEST', decode=False), b'PASS') + + @awaiter + async def test_shell_data_length_exceeds_max(self): + self.assertTrue(await self.device.connect()) + + # Provide the `bulk_read` return values + self.device._handle._bulk_read = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), + AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=b'0'*(constants.MAX_ADB_DATA+1)), + AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) + + await self.device.shell('TEST') + self.assertTrue(True) + + @awaiter + async def test_shell_multibytes_sequence_exceeds_max(self): + self.assertTrue(await self.device.connect()) + + # Provide the `bulk_read` return values + self.device._handle._bulk_read = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), + AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=b'0'*(constants.MAX_ADB_DATA-1) + b'\xe3\x81\x82'), + AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) + + self.assertEqual(await self.device.shell('TEST'), u'0'*(constants.MAX_ADB_DATA-1) + u'\u3042') + + @awaiter + async def test_shell_with_multibytes_sequence_over_two_messages(self): + self.assertTrue(await self.device.connect()) + + # Provide the `bulk_read` return values + self.device._handle._bulk_read = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), + AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=b'\xe3'), + AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=b'\x81\x82'), + AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) + + self.assertEqual(await self.device.shell('TEST'), u'\u3042') + + @awaiter + async def test_shell_multiple_clse(self): + # https://github.com/JeffLIrion/adb_shell/issues/15#issuecomment-536795938 + self.assertTrue(await self.device.connect()) + + # Provide the `bulk_read` return values + msg1 = AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'') + msg2 = AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=b'PASS') + msg3 = AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'') + self.device._handle._bulk_read = b''.join([b'OKAY\xd9R\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb0\xb4\xbe\xa6', + b'WRTE\xd9R\x00\x00\x01\x00\x00\x00\x01\x00\x00\x002\x00\x00\x00\xa8\xad\xab\xba', + b'2', + b'WRTE\xd9R\x00\x00\x01\x00\x00\x00\x0c\x02\x00\x00\xc0\x92\x00\x00\xa8\xad\xab\xba', + b'Wake Locks: size=2\ncom.google.android.tvlauncher\n\n- STREAM_MUSIC:\n Muted: true\n Min: 0\n Max: 15\n Current: 2 (speaker): 15, 4 (headset): 10, 8 (headphone): 10, 80 (bt_a2dp): 10, 1000 (digital_dock): 10, 4000000 (usb_headset): 3, 40000000 (default): 15\n Devices: speaker\n- STREAM_ALARM:\n Muted: true\n Min: 1\n Max: 7\n Current: 2 (speaker): 7, 4 (headset): 5, 8 (headphone): 5, 80 (bt_a2dp): 5, 1000 (digital_dock): 5, 4000000 (usb_headset): 1, 40000000 (default): 7\n Devices: speaker\n- STREAM_NOTIFICATION:\n', + b'CLSE\xd9R\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbc\xb3\xac\xba', + msg1.pack(), + b'CLSE\xdaR\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbc\xb3\xac\xba', + msg2.pack(), + msg2.data, + msg3.pack()]) + + await self.device.shell("dumpsys power | grep 'Display Power' | grep -q 'state=ON' && echo -e '1\\c' && dumpsys power | grep mWakefulness | grep -q Awake && echo -e '1\\c' && dumpsys audio | grep paused | grep -qv 'Buffer Queue' && echo -e '1\\c' || (dumpsys audio | grep started | grep -qv 'Buffer Queue' && echo '2\\c' || echo '0\\c') && dumpsys power | grep Locks | grep 'size=' && CURRENT_APP=$(dumpsys window windows | grep mCurrentFocus) && CURRENT_APP=${CURRENT_APP#*{* * } && CURRENT_APP=${CURRENT_APP%%/*} && echo $CURRENT_APP && (dumpsys media_session | grep -A 100 'Sessions Stack' | grep -A 100 $CURRENT_APP | grep -m 1 'state=PlaybackState {' || echo) && dumpsys audio | grep '\\- STREAM_MUSIC:' -A 12") + self.assertEqual(await self.device.shell('TEST'), 'PASS') + + # ======================================================================= # + # # + # `shell` error tests # + # # + # ======================================================================= # + @awaiter + async def test_shell_error_local_id(self): + self.assertTrue(await self.device.connect()) + + # Provide the `bulk_read` return values + self.device._handle._bulk_read = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=1234, data=b'\x00')) + + with self.assertRaises(exceptions.InvalidResponseError): + await self.device.shell('TEST') + + @awaiter + async def test_shell_error_unknown_command(self): + self.assertTrue(await self.device.connect()) + + # Provide the `bulk_read` return values + self.device._handle._bulk_read = join_messages(AdbMessageForTesting(command=constants.FAIL, arg0=1, arg1=1, data=b'')) + + with self.assertRaises(exceptions.InvalidCommandError): + self.assertEqual(await self.device.shell('TEST'), '') + + @awaiter + async def test_shell_error_timeout(self): + self.assertTrue(await self.device.connect()) + + # Provide the `bulk_read` return values + self.device._handle._bulk_read = join_messages(AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=b'')) + + with self.assertRaises(exceptions.InvalidCommandError): + await self.device.shell('TEST', total_timeout_s=-1) + + @awaiter + async def test_shell_error_timeout_multiple_clse(self): + self.assertTrue(await self.device.connect()) + + # Provide the `bulk_read` return values + self.device._handle._bulk_read = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b''), + AdbMessage(command=constants.CLSE, arg0=2, arg1=1, data=b'')) + + with self.assertRaises(exceptions.InvalidCommandError): + await self.device.shell('TEST', total_timeout_s=-1) + + @awaiter + async def test_shell_error_checksum(self): + self.assertTrue(await self.device.connect()) + + # Provide the `bulk_read` return values + msg1 = AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00') + msg2 = AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=b'PASS') + self.device._handle._bulk_read = b''.join([msg1.pack(), msg1.data, msg2.pack(), msg2.data[:-1] + b'0']) + + with self.assertRaises(exceptions.InvalidChecksumError): + await self.device.shell('TEST') + + @awaiter + async def test_shell_error_local_id2(self): + self.assertTrue(await self.device.connect()) + + # Provide the `bulk_read` return values + self.device._handle._bulk_read = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), + AdbMessage(command=constants.WRTE, arg0=1, arg1=2, data=b'PASS')) + + with self.assertRaises(exceptions.InterleavedDataError): + await self.device.shell('TEST') + await self.device.shell('TEST') + + @awaiter + async def test_shell_error_remote_id2(self): + self.assertTrue(await self.device.connect()) + + # Provide the `bulk_read` return values + self.device._handle._bulk_read = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), + AdbMessage(command=constants.WRTE, arg0=2, arg1=1, data=b'PASS')) + + with self.assertRaises(exceptions.InvalidResponseError): + await self.device.shell('TEST') + + @awaiter + async def test_issue29(self): + # https://github.com/JeffLIrion/adb_shell/issues/29 + with patch('adb_shell.auth.sign_pythonrsa.open', open_priv_pub), patch('adb_shell.auth.keygen.open', open_priv_pub): + keygen('tests/adbkey') + signer = PythonRSASigner.FromRSAKeyPath('tests/adbkey') + + msg1 = AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00') + msg2 = AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'') + + self.device._handle._bulk_read = b''.join([b'AUTH\x01\x00\x00\x00\x00\x00\x00\x00\x14\x00\x00\x00\xc5\n\x00\x00\xbe\xaa\xab\xb7', # Line 22 + b"\x17\xbf\xbf\xff\xc7\xa2eo'Sh\xdf\x8e\xf5\xff\xe0\tJ6H", # Line 23 + b"CNXN\x00\x00\x00\x01\x00\x10\x00\x00i\x00\x00\x00.'\x00\x00\xbc\xb1\xa7\xb1", # Line 26 + b'device::ro.product.name=once;ro.product.model=MIBOX3;ro.product.device=once;features=stat_v2,cmd,shell_v2', # Line 27 + b'OKAY\x99\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb0\xb4\xbe\xa6', # Line 290 (modified --> Line 30) + b'CLSE\xa2\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbc\xb3\xac\xba', # Line 291 + b'CLSE\xa2\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbc\xb3\xac\xba', # Line 292 + b'WRTE\x99\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x001\x00\x00\x00\xa8\xad\xab\xba', # Line 31 + b'1', # Line 32 + b'WRTE\x99\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x001\x00\x00\x00\xa8\xad\xab\xba', # Line 35 + b'1', # Line 36 + b'WRTE\x99\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x000\x00\x00\x00\xa8\xad\xab\xba', # Line 39 + b'0', # Line 40 + b'WRTE\x99\x00\x00\x00\x01\x00\x00\x00\x13\x00\x00\x000\x06\x00\x00\xa8\xad\xab\xba', # Line 43 + b'Wake Locks: size=0\n', # Line 44 + b'WRTE\x99\x00\x00\x00\x01\x00\x00\x00\x1e\x00\x00\x00V\x0b\x00\x00\xa8\xad\xab\xba', # Line 47 + b'com.google.android.youtube.tv\n', # Line 48 + b'WRTE\x99\x00\x00\x00\x01\x00\x00\x00\x98\x00\x00\x00\xa13\x00\x00\xa8\xad\xab\xba', # Line 51 + b' state=PlaybackState {state=0, position=0, buffered position=0, speed=0.0, updated=0, actions=0, custom actions=[], active item id=-1, error=null}\n', # Line 52 + b'WRTE\x99\x00\x00\x00\x01\x00\x00\x00.\x01\x00\x00\xceP\x00\x00\xa8\xad\xab\xba', # Line 55 + b'- STREAM_MUSIC:\n Muted: false\n Min: 0\n Max: 15\n Current: 2 (speaker): 11, 4 (headset): 10, 8 (headphone): 10, 400 (hdmi): 6, 40000000 (default): 11\n Devices: hdmi\n- STREAM_ALARM:\n Muted: false\n Min: 0\n Max: 7\n Current: 40000000 (default): 6\n Devices: speaker\n- STREAM_NOTIFICATION:\n', # Line 56 + b'CLSE\x99\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbc\xb3\xac\xba', # Line 59 + b'AUTH\x01\x00\x00\x00\x00\x00\x00\x00\x14\x00\x00\x00\x94\t\x00\x00\xbe\xaa\xab\xb7', # Line 297 + b'P\xa5\x86\x97\xe8\x01\xb09\x8c>F\x9d\xc6\xbd\xc0J\x80!\xbb\x1a', # Line 298 + b"CNXN\x00\x00\x00\x01\x00\x10\x00\x00i\x00\x00\x00.'\x00\x00\xbc\xb1\xa7\xb1", # Line 301 + b'device::ro.product.name=once;ro.product.model=MIBOX3;ro.product.device=once;features=stat_v2,cmd,shell_v2', # Line 302 + b'OKAY\xa5\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb0\xb4\xbe\xa6', # Line 305 + b'CLSE\xa5\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbc\xb3\xac\xba', # Line 306 + msg1.pack(), + msg1.data, + msg2.pack(), + b'AUTH\x01\x00\x00\x00\x00\x00\x00\x00\x14\x00\x00\x00e\x0c\x00\x00\xbe\xaa\xab\xb7', # Line 315 + b'\xd3\xef\x7f_\xa6\xc0`b\x19\\z\xe4\xf3\xe2\xed\x8d\xe1W\xfbH', # Line 316 + b"CNXN\x00\x00\x00\x01\x00\x10\x00\x00i\x00\x00\x00.'\x00\x00\xbc\xb1\xa7\xb1", # Line 319 + b'device::ro.product.name=once;ro.product.model=MIBOX3;ro.product.device=once;features=stat_v2,cmd,shell_v2', # Line 320 + b'OKAY\xa7\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb0\xb4\xbe\xa6', # Line 323 + b'CLSE\xa7\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbc\xb3\xac\xba', # Line 324 + msg1.pack(), + msg1.data, + msg2.pack(), + b'AUTH\x01\x00\x00\x00\x00\x00\x00\x00\x14\x00\x00\x00\x93\x08\x00\x00\xbe\xaa\xab\xb7', # Line 333 + b's\xd4_e\xa4s\x02\x95\x0f\x1e\xec\n\x95Y9[`\x8e\xe1f', # Line 334 + b"CNXN\x00\x00\x00\x01\x00\x10\x00\x00i\x00\x00\x00.'\x00\x00\xbc\xb1\xa7\xb1", # Line 337 + b'device::ro.product.name=once;ro.product.model=MIBOX3;ro.product.device=once;features=stat_v2,cmd,shell_v2', # Line 338 + b'OKAY\xa9\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb0\xb4\xbe\xa6', # Line 341 + b'CLSE\xa9\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbc\xb3\xac\xba', # Line 342 + msg1.pack(), + msg1.data, + msg2.pack()]) + + self.assertTrue(await self.device.connect([signer])) + + await self.device.shell('Android TV update command') + + self.assertTrue(await self.device.connect([signer])) + await self.device.shell('Android TV update command') + await self.device.shell('Android TV update command') + self.assertTrue(await self.device.connect([signer])) + await self.device.shell('Android TV update command') + await self.device.shell('Android TV update command') + self.assertTrue(await self.device.connect([signer])) + await self.device.shell('Android TV update command') + await self.device.shell('Android TV update command') + + # ======================================================================= # + # # + # `streaming_shell` tests # + # # + # ======================================================================= # + @awaiter + async def test_streaming_shell_decode(self): + self.assertTrue(await self.device.connect()) + + # Provide the `bulk_read` return values + self.device._handle._bulk_read = join_messages( + AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), + AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=b'ABC'), + AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=b'123'), + ) + + async_generator = self.device.streaming_shell('TEST', decode=True) + self.assertEqual(await async_generator.__anext__(), 'ABC') + self.assertEqual(await async_generator.__anext__(), '123') + + @awaiter + async def test_streaming_shell_dont_decode(self): + self.assertTrue(await self.device.connect()) + + # Provide the `bulk_read` return values + self.device._handle._bulk_read = join_messages( + AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), + AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=b'ABC'), + AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=b'123'), + ) + + async_generator = self.device.streaming_shell('TEST', decode=False) + self.assertEqual(await async_generator.__anext__(), b'ABC') + self.assertEqual(await async_generator.__anext__(), b'123') + + + # ======================================================================= # + # # + # `root` test # + # # + # ======================================================================= # + @awaiter + async def test_root(self): + self.assertTrue(await self.device.connect()) + + with patch('adb_shell.adb_device_async.AdbDeviceAsync._service', new_callable=AsyncMock) as patch_service: + await self.device.root() + patch_service.assert_called_once() + + + # ======================================================================= # + # # + # `filesync` tests # + # # + # ======================================================================= # + @awaiter + async def test_list(self): + self.assertTrue(await self.device.connect()) + self.device._handle._bulk_write = b'' + + # Provide the `bulk_read` return values + self.device._handle._bulk_read = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), + AdbMessage(command=constants.OKAY, arg0=1, arg1=1), + AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=join_messages(FileSyncListMessage(constants.DENT, 1, 2, 3, data=b'file1'), + FileSyncListMessage(constants.DENT, 4, 5, 6, data=b'file2'), + FileSyncListMessage(constants.DONE, 0, 0, 0))), + AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) + + # Expected `bulk_write` values + expected_bulk_write = join_messages(AdbMessage(command=constants.OPEN, arg0=1, arg1=0, data=b'sync:\x00'), + AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=join_messages(FileSyncMessage(command=constants.LIST, data=b'/dir'))), + AdbMessage(command=constants.OKAY, arg0=1, arg1=1), + AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) + + expected_result = [DeviceFile(filename=bytearray(b'file1'), mode=1, size=2, mtime=3), + DeviceFile(filename=bytearray(b'file2'), mode=4, size=5, mtime=6)] + + self.assertEqual(await self.device.list('/dir'), expected_result) + self.assertEqual(self.device._handle._bulk_write, expected_bulk_write) + + async def _test_push(self, mtime): + self.assertTrue(await self.device.connect()) + self.device._handle._bulk_write = b'' + + filedata = b'Ohayou sekai.\nGood morning world!' + + # Provide the `bulk_read` return values + self.device._handle._bulk_read = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), + AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b''), + AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=join_messages(FileSyncMessage(constants.OKAY, data=b''))), + AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) + + # Expected `bulk_write` values + expected_bulk_write = join_messages(AdbMessage(command=constants.OPEN, arg0=1, arg1=0, data=b'sync:\x00'), + AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=join_messages(FileSyncMessage(command=constants.SEND, data=b'/data,33272'), + FileSyncMessage(command=constants.DATA, data=filedata), + FileSyncMessage(command=constants.DONE, arg0=mtime))), + AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b''), + AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) + + with patch('time.time', return_value=mtime): + await self.device.push(BytesIO(filedata), '/data', mtime=mtime) + self.assertEqual(self.device._handle._bulk_write, expected_bulk_write) + + return True + + @awaiter + async def test_push(self): + self.assertTrue(await self._test_push(100)) + + @awaiter + async def test_push_mtime0(self): + self.assertTrue(await self._test_push(0)) + + @awaiter + async def test_push_file(self): + self.assertTrue(await self.device.connect()) + self.device._handle._bulk_write = b'' + + mtime = 100 + filedata = b'Ohayou sekai.\nGood morning world!' + + # Provide the `bulk_read` return values + self.device._handle._bulk_read = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), + AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b''), + AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=FileSyncMessage(constants.OKAY).pack()), + AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) + + # Expected `bulk_write` values + expected_bulk_write = join_messages(AdbMessage(command=constants.OPEN, arg0=1, arg1=0, data=b'sync:\x00'), + AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=join_messages(FileSyncMessage(command=constants.SEND, data=b'/data,33272'), + FileSyncMessage(command=constants.DATA, data=filedata), + FileSyncMessage(command=constants.DONE, arg0=mtime, data=b''))), + AdbMessage(command=constants.OKAY, arg0=1, arg1=1), + AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) + + with patch('adb_shell.hidden_helpers.open', mock_open(read_data=filedata)): + await self.device.push('TEST_FILE', '/data', mtime=mtime) + self.assertEqual(self.device._handle._bulk_write, expected_bulk_write) + + @awaiter + async def test_push_fail(self): + self.assertTrue(await self.device.connect()) + self.device._handle._bulk_write = b'' + + mtime = 100 + filedata = b'Ohayou sekai.\nGood morning world!' + + # Provide the `bulk_read` return values + self.device._handle._bulk_read = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), + AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b''), + AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=join_messages(FileSyncMessage(constants.FAIL, data=b'')))) + + with self.assertRaises(exceptions.PushFailedError), patch('adb_shell.hidden_helpers.open', mock_open(read_data=filedata)): + await self.device.push('TEST_FILE', '/data', mtime=mtime) + + @awaiter + async def test_push_big_file(self): + self.assertTrue(await self.device.connect()) + self.device._handle._bulk_write = b'' + + mtime = 100 + filedata = b'0' * int(3.5 * constants.MAX_PUSH_DATA) + + # Provide the `bulk_read` return values + self.device._handle._bulk_read = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), + AdbMessage(command=constants.OKAY, arg0=1, arg1=1), + AdbMessage(command=constants.OKAY, arg0=1, arg1=1), + AdbMessage(command=constants.OKAY, arg0=1, arg1=1), + AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=join_messages(FileSyncMessage(constants.OKAY))), + AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) + + # Expected `bulk_write` values + mpd0, mpd1, mpd2, mpd3 = 0, constants.MAX_PUSH_DATA, 2*constants.MAX_PUSH_DATA, 3*constants.MAX_PUSH_DATA + expected_bulk_write = join_messages(AdbMessage(command=constants.OPEN, arg0=1, arg1=0, data=b'sync:\x00'), + AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=join_messages(FileSyncMessage(command=constants.SEND, data=b'/data,33272'), + FileSyncMessage(command=constants.DATA, data=filedata[mpd0:mpd1]))), + AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=join_messages(FileSyncMessage(command=constants.DATA, data=filedata[mpd1:mpd2]))), + AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=join_messages(FileSyncMessage(command=constants.DATA, data=filedata[mpd2:mpd3]), + FileSyncMessage(command=constants.DATA, data=filedata[mpd3:]), + FileSyncMessage(command=constants.DONE, arg0=mtime))), + AdbMessage(command=constants.OKAY, arg0=1, arg1=1), + AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) + + await self.device.push(BytesIO(filedata), '/data', mtime=mtime) + self.assertEqual(self.device._handle._bulk_write, expected_bulk_write) + + @awaiter + async def test_push_dir(self): + self.assertTrue(await self.device.connect()) + + mtime = 100 + filedata = b'Ohayou sekai.\nGood morning world!' + + # Provide the `bulk_read` return values + self.device._handle._bulk_read = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), + AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b''), + AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), + AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), + AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=join_messages(FileSyncMessage(constants.OKAY))), + AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b''), + AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), + AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), + AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=join_messages(FileSyncMessage(constants.OKAY))), + AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) + + # Expected `bulk_write` values + #TODO + + with patch('adb_shell.hidden_helpers.open', mock_open(read_data=filedata)), patch('os.path.isdir', lambda x: x == 'TEST_DIR/'), patch('os.listdir', return_value=['TEST_FILE1', 'TEST_FILE2']): + await self.device.push('TEST_DIR/', '/data', mtime=mtime) + + @awaiter + async def test_pull(self): + self.assertTrue(await self.device.connect()) + self.device._handle._bulk_write = b'' + + filedata = b'Ohayou sekai.\nGood morning world!' + + # Provide the `bulk_read` return values + self.device._handle._bulk_read = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), + AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), + AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=join_messages(FileSyncMessage(command=constants.DATA, data=filedata), + FileSyncMessage(command=constants.DONE))), + AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) + + # Expected `bulk_write` values + expected_bulk_write = join_messages(AdbMessage(command=constants.OPEN, arg0=1, arg1=0, data=b'sync:\x00'), + AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=join_messages(FileSyncMessage(command=constants.RECV, data=b'/data'))), + AdbMessage(command=constants.OKAY, arg0=1, arg1=1), + AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) + + self.assertEqual(await self.device.pull('/data'), filedata) + self.assertEqual(self.device._handle._bulk_write, expected_bulk_write) + + @awaiter + async def test_pull_file(self): + self.assertTrue(await self.device.connect()) + self.device._handle._bulk_write = b'' + + filedata = b'Ohayou sekai.\nGood morning world!' + + # Provide the `bulk_read` return values + self.device._handle._bulk_read = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), + AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), + AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=join_messages(FileSyncMessage(command=constants.DATA, data=filedata), + FileSyncMessage(command=constants.DONE))), + AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) + + # Expected `bulk_write` values + expected_bulk_write = join_messages(AdbMessage(command=constants.OPEN, arg0=1, arg1=0, data=b'sync:\x00'), + AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=join_messages(FileSyncMessage(command=constants.RECV, data=b'/data'))), + AdbMessage(command=constants.OKAY, arg0=1, arg1=1), + AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) + + with patch('adb_shell.hidden_helpers.open', mock_open()), patch('os.path.exists', return_value=True): + self.assertTrue(await self.device.pull('/data', 'TEST_FILE')) + self.assertEqual(self.device._handle._bulk_write, expected_bulk_write) + + @awaiter + async def test_pull_file_return_true(self): + self.assertTrue(await self.device.connect()) + self.device._handle._bulk_write = b'' + + filedata = b'Ohayou sekai.\nGood morning world!' + + # Provide the `bulk_read` return values + self.device._handle._bulk_read = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), + AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), + AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=join_messages(FileSyncMessage(command=constants.DATA, data=filedata), + FileSyncMessage(command=constants.DONE))), + AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) + + # Expected `bulk_write` values + expected_bulk_write = join_messages(AdbMessage(command=constants.OPEN, arg0=1, arg1=0, data=b'sync:\x00'), + AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=join_messages(FileSyncMessage(command=constants.RECV, data=b'/data'))), + AdbMessage(command=constants.OKAY, arg0=1, arg1=1), + AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) + + with patch('adb_shell.hidden_helpers.open', mock_open()), patch('adb_shell.adb_device_async.hasattr', return_value=False): + self.assertTrue(await self.device.pull('/data', 'TEST_FILE')) + self.assertEqual(self.device._handle._bulk_write, expected_bulk_write) + + @awaiter + async def test_pull_big_file(self): + self.assertTrue(await self.device.connect()) + self.device._handle._bulk_write = b'' + + filedata = b'0' * int(1.5 * constants.MAX_ADB_DATA) + + # Provide the `bulk_read` return values + + self.device._handle._bulk_read = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), + AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), + AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=join_messages(FileSyncMessage(command=constants.DATA, data=filedata), + FileSyncMessage(command=constants.DONE))), + AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) + + # Expected `bulk_write` values + expected_bulk_write = join_messages(AdbMessage(command=constants.OPEN, arg0=1, arg1=0, data=b'sync:\x00'), + AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=join_messages(FileSyncMessage(command=constants.RECV, data=b'/data'))), + AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b''), + AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) + + with patch('adb_shell.hidden_helpers.open', mock_open()), patch('os.path.exists', return_value=True): + self.assertTrue(await self.device.pull('/data', 'TEST_FILE')) + self.assertEqual(self.device._handle._bulk_write, expected_bulk_write) + + @awaiter + async def test_stat(self): + self.assertTrue(await self.device.connect()) + self.device._handle._bulk_write = b'' + + # Provide the `bulk_read` return values + + self.device._handle._bulk_read = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), + AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), + AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=join_messages(FileSyncStatMessage(constants.STAT, 1, 2, 3), + FileSyncStatMessage(constants.DONE, 0, 0, 0))), + AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) + + # Expected `bulk_write` values + expected_bulk_write = join_messages(AdbMessage(command=constants.OPEN, arg0=1, arg1=0, data=b'sync:\x00'), + AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=join_messages(FileSyncMessage(command=constants.STAT, data=b'/data'))), + AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b''), + AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) + + self.assertEqual(await self.device.stat('/data'), (1, 2, 3)) + self.assertEqual(self.device._handle._bulk_write, expected_bulk_write) + + # ======================================================================= # + # # + # `filesync` hidden methods tests # + # # + # ======================================================================= # + @awaiter + async def test_filesync_read_adb_command_failure_exceptions(self): + self.assertTrue(await self.device.connect()) + self.device._handle._bulk_write = b'' + + # Provide the `bulk_read` return values + self.device._handle._bulk_read = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), + AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), + AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=join_messages(FileSyncStatMessage(constants.FAIL, 1, 2, 3), + FileSyncStatMessage(constants.DONE, 0, 0, 0)))) + + with self.assertRaises(exceptions.AdbCommandFailureException): + await self.device.stat('/data') + + @awaiter + async def test_filesync_read_invalid_response_error(self): + self.assertTrue(await self.device.connect()) + self.device._handle._bulk_write = b'' + + # Provide the `bulk_read` return values + self.device._handle._bulk_read = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), + AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), + AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=join_messages(FileSyncStatMessage(constants.DENT, 1, 2, 3), + FileSyncStatMessage(constants.DONE, 0, 0, 0)))) + + with self.assertRaises(exceptions.InvalidResponseError): + await self.device.stat('/data') + + # ======================================================================= # + # # + # `filesync` error tests # + # # + # ======================================================================= # + @awaiter + async def test_pull_value_error(self): + self.assertTrue(await self.device.connect()) + self.device._handle._bulk_write = b'' + + with self.assertRaises(ValueError): + await self.device.pull('device_filename', 123) diff --git a/tests/test_tcp_handle_async.py b/tests/test_tcp_handle_async.py new file mode 100644 index 0000000..8788a70 --- /dev/null +++ b/tests/test_tcp_handle_async.py @@ -0,0 +1,81 @@ +import asyncio +import unittest +from unittest.mock import patch + +from adb_shell.exceptions import TcpTimeoutException +from adb_shell.handle.tcp_handle_async import TcpHandleAsync + +from .async_patchers import AsyncMock, FakeStreamReader, FakeStreamWriter +from .async_wrapper import awaiter +from . import patchers + + +@patchers.ASYNC_SKIPPER +class TestTcpHandleAsync(unittest.TestCase): + def setUp(self): + """Create a ``TcpHandleAsync`` and connect to a TCP service. + + """ + self.handle = TcpHandleAsync('host', 5555) + + @awaiter + async def test_close(self): + await self.handle.close() + + @awaiter + async def test_close2(self): + await self.handle.close() + + @awaiter + async def test_connect(self): + with patch('asyncio.open_connection', return_value=(True, True), new_callable=AsyncMock): + await self.handle.connect() + + @awaiter + async def test_connect_close(self): + with patch('asyncio.open_connection', return_value=(FakeStreamReader(), FakeStreamWriter()), new_callable=AsyncMock): + await self.handle.connect() + self.assertIsNotNone(self.handle._writer) + + await self.handle.close() + self.assertIsNone(self.handle._reader) + self.assertIsNone(self.handle._writer) + + @awaiter + async def test_connect_close_catch_oserror(self): + with patch('asyncio.open_connection', return_value=(FakeStreamReader(), FakeStreamWriter()), new_callable=AsyncMock): + await self.handle.connect() + self.assertIsNotNone(self.handle._writer) + + with patch('{}.FakeStreamWriter.close'.format(__name__), side_effect=OSError): + await self.handle.close() + self.assertIsNone(self.handle._reader) + self.assertIsNone(self.handle._writer) + + @awaiter + async def test_connect_with_timeout(self): + with self.assertRaises(TcpTimeoutException): + with patch('asyncio.open_connection', side_effect=asyncio.TimeoutError, new_callable=AsyncMock): + await self.handle.connect() + + @awaiter + async def test_bulk_read(self): + with patch('asyncio.open_connection', return_value=(FakeStreamReader(), FakeStreamWriter()), new_callable=AsyncMock): + await self.handle.connect() + + self.assertEqual(await self.handle.bulk_read(4), b'TEST') + + with self.assertRaises(TcpTimeoutException): + with patch('{}.FakeStreamReader.read'.format(__name__), side_effect=asyncio.TimeoutError): + await self.handle.bulk_read(4) + + @awaiter + async def test_bulk_write(self): + with patch('asyncio.open_connection', return_value=(FakeStreamReader(), FakeStreamWriter()), new_callable=AsyncMock): + await self.handle.connect() + + self.assertEqual(await self.handle.bulk_write(b'TEST'), 4) + + with self.assertRaises(TcpTimeoutException): + with patch('{}.FakeStreamWriter.write'.format(__name__), side_effect=asyncio.TimeoutError): + await self.handle.bulk_write(b'TEST')