Repository: JeffLIrion/adb_shell Branch: master Commit: 149ffbcff47b Files: 82 Total size: 444.2 KB Directory structure: gitextract_gtoem9ut/ ├── .flake8 ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ └── bug_report.md │ └── workflows/ │ └── python-package.yml ├── .gitignore ├── .pylintrc ├── .readthedocs.yml ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── adb_shell/ │ ├── __init__.py │ ├── adb_device.py │ ├── adb_device_async.py │ ├── adb_message.py │ ├── auth/ │ │ ├── __init__.py │ │ ├── keygen.py │ │ ├── sign_cryptography.py │ │ ├── sign_pycryptodome.py │ │ └── sign_pythonrsa.py │ ├── constants.py │ ├── exceptions.py │ ├── hidden_helpers.py │ └── transport/ │ ├── __init__.py │ ├── base_transport.py │ ├── base_transport_async.py │ ├── tcp_transport.py │ ├── tcp_transport_async.py │ └── usb_transport.py ├── docs/ │ ├── Makefile │ ├── make.bat │ ├── requirements.txt │ └── source/ │ ├── adb_shell.adb_device.rst │ ├── adb_shell.adb_device_async.rst │ ├── adb_shell.adb_message.rst │ ├── adb_shell.auth.keygen.rst │ ├── adb_shell.auth.rst │ ├── adb_shell.auth.sign_cryptography.rst │ ├── adb_shell.auth.sign_pycryptodome.rst │ ├── adb_shell.auth.sign_pythonrsa.rst │ ├── adb_shell.constants.rst │ ├── adb_shell.exceptions.rst │ ├── adb_shell.hidden_helpers.rst │ ├── adb_shell.rst │ ├── adb_shell.transport.base_transport.rst │ ├── adb_shell.transport.base_transport_async.rst │ ├── adb_shell.transport.rst │ ├── adb_shell.transport.tcp_transport.rst │ ├── adb_shell.transport.tcp_transport_async.rst │ ├── adb_shell.transport.usb_transport.rst │ ├── conf.py │ ├── index.rst │ └── modules.rst ├── scripts/ │ ├── bumpversion.sh │ ├── get_package_name.sh │ ├── get_version.sh │ ├── git_retag.sh │ ├── git_tag.sh │ ├── pre-commit.sh │ └── rename_package.sh ├── setup.py ├── tests/ │ ├── __init__.py │ ├── async_patchers.py │ ├── async_wrapper.py │ ├── filesync_helpers.py │ ├── keygen_stub.py │ ├── patchers.py │ ├── test_adb_device.py │ ├── test_adb_device_async.py │ ├── test_adb_message.py │ ├── test_exceptions.py │ ├── test_hidden_helpers.py │ ├── test_keygen.py │ ├── test_sign_cryptography.py │ ├── test_sign_pycryptodome.py │ ├── test_sign_pythonrsa.py │ ├── test_tcp_transport.py │ ├── test_tcp_transport_async.py │ ├── test_usb_importerror.py │ └── test_usb_transport.py └── venv_requirements.txt ================================================ FILE CONTENTS ================================================ ================================================ FILE: .flake8 ================================================ [flake8] ignore = E501,W504 ================================================ FILE: .gitattributes ================================================ Makefile eol=lf *.sh eol=lf *.ipynb eol=lf ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- ### Description ### Log To enable debug logging in Python: ```python import logging logging.basicConfig(level=logging.DEBUG) ``` To enable debug logging in Home Assistant: #### Approach 1: configuration.yaml ```yaml logger: default: warning # or whatever logs: adb_shell: debug ``` #### Approach 2: `logger.set_level` service ```yaml adb_shell: debug ``` ================================================ FILE: .github/workflows/python-package.yml ================================================ # This workflow will install Python dependencies, run tests and lint with a variety of Python versions # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions name: Python package on: push: branches: [ master ] pull_request: branches: [ master ] env: ENV_GITHUB_ACTIONS: 'ENV_GITHUB_ACTIONS' jobs: build: runs-on: ubuntu-latest strategy: matrix: python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip make venv - name: Linting checks with pylint, flake8, and (soon) black run: | make lint-flake8 lint-pylint - name: Test with pytest env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} COVERALLS_SERVICE_NAME: github run: | make coverage && coveralls - name: Upload wheel as a workflow artifact uses: actions/upload-artifact@v4 with: name: wheel path: dist/*.whl ================================================ FILE: .gitignore ================================================ # Python files *.idea *.pyc **/__pycache__/ adb_shell.egg-info # Build files build/ dist/ # Documentation docs/build/ docs/html/ # Coverage .coverage htmlcov/ ================================================ FILE: .pylintrc ================================================ [MASTER] # A comma-separated list of package or module names from where C extensions may # be loaded. Extensions are loading into the active Python interpreter and may # run arbitrary code. extension-pkg-whitelist= # Add files or directories to the blacklist. They should be base names, not # paths. ignore=CVS # Add files or directories matching the regex patterns to the blacklist. The # regex matches against base names, not paths. ignore-patterns= # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). #init-hook= # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the # number of processors available to use. jobs=1 # Control the amount of potential inferred values when inferring a single # object. This can help the performance when dealing with large functions or # complex, nested conditions. limit-inference-results=100 # List of plugins (as comma separated values of python modules names) to load, # usually to register additional checkers. load-plugins= # Pickle collected data for later comparisons. persistent=yes # Specify a configuration file. #rcfile= # When enabled, pylint would attempt to guess common misconfiguration and emit # user-friendly hints instead of false-positive error messages. suggestion-mode=yes # Allow loading of arbitrary C extensions. Extensions are imported into the # active Python interpreter and may run arbitrary code. unsafe-load-any-extension=no [MESSAGES CONTROL] # Only show warnings with the listed confidence levels. Leave empty to show # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. confidence= # Disable the message, report, category or checker with the given id(s). You # can either give multiple identifiers separated by comma (,) or put this # option multiple times (only on the command line, not in the configuration # file where it should appear only once). You can also use "--disable=all" to # disable everything first and then reenable specific checks. For example, if # you want to run only the similarities checker, you can use "--disable=all # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use "--disable=all --enable=classes # --disable=W". disable=consider-using-f-string, duplicate-code, invalid-name, line-too-long, raise-missing-from, super-with-arguments, too-many-arguments, too-many-branches, too-many-instance-attributes, too-many-lines, too-many-locals, too-many-nested-blocks, too-many-positional-arguments, too-many-public-methods, too-many-return-statements, too-many-statements, useless-object-inheritance, unspecified-encoding # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option # multiple time (only on the command line, not in the configuration file where # it should appear only once). See also the "--disable" option for examples. enable=c-extension-no-member [REPORTS] # Python expression which should return a note less than 10 (10 is the highest # note). You have access to the variables errors warning, statement which # respectively contain the number of errors / warnings messages and the total # number of statements analyzed. This is used by the global evaluation report # (RP0004). evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) # Template used to display messages. This is a python new-style format string # used to format the message information. See doc for all details. #msg-template= # Set the output format. Available formats are text, parseable, colorized, json # and msvs (visual studio). You can also give a reporter class, e.g. # mypackage.mymodule.MyReporterClass. output-format=text # Tells whether to display a full report or only the messages. reports=no # Activate the evaluation score. score=yes [REFACTORING] # Maximum number of nested blocks for function / method body max-nested-blocks=5 # Complete name of functions that never returns. When checking for # inconsistent-return-statements if a never returning function is called then # it will be considered as an explicit return statement and no message will be # printed. never-returning-functions=sys.exit [LOGGING] # Format style used to check logging format string. `old` means using % # formatting, while `new` is for `{}` formatting. logging-format-style=old # Logging modules to check that the string format arguments are in logging # function parameter format. logging-modules=logging [FORMAT] # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. expected-line-ending-format= # Regexp for a line that is allowed to be longer than the limit. ignore-long-lines=^\s*(# )??$ # Number of spaces of indent required inside a hanging or continued line. indent-after-paren=4 # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 # tab). indent-string=' ' # Maximum number of characters on a single line. max-line-length=100 # Maximum number of lines in a module. max-module-lines=1000 # Allow the body of a class to be on the same line as the declaration if body # contains single statement. single-line-class-stmt=no # Allow the body of an if to be on the same line as the test if there is no # else. single-line-if-stmt=no [MISCELLANEOUS] # List of note tags to take in consideration, separated by a comma. notes=FIXME, XXX, TODO [BASIC] # Naming style matching correct argument names. argument-naming-style=snake_case # Regular expression matching correct argument names. Overrides argument- # naming-style. #argument-rgx= # Naming style matching correct attribute names. attr-naming-style=snake_case # Regular expression matching correct attribute names. Overrides attr-naming- # style. #attr-rgx= # Bad variable names which should always be refused, separated by a comma. bad-names=foo, bar, baz, toto, tutu, tata # Naming style matching correct class attribute names. class-attribute-naming-style=any # Regular expression matching correct class attribute names. Overrides class- # attribute-naming-style. #class-attribute-rgx= # Naming style matching correct class names. class-naming-style=PascalCase # Regular expression matching correct class names. Overrides class-naming- # style. #class-rgx= # Naming style matching correct constant names. const-naming-style=UPPER_CASE # Regular expression matching correct constant names. Overrides const-naming- # style. #const-rgx= # Minimum line length for functions/classes that require docstrings, shorter # ones are exempt. docstring-min-length=-1 # Naming style matching correct function names. function-naming-style=snake_case # Regular expression matching correct function names. Overrides function- # naming-style. #function-rgx= # Good variable names which should always be accepted, separated by a comma. good-names=i, j, k, ex, Run, _ # Include a hint for the correct naming format with invalid-name. include-naming-hint=no # Naming style matching correct inline iteration names. inlinevar-naming-style=any # Regular expression matching correct inline iteration names. Overrides # inlinevar-naming-style. #inlinevar-rgx= # Naming style matching correct method names. method-naming-style=snake_case # Regular expression matching correct method names. Overrides method-naming- # style. #method-rgx= # Naming style matching correct module names. module-naming-style=snake_case # Regular expression matching correct module names. Overrides module-naming- # style. #module-rgx= # Colon-delimited sets of names that determine each other's naming style when # the name regexes allow several styles. name-group= # Regular expression which should only match function or class names that do # not require a docstring. no-docstring-rgx=^_ # List of decorators that produce properties, such as abc.abstractproperty. Add # to this list to register other decorators that produce valid properties. # These decorators are taken in consideration only for invalid-name. property-classes=abc.abstractproperty # Naming style matching correct variable names. variable-naming-style=snake_case # Regular expression matching correct variable names. Overrides variable- # naming-style. #variable-rgx= [TYPECHECK] # List of decorators that produce context managers, such as # contextlib.contextmanager. Add to this list to register other decorators that # produce valid context managers. contextmanager-decorators=contextlib.contextmanager # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E1101 when accessed. Python regular # expressions are accepted. generated-members= # Tells whether missing members accessed in mixin class should be ignored. A # mixin class is detected if its name ends with "mixin" (case insensitive). ignore-mixin-members=yes # Tells whether to warn about missing members when the owner of the attribute # is inferred to be None. ignore-none=yes # This flag controls whether pylint should warn about no-member and similar # checks whenever an opaque object is returned when inferring. The inference # can return multiple potential results while evaluating a Python object, but # some branches might not be evaluated, which results in partial inference. In # that case, it might be useful to still emit no-member and other checks for # the rest of the inferred objects. ignore-on-opaque-inference=yes # List of class names for which member attributes should not be checked (useful # for classes with dynamically set attributes). This supports the use of # qualified names. ignored-classes=optparse.Values,thread._local,_thread._local,_socketobject # List of module names for which member attributes should not be checked # (useful for modules/projects where namespaces are manipulated during runtime # and thus existing member attributes cannot be deduced by static analysis. It # supports qualified module names, as well as Unix pattern matching. ignored-modules= # Show a hint with possible names when a member name was not found. The aspect # of finding the hint is based on edit distance. missing-member-hint=yes # The minimum edit distance a name should have in order to be considered a # similar match for a missing member name. missing-member-hint-distance=1 # The total number of similar names that should be taken in consideration when # showing a hint for a missing member. missing-member-max-choices=1 [STRING] # This flag controls whether the implicit-str-concat-in-sequence should # generate a warning on implicit string concatenation in sequences defined over # several lines. check-str-concat-over-line-jumps=no [SPELLING] # Limits count of emitted suggestions for spelling mistakes. max-spelling-suggestions=4 # Spelling dictionary name. Available dictionaries: none. To make it working # install python-enchant package.. spelling-dict= # List of comma separated words that should not be checked. spelling-ignore-words= # A path to a file that contains private dictionary; one word per line. spelling-private-dict-file= # Tells whether to store unknown words to indicated private dictionary in # --spelling-private-dict-file option instead of raising a message. spelling-store-unknown-words=no [SIMILARITIES] # Ignore comments when computing similarities. ignore-comments=yes # Ignore docstrings when computing similarities. ignore-docstrings=yes # Ignore imports when computing similarities. ignore-imports=no # Minimum lines number of a similarity. min-similarity-lines=4 [VARIABLES] # List of additional names supposed to be defined in builtins. Remember that # you should avoid defining new builtins when possible. additional-builtins= # Tells whether unused global variables should be treated as a violation. allow-global-unused-variables=yes # List of strings which can identify a callback function by name. A callback # name must start or end with one of those strings. callbacks=cb_, _cb # A regular expression matching the name of dummy variables (i.e. expected to # not be used). dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ # Argument names that match this expression will be ignored. Default to name # with leading underscore. ignored-argument-names=_.*|^ignored_|^unused_ # Tells whether we should check for unused import in __init__ files. init-import=no # List of qualified module names which can have objects that can redefine # builtins. redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io [DESIGN] # Maximum number of arguments for function / method. max-args=5 # Maximum number of attributes for a class (see R0902). max-attributes=7 # Maximum number of boolean expressions in an if statement. max-bool-expr=5 # Maximum number of branch for function / method body. max-branches=12 # Maximum number of locals for function / method body. max-locals=15 # Maximum number of parents for a class (see R0901). max-parents=7 # Maximum number of public methods for a class (see R0904). max-public-methods=20 # Maximum number of return / yield for function / method body. max-returns=6 # Maximum number of statements in function / method body. max-statements=50 # Minimum number of public methods for a class (see R0903). min-public-methods=2 [IMPORTS] # Allow wildcard imports from modules that define __all__. allow-wildcard-with-all=no # Analyse import fallback blocks. This can be used to support both Python 2 and # 3 compatible code, which means that the block might have code that exists # only in one or another interpreter, leading to false positives when analysed. analyse-fallback-blocks=no # Deprecated modules which should not be used, separated by a comma. deprecated-modules=optparse,tkinter.tix # Create a graph of external dependencies in the given file (report RP0402 must # not be disabled). ext-import-graph= # Create a graph of every (i.e. internal and external) dependencies in the # given file (report RP0402 must not be disabled). import-graph= # Create a graph of internal dependencies in the given file (report RP0402 must # not be disabled). int-import-graph= # Force import order to recognize a module as part of the standard # compatibility libraries. known-standard-library= # Force import order to recognize a module as part of a third party library. known-third-party=enchant [CLASSES] # List of method names used to declare (i.e. assign) instance attributes. defining-attr-methods=__init__, __new__, setUp # List of member names, which should be excluded from the protected access # warning. exclude-protected=_asdict, _fields, _replace, _source, _make # List of valid names for the first argument in a class method. valid-classmethod-first-arg=cls # List of valid names for the first argument in a metaclass class method. valid-metaclass-classmethod-first-arg=cls [EXCEPTIONS] # Exceptions that will emit a warning when being caught. Defaults to # "BaseException, Exception". overgeneral-exceptions=builtins.Exception ================================================ FILE: .readthedocs.yml ================================================ # .readthedocs.yml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Set the OS build: os: ubuntu-22.04 tools: python: "3.11" # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/source/conf.py # Optionally build your docs in additional formats such as PDF and ePub formats: all # Optionally set the version of Python and requirements required to build your docs python: install: - requirements: docs/requirements.txt ================================================ FILE: .travis.yml ================================================ language: python python: - "2.7" - "3.5" - "3.6" - "3.7" - "3.8" - "3.9" addons: apt: packages: - swig - libusb-1.0-0-dev install: - pip install . - pip install flake8 pylint coveralls cryptography libusb1>=1.0.16 pycryptodome - python --version 2>&1 | grep -q "Python 2" && pip install mock || true - if python --version 2>&1 | grep -q "Python 3.7" || python --version 2>&1 | grep -q "Python 3.8" || python --version 2>&1 | grep -q "Python 3.9"; then pip install aiofiles; fi script: - if python --version 2>&1 | grep -q "Python 2" || python --version 2>&1 | grep -q "Python 3.5" || python --version 2>&1 | grep -q "Python 3.6"; then flake8 adb_shell/ --exclude="adb_shell/adb_device_async.py,adb_shell/transport/base_transport_async.py,adb_shell/transport/tcp_transport_async.py" && pylint --ignore="adb_device_async.py,base_transport_async.py,tcp_transport_async.py" adb_shell/; fi - if python --version 2>&1 | grep -q "Python 3.7" || python --version 2>&1 | grep -q "Python 3.8" || python --version 2>&1 | grep -q "Python 3.9"; 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" || python --version 2>&1 | grep -q "Python 3.6"; 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.7" || python --version 2>&1 | grep -q "Python 3.8" || python --version 2>&1 | grep -q "Python 3.9"; then coverage run --source adb_shell -m unittest discover -s tests/ -t . && coverage report -m; fi after_success: - if python --version 2>&1 | grep -q "Python 3.7" || python --version 2>&1 | grep -q "Python 3.8" || python --version 2>&1 | grep -q "Python 3.9"; then coveralls; fi ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS ================================================ FILE: MANIFEST.in ================================================ include LICENSE ================================================ FILE: Makefile ================================================ #-------------------- ONLY MODIFY CODE IN THIS SECTION --------------------# PACKAGE_DIR := adb_shell TEST_DIR := tests DOCS_DIR := docs # Change to false if you don't want to use pytest USE_PYTEST := true # Change this to false if you don't want to run linting checks on the tests LINT_TEST_DIR := false #-------------------- DO NOT MODIFY CODE BELOW!!!!!!!! --------------------# export PATH := $(abspath venv)/bin:${PATH} # Binaries to run BLACK := $(abspath venv)/bin/black COVERAGE := $(abspath venv)/bin/coverage FLAKE8 := $(abspath venv)/bin/flake8 PIP := $(abspath venv)/bin/pip3 PYLINT := $(abspath venv)/bin/pylint PYTEST := $(abspath venv)/bin/pytest PYTHON := $(abspath venv)/bin/python SPHINX_APIDOC := $(abspath venv)/bin/sphinx-apidoc TWINE := $(abspath venv)/bin/twine # Whether to include "*_async.py" files INCLUDE_ASYNC = $(shell $(PYTHON) --version | grep -q "Python 3.[7891]" && echo "true" || echo "false") # Async vs. Sync files PACKAGE_ASYNC_FILES = $(shell ls -m $(PACKAGE_DIR)/*_async.py 2>/dev/null) TEST_ASYNC_FILES = $(shell ls -m $(TEST_DIR)/*_async.py 2>/dev/null) TEST_SYNC_FILES = $(shell cd $(TEST_DIR) && ls test*.py | grep -v async) # Target prerequisites that may or may not exist VENV_REQUIREMENTS_TXT := $(wildcard venv_requirements.txt) SETUP_PY := $(wildcard setup.py) # A prerequisite for forcing targets to run FORCE: # Help! help: ## Show this help menu @echo "\n\033[1mUsage:\033[0m"; \ awk -F ':|##' '/^[^\t].+?:.*?##/ { printf "\033[36m make %-20s\033[0m %s\n", $$1, $$NF }' $(MAKEFILE_LIST) | grep -v "make venv/\." | sort @echo "" @echo "NOTES:" @echo "- The 'venv/.bin' target may fail because newer Python versions include the 'venv' package. Follow the instructions to create the virtual environment manually." ifneq ("$(wildcard scripts/pre-commit.sh)", "") @echo "- To install the git pre-commit hook:\n\n scripts/pre-commit.sh\n" endif @echo "- You may need to activate the virtual environment prior to running any Make commands:\n\n source venv/bin/activate\n" # Virtual environment targets .PHONY: clean-venv clean-venv: ## Remove the virtual environment rm -rf venv venv: venv/.bin venv/.requirements venv/.setup .git/hooks/pre-commit ## Create the virtual environment and install all necessary packages venv/.bin: ## Create the virtual environment if [ -z "$$ENV_GITHUB_ACTIONS" ]; then \ echo -e "If this target fails, you can perform this action manually via:\n\n make clean-venv && python3 -m venv venv && source venv/bin/activate && pip install -U setuptools && echo -e '*.*\\\n**/' > venv/.gitignore && touch venv/.bin\n\n"; \ apt list -a --installed python3-venv 2>&1 | grep -q installed || sudo apt update && sudo apt install python3-venv; \ python3 -m venv venv; \ $(PIP) install -U setuptools; \ else \ mkdir -p venv/bin; \ ln -s $$(which pip) $(PIP); \ ln -s $$(which python) $(PYTHON); \ fi mkdir -p venv/bin echo '*.*\n**/' > venv/.gitignore touch venv/.bin venv/.requirements: venv/.bin $(VENV_REQUIREMENTS_TXT) ## Install the requirements from 'venv_requirements.txt' in the virtual environment ifneq ("$(wildcard venv_requirements.txt)", "") $(PIP) install -U -r venv_requirements.txt if ! [ -z "$$ENV_GITHUB_ACTIONS" ]; then \ ln -s $$(which black) $(BLACK); \ ln -s $$(which coverage) $(COVERAGE); \ ln -s $$(which flake8) $(FLAKE8); \ ln -s $$(which pylint) $(PYLINT); \ ln -s $$(which pytest) $(PYTEST); \ ln -s $$(which sphinx-apidoc) $(SPHINX_APIDOC); \ ln -s $$(which twine) $(TWINE); \ fi endif touch venv/.requirements # Install the package in the virtual environment venv/.setup: venv/.bin $(SETUP_PY) ifneq ("$(wildcard setup.py)", "") $(PIP) install . endif touch venv/.setup .PHONY: uninstall uninstall: rm -f venv/.setup .PHONY: install install: uninstall venv/.setup ## Install the package in the virtual environment # Create the pre-commit hook .git/hooks/pre-commit: ./scripts/pre-commit.sh MAKE_PRECOMMIT_HOOK .PHONY: pre-commit pre-commit: .git/hooks/pre-commit ## Create the pre-commit hook # Linting and code analysis .PHONY: black black: venv ## Format the code using black $(BLACK) --safe --line-length 120 --target-version py35 $(PACKAGE_DIR) $(BLACK) --safe --line-length 120 --target-version py35 $(TEST_DIR) ifneq ("$(wildcard setup.py)", "") $(BLACK) --safe --line-length 120 --target-version py35 setup.py endif .PHONY: lint-black lint-black: venv ## Check that the code is formatted using black $(BLACK) --check --line-length 120 --safe --target-version py35 $(PACKAGE_DIR) $(BLACK) --check --line-length 120 --safe --target-version py35 $(TEST_DIR) ifneq ("$(wildcard setup.py)", "") $(BLACK) --check --line-length 120 --safe --target-version py35 setup.py endif .PHONY: lint-flake8 lint-flake8: venv ## Check the code using flake8 ifeq ($(INCLUDE_ASYNC), true) $(FLAKE8) $(PACKAGE_DIR) ifeq ($(LINT_TEST_DIR), true) $(FLAKE8) $(TEST_DIR) endif else $(FLAKE8) $(PACKAGE_DIR) --exclude="$(PACKAGE_ASYNC_FILES)" ifeq ($(LINT_TEST_DIR), true) $(FLAKE8) $(TEST_DIR) --exclude="$(TEST_ASYNC_FILES)" endif endif ifneq ("$(wildcard setup.py)", "") $(FLAKE8) setup.py endif .PHONY: lint-pylint lint-pylint: venv ## Check the code using pylint ifeq ($(INCLUDE_ASYNC), true) $(PYLINT) $(PACKAGE_DIR) ifeq ($(LINT_TEST_DIR), true) $(PYLINT) $(TEST_DIR) endif else $(PYLINT) $(PACKAGE_DIR) --ignore="$(PACKAGE_ASYNC_FILES)" ifeq ($(LINT_TEST_DIR), true) $(PYLINT) $(TEST_DIR) --ignore="$(TEST_ASYNC_FILES)" endif endif ifneq ("$(wildcard setup.py)", "") $(PYLINT) setup.py endif .PHONY: lint lint: lint-black lint-flake8 lint-pylint ## Run all linting checks on the code # Testing and coverage. .PHONY: test test: venv ## Run the unit tests ifeq ($(INCLUDE_ASYNC), true) ifeq ($(USE_PYTEST), true) $(PYTEST) $(TEST_DIR) else $(PYTHON) -m unittest discover -s $(TEST_DIR)/ -t . endif else ifeq ($(USE_PYTEST), true) $(PYTEST) $(TEST_DIR) --ignore-glob="*async.py" else for synctest in $(TEST_SYNC_FILES); do echo "\033[1;32m$(TEST_DIR)/$$synctest\033[0m" && $(PYTHON) -m unittest "$(TEST_DIR)/$$synctest"; done endif endif .PHONY: coverage coverage: venv ## Run the unit tests and produce coverage info ifeq ($(INCLUDE_ASYNC), true) ifeq ($(USE_PYTEST), true) $(COVERAGE) run --source $(PACKAGE_DIR) -m pytest $(TEST_DIR)/ && $(COVERAGE) report -m else $(COVERAGE) run --source $(PACKAGE_DIR) -m unittest discover -s $(TEST_DIR) -t . && $(COVERAGE) report -m endif else ifeq ($(USE_PYTEST), true) $(COVERAGE) run --source $(PACKAGE_DIR) -m pytest $(TEST_DIR)/ --ignore-glob="*async.py" && $(COVERAGE) report -m else for synctest in $(TEST_SYNC_FILES); do echo "\033[1;32m$(TEST_DIR)/$$synctest\033[0m" && $(COVERAGE) run --source $(PACKAGE_DIR) -m unittest "$(TEST_DIR)/$$synctest"; done $(COVERAGE) report -m endif endif .PHONY: htmlcov htmlcov: coverage ## Produce a coverage report $(COVERAGE) html # Documentation .PHONY: docs docs: venv ## Build the documentation rm -rf $(DOCS_DIR)/build @cd $(DOCS_DIR) && $(SPHINX_APIDOC) -f -e -o source/ $(CURDIR)/$(PACKAGE_DIR)/ @cd $(DOCS_DIR) && make html && make html .PHONY: release release: ## Make a release and upload it to pypi rm -rf dist scripts/git_tag.sh $(PYTHON) setup.py sdist bdist_wheel $(TWINE) upload dist/* .PHONY: all all: lint htmlcov ## Run all linting checks and unit tests and produce a coverage report ================================================ FILE: README.rst ================================================ adb\_shell ========== .. image:: https://travis-ci.com/JeffLIrion/adb_shell.svg?branch=master :target: https://travis-ci.com/JeffLIrion/adb_shell .. image:: https://coveralls.io/repos/github/JeffLIrion/adb_shell/badge.svg?branch=master :target: https://coveralls.io/github/JeffLIrion/adb_shell?branch=master .. image:: https://pepy.tech/badge/adb-shell :target: https://pepy.tech/project/adb-shell Documentation for this package can be found at https://adb-shell.readthedocs.io/. Prebuilt wheel can be downloaded from `nightly.link `_. This Python package implements ADB shell and FileSync functionality. It originated from `python-adb `_. Installation ------------ .. code-block:: pip install adb-shell Async ***** To utilize the async version of this code, you must install into a Python 3.7+ environment via: .. code-block:: pip install adb-shell[async] USB Support (Experimental) ************************** To connect to a device via USB, install this package via: .. code-block:: pip install adb-shell[usb] Example Usage ------------- (Based on `androidtv/adb_manager.py `_) .. code-block:: python from adb_shell.adb_device import AdbDeviceTcp, AdbDeviceUsb from adb_shell.auth.sign_pythonrsa import PythonRSASigner # Load the public and private keys adbkey = 'path/to/adbkey' with open(adbkey) as f: priv = f.read() with open(adbkey + '.pub') as f: pub = f.read() signer = PythonRSASigner(pub, priv) # Connect device1 = AdbDeviceTcp('192.168.0.222', 5555, default_transport_timeout_s=9.) device1.connect(rsa_keys=[signer], auth_timeout_s=0.1) # Connect via USB (package must be installed via `pip install adb-shell[usb])` device2 = AdbDeviceUsb() device2.connect(rsa_keys=[signer], auth_timeout_s=0.1) # Send a shell command response1 = device1.shell('echo TEST1') response2 = device2.shell('echo TEST2') Generate ADB Key Files ********************** If you need to generate a key, you can do so as follows. .. code-block:: python from adb_shell.auth.keygen import keygen keygen('path/to/adbkey') ================================================ FILE: adb_shell/__init__.py ================================================ # Copyright (c) 2021 Jeff Irion and contributors # # This file is part of the adb-shell package. """ADB shell functionality. """ __version__ = "0.4.4" ================================================ FILE: adb_shell/adb_device.py ================================================ # Copyright (c) 2021 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:`AdbDevice` class, which can connect to a device and run ADB shell commands. .. rubric:: Contents * :class:`_AdbIOManager` * :meth:`_AdbIOManager._read_bytes_from_device` * :meth:`_AdbIOManager._read_expected_packet_from_device` * :meth:`_AdbIOManager._read_packet_from_device` * :meth:`_AdbIOManager._send` * :meth:`_AdbIOManager.close` * :meth:`_AdbIOManager.connect` * :meth:`_AdbIOManager.read` * :meth:`_AdbIOManager.send` * :func:`_open_bytesio` * :class:`AdbDevice` * :meth:`AdbDevice._clse` * :meth:`AdbDevice._filesync_flush` * :meth:`AdbDevice._filesync_read` * :meth:`AdbDevice._filesync_read_buffered` * :meth:`AdbDevice._filesync_read_until` * :meth:`AdbDevice._filesync_send` * :meth:`AdbDevice._okay` * :meth:`AdbDevice._open` * :meth:`AdbDevice._pull` * :meth:`AdbDevice._push` * :meth:`AdbDevice._read_until` * :meth:`AdbDevice._read_until_close` * :meth:`AdbDevice._service` * :meth:`AdbDevice._streaming_command` * :meth:`AdbDevice._streaming_service` * :attr:`AdbDevice.available` * :meth:`AdbDevice.close` * :meth:`AdbDevice.connect` * :meth:`AdbDevice.list` * :attr:`AdbDevice.max_chunk_size` * :meth:`AdbDevice.pull` * :meth:`AdbDevice.push` * :meth:`AdbDevice.root` * :meth:`AdbDevice.shell` * :meth:`AdbDevice.stat` * :meth:`AdbDevice.streaming_shell` * :class:`AdbDeviceTcp` * :class:`AdbDeviceUsb` """ from contextlib import contextmanager from io import BytesIO import logging import os import struct import sys from threading import Lock import time from . import constants from . import exceptions from .adb_message import AdbMessage, checksum, int_to_cmd, unpack from .transport.base_transport import BaseTransport from .transport.tcp_transport import TcpTransport from .hidden_helpers import DeviceFile, _AdbPacketStore, _AdbTransactionInfo, _FileSyncTransactionInfo, get_banner, get_files_to_push try: from .transport.usb_transport import UsbTransport except (ImportError, OSError): UsbTransport = None _LOGGER = logging.getLogger(__name__) _DECODE_ERRORS = "backslashreplace" if sys.version_info[0] > 2 else "replace" @contextmanager def _open_bytesio(stream, *args, **kwargs): # pylint: disable=unused-argument """A context manager for a BytesIO object that does nothing. Parameters ---------- stream : BytesIO The BytesIO stream args : list Unused positional arguments kwargs : dict Unused keyword arguments Yields ------ stream : BytesIO The `stream` input parameter """ yield stream class _AdbIOManager(object): """A class for handling all ADB I/O. Notes ----- When the ``self._store_lock`` and ``self._transport_lock`` locks are held at the same time, it must always be the case that the ``self._transport_lock`` is acquired first. This ensures that there is no potential for deadlock. Parameters ---------- transport : BaseTransport A transport for communicating with the device; must be an instance of a subclass of :class:`~adb_shell.transport.base_transport.BaseTransport` Attributes ---------- _packet_store : _AdbPacketStore A store for holding packets that correspond to different ADB streams _store_lock : Lock A lock for protecting ``self._packet_store`` (this lock is never held for long) _transport : BaseTransport A transport for communicating with the device; must be an instance of a subclass of :class:`~adb_shell.transport.base_transport.BaseTransport` _transport_lock : Lock A lock for protecting ``self._transport`` """ def __init__(self, transport): self._packet_store = _AdbPacketStore() self._transport = transport self._store_lock = Lock() self._transport_lock = Lock() def close(self): """Close the connection via the provided transport's ``close()`` method and clear the packet store. """ with self._transport_lock: self._transport.close() with self._store_lock: self._packet_store.clear_all() def connect(self, banner, rsa_keys, auth_timeout_s, auth_callback, adb_info): """Establish an ADB connection to the device. 1. Use the transport to establish a connection 2. Send a ``b'CNXN'`` message 3. Read the response from the device 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 ``banner2`` that we received 1. If the last ``arg0`` was not :const:`adb_shell.constants.AUTH_TOKEN`, raise an exception 2. Sign the last ``banner2`` and send it in an ``b'AUTH'`` message 3. Read the response from the device 4. If ``cmd`` is ``b'CNXN'``, we are done 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 ---------- banner : bytearray, bytes The hostname of the machine where the Python interpreter is currently running (:attr:`adb_shell.adb_device.AdbDevice._banner`) 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` auth_timeout_s : float, None The time in seconds to wait for a ``b'CNXN'`` authentication response auth_callback : function, None Function callback invoked when the connection needs to be accepted on the device adb_info : _AdbTransactionInfo Info and settings for this connection attempt Returns ------- bool Whether the connection was established maxdata : int Maximum amount of data in an ADB packet Raises ------ adb_shell.exceptions.DeviceAuthError Device authentication required, no keys available adb_shell.exceptions.InvalidResponseError Invalid auth response from the device """ with self._transport_lock: # 0. Close the connection and clear the store self._transport.close() with self._store_lock: # We can release this lock because packets are only added to the store when the transport lock is held self._packet_store.clear_all() # 1. Use the transport to establish a connection self._transport.connect(adb_info.transport_timeout_s) # 2. Send a ``b'CNXN'`` message msg = AdbMessage(constants.CNXN, constants.VERSION, constants.MAX_ADB_DATA, b'host::%s\0' % banner) self._send(msg, adb_info) # 3. Read the response from the device cmd, arg0, maxdata, banner2 = self._read_expected_packet_from_device([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: return True, maxdata # 5. If no ``rsa_keys`` are provided, raise an exception if not rsa_keys: self._transport.close() raise exceptions.DeviceAuthError('Device authentication required, no keys available.') # 6. Loop through our keys, signing the last ``banner2`` 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: self._transport.close() raise exceptions.InvalidResponseError('Unknown AUTH response: %s %s %s' % (arg0, maxdata, banner2)) # 6.2. Sign the last ``banner2`` and send it in an ``b'AUTH'`` message signed_token = rsa_key.Sign(banner2) msg = AdbMessage(constants.AUTH, constants.AUTH_SIGNATURE, 0, signed_token) self._send(msg, adb_info) # 6.3. Read the response from the device cmd, arg0, maxdata, banner2 = self._read_expected_packet_from_device([constants.CNXN, constants.AUTH], adb_info) # 6.4. If ``cmd`` is ``b'CNXN'``, we are done if cmd == constants.CNXN: return True, maxdata # 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') self._send(msg, adb_info) adb_info.transport_timeout_s = auth_timeout_s _, _, maxdata, _ = self._read_expected_packet_from_device([constants.CNXN], adb_info) return True, maxdata def read(self, expected_cmds, adb_info, allow_zeros=False): """Read packets from the device until we get an expected packet type. 1. See if the expected packet is in the packet store 2. While the time limit has not been exceeded: 1. See if the expected packet is in the packet store 2. Read a packet from the device. If it matches what we are looking for, we are done. If it corresponds to a different stream, add it to the store. 3. Raise a timeout exception 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 allow_zeros : bool Whether to allow the received ``arg0`` and ``arg1`` values to match with 0, in addition to ``adb_info.remote_id`` and ``adb_info.local_id``, respectively Returns ------- cmd : 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 data : bytes The data that was read Raises ------ adb_shell.exceptions.AdbTimeoutError Never got one of the expected responses """ # First, try reading from the store. This way, you won't be waiting for the transport if it isn't needed with self._store_lock: # Recall that `arg0` from the device corresponds to `adb_info.remote_id` and `arg1` from the device corresponds to `adb_info.local_id` arg0_arg1 = self._packet_store.find(adb_info.remote_id, adb_info.local_id) if not allow_zeros else self._packet_store.find_allow_zeros(adb_info.remote_id, adb_info.local_id) while arg0_arg1: cmd, arg0, arg1, data = self._packet_store.get(arg0_arg1[0], arg0_arg1[1]) if cmd in expected_cmds: return cmd, arg0, arg1, data arg0_arg1 = self._packet_store.find(adb_info.remote_id, adb_info.local_id) if not allow_zeros else self._packet_store.find_allow_zeros(adb_info.remote_id, adb_info.local_id) # Start the timer start = time.time() while True: with self._transport_lock: # Try reading from the store (again) in case a packet got added while waiting to acquire the transport lock with self._store_lock: # Recall that `arg0` from the device corresponds to `adb_info.remote_id` and `arg1` from the device corresponds to `adb_info.local_id` arg0_arg1 = self._packet_store.find(adb_info.remote_id, adb_info.local_id) if not allow_zeros else self._packet_store.find_allow_zeros(adb_info.remote_id, adb_info.local_id) while arg0_arg1: cmd, arg0, arg1, data = self._packet_store.get(arg0_arg1[0], arg0_arg1[1]) if cmd in expected_cmds: return cmd, arg0, arg1, data arg0_arg1 = self._packet_store.find(adb_info.remote_id, adb_info.local_id) if not allow_zeros else self._packet_store.find_allow_zeros(adb_info.remote_id, adb_info.local_id) # Read from the device cmd, arg0, arg1, data = self._read_packet_from_device(adb_info) if not adb_info.args_match(arg0, arg1, allow_zeros): # The packet is not a match -> put it in the store with self._store_lock: self._packet_store.put(arg0, arg1, cmd, data) else: # The packet is a match for this `(adb_info.local_id, adb_info.remote_id)` pair if cmd == constants.CLSE: # Clear the entry in the store with self._store_lock: self._packet_store.clear(arg0, arg1) # If `cmd` is a match, then we are done if cmd in expected_cmds: return cmd, arg0, arg1, data # Check if time is up if time.time() - start > adb_info.read_timeout_s: break # Timeout raise exceptions.AdbTimeoutError("Never got one of the expected responses: {} (transport_timeout_s = {}, read_timeout_s = {})".format(expected_cmds, adb_info.transport_timeout_s, adb_info.read_timeout_s)) def send(self, msg, adb_info): """Send a message to the device. Parameters ---------- msg : AdbMessage The data that will be sent adb_info : _AdbTransactionInfo Info and settings for this ADB transaction """ with self._transport_lock: self._send(msg, adb_info) def _read_expected_packet_from_device(self, expected_cmds, adb_info): """Read packets from the device until we get an expected packet type. 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 ------- cmd : 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 data : bytes The data that was read Raises ------ adb_shell.exceptions.AdbTimeoutError Never got one of the expected responses """ start = time.time() while True: cmd, arg0, arg1, data = self._read_packet_from_device(adb_info) if cmd in expected_cmds: return cmd, arg0, arg1, data if time.time() - start > adb_info.read_timeout_s: # Timeout raise exceptions.AdbTimeoutError("Never got one of the expected responses: {} (transport_timeout_s = {}, read_timeout_s = {})".format(expected_cmds, adb_info.transport_timeout_s, adb_info.read_timeout_s)) def _read_bytes_from_device(self, length, adb_info): """Read ``length`` bytes from the device. Parameters ---------- length : int We will read packets until we get this length of data adb_info : _AdbTransactionInfo Info and settings for this ADB transaction Returns ------- bytes The data that was read Raises ------ adb_shell.exceptions.AdbTimeoutError Did not read ``length`` bytes in time """ start = time.time() data = bytearray() while length > 0: temp = self._transport.bulk_read(length, adb_info.transport_timeout_s) if temp: # Only log if `temp` is not empty _LOGGER.debug("bulk_read(%d): %.1000r", length, temp) data += temp length -= len(temp) if length == 0: break if time.time() - start > adb_info.read_timeout_s: # Timeout raise exceptions.AdbTimeoutError("Timeout: read {} of {} bytes (transport_timeout_s = {}, read_timeout_s = {})".format(len(data), len(data) + length, adb_info.transport_timeout_s, adb_info.read_timeout_s)) return bytes(data) def _read_packet_from_device(self, adb_info): """Read a complete ADB packet (header + data) from the device. Parameters ---------- adb_info : _AdbTransactionInfo Info and settings for this ADB transaction Returns ------- cmd : 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 adb_shell.exceptions.InvalidChecksumError Received checksum does not match the expected checksum """ msg = self._read_bytes_from_device(constants.MESSAGE_SIZE, adb_info) cmd, arg0, arg1, data_length, data_checksum = unpack(msg) command = constants.WIRE_TO_ID.get(cmd) if not command: raise exceptions.InvalidCommandError("Unknown command: %d = '%s' (arg0 = %d, arg1 = %d, msg = '%s')" % (cmd, int_to_cmd(cmd), arg0, arg1, msg)) if data_length == 0: return command, arg0, arg1, b"" data = self._read_bytes_from_device(data_length, adb_info) actual_checksum = checksum(data) if actual_checksum != data_checksum: raise exceptions.InvalidChecksumError("Received checksum {} != {}".format(actual_checksum, data_checksum)) return command, arg0, arg1, data 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 """ packed = msg.pack() _LOGGER.debug("bulk_write(%d): %r", len(packed), packed) self._transport.bulk_write(packed, adb_info.transport_timeout_s) if msg.data: _LOGGER.debug("bulk_write(%d): %r", len(msg.data), msg.data) self._transport.bulk_write(msg.data, adb_info.transport_timeout_s) class AdbDevice(object): """A class with methods for connecting to a device and executing ADB commands. Parameters ---------- transport : BaseTransport A user-provided transport for communicating with the device; must be an instance of a subclass of :class:`~adb_shell.transport.base_transport.BaseTransport` default_transport_timeout_s : float, None Default timeout in seconds for transport 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()`` Raises ------ adb_shell.exceptions.InvalidTransportError The passed ``transport`` is not an instance of a subclass of :class:`~adb_shell.transport.base_transport.BaseTransport` 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 _default_transport_timeout_s : float, None Default timeout in seconds for transport packets, or ``None`` _io_manager : _AdbIOManager Used for handling all ADB I/O _local_id : int The local ID that is used for ADB transactions; the value is incremented each time and is always in the range ``[1, 2^32)`` _local_id_lock : Lock A lock for protecting ``_local_id``; this is never held for long _maxdata: int Maximum amount of data in an ADB packet """ def __init__(self, transport, default_transport_timeout_s=None, banner=None): if banner and not isinstance(banner, (bytes, bytearray)): self._banner = bytearray(banner, 'utf-8') else: self._banner = banner if not isinstance(transport, BaseTransport): raise exceptions.InvalidTransportError("`transport` must be an instance of a subclass of `BaseTransport`") self._io_manager = _AdbIOManager(transport) self._available = False self._default_transport_timeout_s = default_transport_timeout_s self._local_id = 0 self._local_id_lock = Lock() self._maxdata = constants.MAX_PUSH_DATA # ======================================================================= # # # # Properties & simple methods # # # # ======================================================================= # @property def available(self): """Whether or not an ADB connection to the device has been established. Returns ------- bool ``self._available`` """ return self._available @property def max_chunk_size(self): """Maximum chunk size for filesync operations Returns ------- int Minimum value based on :const:`adb_shell.constants.MAX_CHUNK_SIZE` and ``_max_data / 2``, fallback to legacy :const:`adb_shell.constants.MAX_PUSH_DATA` """ return min(constants.MAX_CHUNK_SIZE, self._maxdata // 2) or constants.MAX_PUSH_DATA def _get_transport_timeout_s(self, transport_timeout_s): """Use the provided ``transport_timeout_s`` if it is not ``None``; otherwise, use ``self._default_transport_timeout_s`` Parameters ---------- transport_timeout_s : float, None The potential transport timeout Returns ------- float ``transport_timeout_s`` if it is not ``None``; otherwise, ``self._default_transport_timeout_s`` """ return transport_timeout_s if transport_timeout_s is not None else self._default_transport_timeout_s # ======================================================================= # # # # Close & Connect # # # # ======================================================================= # def close(self): """Close the connection via the provided transport's ``close()`` method. """ self._available = False self._io_manager.close() def connect(self, rsa_keys=None, transport_timeout_s=None, auth_timeout_s=constants.DEFAULT_AUTH_TIMEOUT_S, read_timeout_s=constants.DEFAULT_READ_TIMEOUT_S, auth_callback=None): """Establish an ADB connection to the device. See :meth:`_AdbIOManager.connect`. 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` transport_timeout_s : float, None Timeout in seconds for sending and receiving data, or ``None``; see :meth:`BaseTransport.bulk_read() ` and :meth:`BaseTransport.bulk_write() ` auth_timeout_s : float, None The time in seconds to wait for a ``b'CNXN'`` authentication response read_timeout_s : float The total time in seconds to wait for expected commands in :meth:`_AdbIOManager._read_expected_packet_from_device` 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:`AdbDevice.available`) """ # Get `self._banner` if it was not provided in the constructor if not self._banner: self._banner = get_banner() # Instantiate the `_AdbTransactionInfo` adb_info = _AdbTransactionInfo(None, None, self._get_transport_timeout_s(transport_timeout_s), read_timeout_s, None) # Mark the device as unavailable self._available = False # Use the IO manager to connect self._available, self._maxdata = self._io_manager.connect(self._banner, rsa_keys, auth_timeout_s, auth_callback, adb_info) return self._available # ======================================================================= # # # # Services # # # # ======================================================================= # def _service(self, service, command, transport_timeout_s=None, read_timeout_s=constants.DEFAULT_READ_TIMEOUT_S, timeout_s=None, 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 transport_timeout_s : float, None Timeout in seconds for sending and receiving data, or ``None``; see :meth:`BaseTransport.bulk_read() ` and :meth:`BaseTransport.bulk_write() ` read_timeout_s : float The total time in seconds to wait for a ``b'CLSE'`` or ``b'OKAY'`` command in :meth:`_AdbIOManager.read` timeout_s : float, None The total time in seconds to wait for the ADB command to finish 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. """ if decode: return b''.join(self._streaming_command(service, command, transport_timeout_s, read_timeout_s, timeout_s)).decode('utf8', _DECODE_ERRORS) return b''.join(self._streaming_command(service, command, transport_timeout_s, read_timeout_s, timeout_s)) def _streaming_service(self, service, command, transport_timeout_s=None, read_timeout_s=constants.DEFAULT_READ_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 transport_timeout_s : float, None Timeout in seconds for sending and receiving data, or ``None``; see :meth:`BaseTransport.bulk_read() ` and :meth:`BaseTransport.bulk_write() ` read_timeout_s : float The total time in seconds to wait for a ``b'CLSE'`` or ``b'OKAY'`` command in :meth:`_AdbIOManager.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. """ stream = self._streaming_command(service, command, transport_timeout_s, read_timeout_s, None) if decode: yield from (stream_line.decode('utf8', _DECODE_ERRORS) for stream_line in stream) else: yield from stream def exec_out(self, command, transport_timeout_s=None, read_timeout_s=constants.DEFAULT_READ_TIMEOUT_S, timeout_s=None, decode=True): """Send an ADB ``exec-out`` command to the device. https://www.linux-magazine.com/Issues/2017/195/Ask-Klaus Parameters ---------- command : str The exec-out command that will be sent transport_timeout_s : float, None Timeout in seconds for sending and receiving data, or ``None``; see :meth:`BaseTransport.bulk_read() ` and :meth:`BaseTransport.bulk_write() ` read_timeout_s : float The total time in seconds to wait for a ``b'CLSE'`` or ``b'OKAY'`` command in :meth:`_AdbIOManager.read` timeout_s : float, None The total time in seconds to wait for the ADB command to finish decode : bool Whether to decode the output to utf8 before returning Returns ------- bytes, str The output of the ADB exec-out 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 `AdbDevice.connect()`?)") return self._service(b'exec', command.encode('utf8'), transport_timeout_s, read_timeout_s, timeout_s, decode) def reboot(self, fastboot=False, transport_timeout_s=None, read_timeout_s=constants.DEFAULT_READ_TIMEOUT_S, timeout_s=None): """Reboot the device. Parameters ---------- fastboot : bool Whether to reboot the device into fastboot transport_timeout_s : float, None Timeout in seconds for sending and receiving data, or ``None``; see :meth:`BaseTransport.bulk_read() ` and :meth:`BaseTransport.bulk_write() ` read_timeout_s : float The total time in seconds to wait for a ``b'CLSE'`` or ``b'OKAY'`` command in :meth:`_AdbIOManager.read` timeout_s : float, None The total time in seconds to wait for the ADB command to finish """ if not self.available: raise exceptions.AdbConnectionError("ADB command not sent because a connection to the device has not been established. (Did you call `AdbDevice.connect()`?)") self._open(b'reboot:bootloader' if fastboot else b'reboot:', transport_timeout_s, read_timeout_s, timeout_s) def root(self, transport_timeout_s=None, read_timeout_s=constants.DEFAULT_READ_TIMEOUT_S, timeout_s=None): """Gain root access. The device must be rooted in order for this to work. Parameters ---------- transport_timeout_s : float, None Timeout in seconds for sending and receiving data, or ``None``; see :meth:`BaseTransport.bulk_read() ` and :meth:`BaseTransport.bulk_write() ` read_timeout_s : float The total time in seconds to wait for a ``b'CLSE'`` or ``b'OKAY'`` command in :meth:`_AdbIOManager.read` timeout_s : float, None The total time in seconds to wait for the ADB command to finish """ if not self.available: raise exceptions.AdbConnectionError("ADB command not sent because a connection to the device has not been established. (Did you call `AdbDevice.connect()`?)") self._service(b'root', b'', transport_timeout_s, read_timeout_s, timeout_s, False) def shell(self, command, transport_timeout_s=None, read_timeout_s=constants.DEFAULT_READ_TIMEOUT_S, timeout_s=None, decode=True): """Send an ADB shell command to the device. Parameters ---------- command : str The shell command that will be sent transport_timeout_s : float, None Timeout in seconds for sending and receiving data, or ``None``; see :meth:`BaseTransport.bulk_read() ` and :meth:`BaseTransport.bulk_write() ` read_timeout_s : float The total time in seconds to wait for a ``b'CLSE'`` or ``b'OKAY'`` command in :meth:`_AdbIOManager.read` timeout_s : float, None The total time in seconds to wait for the ADB command to finish 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 `AdbDevice.connect()`?)") return self._service(b'shell', command.encode('utf8'), transport_timeout_s, read_timeout_s, timeout_s, decode) def streaming_shell(self, command, transport_timeout_s=None, read_timeout_s=constants.DEFAULT_READ_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 transport_timeout_s : float, None Timeout in seconds for sending and receiving data, or ``None``; see :meth:`BaseTransport.bulk_read() ` and :meth:`BaseTransport.bulk_write() ` read_timeout_s : float The total time in seconds to wait for a ``b'CLSE'`` or ``b'OKAY'`` command in :meth:`_AdbIOManager.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 `AdbDevice.connect()`?)") yield from self._streaming_service(b'shell', command.encode('utf8'), transport_timeout_s, read_timeout_s, decode) # ======================================================================= # # # # FileSync # # # # ======================================================================= # def list(self, device_path, transport_timeout_s=None, read_timeout_s=constants.DEFAULT_READ_TIMEOUT_S): """Return a directory listing of the given path. Parameters ---------- device_path : str Directory to list. transport_timeout_s : float, None Expected timeout for any part of the pull. read_timeout_s : float The total time in seconds to wait for a ``b'CLSE'`` or ``b'OKAY'`` command in :meth:`_AdbIOManager.read` Returns ------- files : list[DeviceFile] Filename, mode, size, and mtime info for the files in the directory """ if not device_path: raise exceptions.DevicePathInvalidError("Cannot list an empty device path") if not self.available: raise exceptions.AdbConnectionError("ADB command not sent because a connection to the device has not been established. (Did you call `AdbDevice.connect()`?)") adb_info = self._open(b"sync:", transport_timeout_s, read_timeout_s, None) filesync_info = _FileSyncTransactionInfo(constants.FILESYNC_LIST_FORMAT, maxdata=self._maxdata) self._filesync_send(constants.LIST, adb_info, filesync_info, data=device_path) files = [] 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)) self._clse(adb_info) return files def pull(self, device_path, local_path, progress_callback=None, transport_timeout_s=None, read_timeout_s=constants.DEFAULT_READ_TIMEOUT_S): """Pull a file from the device. Parameters ---------- device_path : str The file on the device that will be pulled local_path : str, BytesIO The path or BytesIO stream where the file will be downloaded progress_callback : function, None Callback method that accepts ``device_path``, ``bytes_written``, and ``total_bytes`` transport_timeout_s : float, None Expected timeout for any part of the pull. read_timeout_s : float The total time in seconds to wait for a ``b'CLSE'`` or ``b'OKAY'`` command in :meth:`_AdbIOManager.read` """ if not device_path: raise exceptions.DevicePathInvalidError("Cannot pull from an empty device path") if not self.available: raise exceptions.AdbConnectionError("ADB command not sent because a connection to the device has not been established. (Did you call `AdbDevice.connect()`?)") opener = _open_bytesio if isinstance(local_path, BytesIO) else open with opener(local_path, 'wb') as stream: adb_info = self._open(b'sync:', transport_timeout_s, read_timeout_s, None) filesync_info = _FileSyncTransactionInfo(constants.FILESYNC_PULL_FORMAT, maxdata=self._maxdata) try: self._pull(device_path, stream, progress_callback, adb_info, filesync_info) finally: self._clse(adb_info) def _pull(self, device_path, stream, progress_callback, adb_info, filesync_info): """Pull a file from the device into the file-like ``local_path``. Parameters ---------- device_path : str The file on the device that will be pulled stream : _io.BytesIO File-like object for writing to progress_callback : function, None Callback method that accepts ``device_path``, ``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 = self.stat(device_path)[1] self._filesync_send(constants.RECV, adb_info, filesync_info, data=device_path) for cmd_id, _, data in self._filesync_read_until([constants.DATA], [constants.DONE], adb_info, filesync_info): if cmd_id == constants.DONE: break stream.write(data) if progress_callback: try: progress_callback(device_path, len(data), total_bytes) except: # noqa pylint: disable=bare-except pass def push(self, local_path, device_path, st_mode=constants.DEFAULT_PUSH_MODE, mtime=0, progress_callback=None, transport_timeout_s=None, read_timeout_s=constants.DEFAULT_READ_TIMEOUT_S): """Push a file or directory to the device. Parameters ---------- local_path : str, BytesIO A filename, directory, or BytesIO stream to push to the device device_path : str Destination on the device to write to. st_mode : int Stat mode for ``local_path`` mtime : int Modification time to set on the file. progress_callback : function, None Callback method that accepts ``device_path``, ``bytes_written``, and ``total_bytes`` transport_timeout_s : float, None Expected timeout for any part of the push. read_timeout_s : float The total time in seconds to wait for a ``b'CLSE'`` or ``b'OKAY'`` command in :meth:`_AdbIOManager.read` """ if not device_path: raise exceptions.DevicePathInvalidError("Cannot push to an empty device path") if not self.available: raise exceptions.AdbConnectionError("ADB command not sent because a connection to the device has not been established. (Did you call `AdbDevice.connect()`?)") local_path_is_dir, local_paths, device_paths = get_files_to_push(local_path, device_path) if local_path_is_dir: self.shell("mkdir " + device_path, transport_timeout_s, read_timeout_s) for _local_path, _device_path in zip(local_paths, device_paths): opener = _open_bytesio if isinstance(local_path, BytesIO) else open with opener(_local_path, 'rb') as stream: adb_info = self._open(b'sync:', transport_timeout_s, read_timeout_s, None) filesync_info = _FileSyncTransactionInfo(constants.FILESYNC_PUSH_FORMAT, maxdata=self._maxdata) self._push(stream, _device_path, st_mode, mtime, progress_callback, adb_info, filesync_info) self._clse(adb_info) def _push(self, stream, device_path, st_mode, mtime, progress_callback, adb_info, filesync_info): """Push a file-like object to the device. Parameters ---------- stream : _io.BytesIO File-like object for reading from device_path : str Destination on the device to write to st_mode : int Stat mode for the file mtime : int Modification time progress_callback : function, None Callback method that accepts ``device_path``, ``bytes_written``, and ``total_bytes`` adb_info : _AdbTransactionInfo Info and settings for this ADB transaction Raises ------ PushFailedError Raised on push failure. """ fileinfo = ('{},{}'.format(device_path, int(st_mode))).encode('utf-8') self._filesync_send(constants.SEND, adb_info, filesync_info, data=fileinfo) if progress_callback: total_bytes = os.fstat(stream.fileno()).st_size while True: data = stream.read(self.max_chunk_size) if data: self._filesync_send(constants.DATA, adb_info, filesync_info, data=data) if progress_callback: try: progress_callback(device_path, len(data), total_bytes) except: # noqa pylint: disable=bare-except pass 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. self._filesync_send(constants.DONE, adb_info, filesync_info, size=mtime) 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) def stat(self, device_path, transport_timeout_s=None, read_timeout_s=constants.DEFAULT_READ_TIMEOUT_S): """Get a file's ``stat()`` information. Parameters ---------- device_path : str The file on the device for which we will get information. transport_timeout_s : float, None Expected timeout for any part of the pull. read_timeout_s : float The total time in seconds to wait for a ``b'CLSE'`` or ``b'OKAY'`` command in :meth:`_AdbIOManager.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 device_path: raise exceptions.DevicePathInvalidError("Cannot stat an empty device path") if not self.available: raise exceptions.AdbConnectionError("ADB command not sent because a connection to the device has not been established. (Did you call `AdbDevice.connect()`?)") adb_info = self._open(b'sync:', transport_timeout_s, read_timeout_s, None) filesync_info = _FileSyncTransactionInfo(constants.FILESYNC_STAT_FORMAT, maxdata=self._maxdata) self._filesync_send(constants.STAT, adb_info, filesync_info, data=device_path) _, (mode, size, mtime), _ = self._filesync_read([constants.STAT], adb_info, filesync_info) self._clse(adb_info) return mode, size, mtime # ======================================================================= # # # # Hidden Methods: send packets # # # # ======================================================================= # def _clse(self, adb_info): """Send a ``b'CLSE'`` message and then read a ``b'CLSE'`` message. .. warning:: This is not to be confused with the :meth:`AdbDevice.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) self._io_manager.send(msg, adb_info) self._read_until([constants.CLSE], adb_info) 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) self._io_manager.send(msg, adb_info) # ======================================================================= # # # # Hidden Methods # # # # ======================================================================= # def _open(self, destination, transport_timeout_s, read_timeout_s, timeout_s): """Opens a new connection to the device via an ``b'OPEN'`` message. 1. :meth:`~_AdbIOManager.send` an ``b'OPEN'`` command to the device that specifies the ``local_id`` 2. :meth:`~_AdbIOManager.read` the response from the device and fill in the ``adb_info.remote_id`` attribute Parameters ---------- destination : bytes ``b'SERVICE:COMMAND'`` transport_timeout_s : float, None Timeout in seconds for sending and receiving data, or ``None``; see :meth:`BaseTransport.bulk_read() ` and :meth:`BaseTransport.bulk_write() ` read_timeout_s : float The total time in seconds to wait for a ``b'CLSE'`` or ``b'OKAY'`` command in :meth:`_AdbIOManager.read` timeout_s : float, None The total time in seconds to wait for the ADB command to finish Returns ------- adb_info : _AdbTransactionInfo Info and settings for this ADB transaction """ with self._local_id_lock: self._local_id += 1 if self._local_id == 2**32: self._local_id = 1 adb_info = _AdbTransactionInfo(self._local_id, None, self._get_transport_timeout_s(transport_timeout_s), read_timeout_s, timeout_s) msg = AdbMessage(constants.OPEN, adb_info.local_id, 0, destination + b'\0') self._io_manager.send(msg, adb_info) _, adb_info.remote_id, _, _ = self._io_manager.read([constants.OKAY], adb_info) return adb_info def _read_until(self, expected_cmds, adb_info): """Read a packet, acknowledging any write packets. 1. Read data via :meth:`_AdbIOManager.read` 2. If a ``b'WRTE'`` packet is received, send an ``b'OKAY'`` packet via :meth:`AdbDevice._okay` 3. Return the ``cmd`` and ``data`` that were read by :meth:`_AdbIOManager.read` Parameters ---------- expected_cmds : list[bytes] :meth:`_AdbIOManager.read` will 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:`_AdbIOManager.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:`_AdbIOManager.read` """ cmd, _, _, data = self._io_manager.read(expected_cmds, adb_info, allow_zeros=True) # Acknowledge write packets if cmd == constants.WRTE: self._okay(adb_info) return cmd, data 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:`AdbDevice._read_until` 2. If ``cmd`` is ``b'CLSE'``, then send a ``b'CLSE'`` message and stop 3. 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:`AdbDevice._read_until` """ start = time.time() while True: cmd, data = 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) self._io_manager.send(msg, adb_info) break yield data # Make sure the ADB command has not timed out if adb_info.timeout_s is not None and time.time() - start > adb_info.timeout_s: raise exceptions.AdbTimeoutError("The command did not complete within {} seconds".format(adb_info.timeout_s)) def _streaming_command(self, service, command, transport_timeout_s, read_timeout_s, timeout_s): """One complete set of packets for a single command. 1. :meth:`~AdbDevice._open` a new connection to the device, where the ``destination`` parameter is ``service:command`` 2. Read the response data via :meth:`AdbDevice._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:`AdbDevice.shell`) command : bytes The service command transport_timeout_s : float, None Timeout in seconds for sending and receiving data, or ``None``; see :meth:`BaseTransport.bulk_read() ` and :meth:`BaseTransport.bulk_write() ` read_timeout_s : float The total time in seconds to wait for a ``b'CLSE'`` or ``b'OKAY'`` command in :meth:`_AdbIOManager.read` timeout_s : float, None The total time in seconds to wait for the ADB command to finish Yields ------ bytes The responses from the service. """ adb_info = self._open(b'%s:%s' % (service, command), transport_timeout_s, read_timeout_s, timeout_s) yield from self._read_until_close(adb_info) # ======================================================================= # # # # FileSync Hidden Methods # # # # ======================================================================= # 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 """ # Send the buffer msg = AdbMessage(constants.WRTE, adb_info.local_id, adb_info.remote_id, filesync_info.send_buffer[:filesync_info.send_idx]) self._io_manager.send(msg, adb_info) # Expect an 'OKAY' in response self._read_until([constants.OKAY], adb_info) # Reset the send index filesync_info.send_idx = 0 def _filesync_read(self, expected_ids, adb_info, filesync_info): """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 Returns ------- command_id : bytes The received header ID tuple The contents of the header data : bytearray, None The received data, or ``None`` if the command ID is :const:`adb_shell.constants.STAT` Raises ------ adb_shell.exceptions.AdbCommandFailureException Command failed adb_shell.exceptions.InvalidResponseError Received response was not in ``expected_ids`` """ if filesync_info.send_idx: self._filesync_flush(adb_info, filesync_info) # Read one filesync packet off the recv buffer. header_data = 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]] # Whether there is data to read read_data = command_id != constants.STAT if read_data: # Header is (ID, ..., size) --> read the data data = self._filesync_read_buffered(header[-1], adb_info, filesync_info) else: # No data to be read data = bytearray() if command_id not in expected_ids: if command_id == constants.FAIL: reason = data.decode('utf-8', errors=_DECODE_ERRORS) 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 return command_id, header[1:-1], data 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 = 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 def _filesync_read_until(self, expected_ids, finish_ids, adb_info, filesync_info): """Useful wrapper around :meth:`AdbDevice._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 = 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 def _filesync_send(self, command_id, adb_info, filesync_info, data=b'', size=None): """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, None Optionally override size from len(data). """ if not isinstance(data, bytes): data = data.encode('utf8') if size is None: size = len(data) if not filesync_info.can_add_to_send_buffer(len(data)): 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) class AdbDeviceTcp(AdbDevice): """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_transport_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 _default_transport_timeout_s : float, None Default timeout in seconds for TCP packets, or ``None`` _local_id : int The local ID that is used for ADB transactions; the value is incremented each time and is always in the range ``[1, 2^32)`` _maxdata : int Maximum amount of data in an ADB packet _transport : TcpTransport The transport that is used to connect to the device """ def __init__(self, host, port=5555, default_transport_timeout_s=None, banner=None): transport = TcpTransport(host, port) super(AdbDeviceTcp, self).__init__(transport, default_transport_timeout_s, banner) class AdbDeviceUsb(AdbDevice): """A class with methods for connecting to a device via USB and executing ADB commands. Parameters ---------- serial : str, None The USB device serial ID port_path : TODO, None TODO default_transport_timeout_s : float, None Default timeout in seconds for USB 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()`` Raises ------ adb_shell.exceptions.InvalidTransportError Raised if package was not installed with the "usb" extras option (``pip install adb-shell[usb]``) 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 _default_transport_timeout_s : float, None Default timeout in seconds for USB packets, or ``None`` _local_id : int The local ID that is used for ADB transactions; the value is incremented each time and is always in the range ``[1, 2^32)`` _maxdata : int Maximum amount of data in an ADB packet _transport : UsbTransport The transport that is used to connect to the device """ def __init__(self, serial=None, port_path=None, default_transport_timeout_s=None, banner=None): if UsbTransport is None: raise exceptions.InvalidTransportError("To enable USB support you must install this package via `pip install adb-shell[usb]`") transport = UsbTransport.find_adb(serial, port_path, default_transport_timeout_s) super(AdbDeviceUsb, self).__init__(transport, default_transport_timeout_s, banner) ================================================ FILE: adb_shell/adb_device_async.py ================================================ # Copyright (c) 2021 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:`_AdbIOManagerAsync` * :meth:`_AdbIOManagerAsync._read_bytes_from_device` * :meth:`_AdbIOManagerAsync._read_expected_packet_from_device` * :meth:`_AdbIOManagerAsync._read_packet_from_device` * :meth:`_AdbIOManagerAsync._send` * :meth:`_AdbIOManagerAsync.close` * :meth:`_AdbIOManagerAsync.connect` * :meth:`_AdbIOManagerAsync.read` * :meth:`_AdbIOManagerAsync.send` * :class:`_AsyncBytesIO` * :meth:`_AsyncBytesIO.read` * :meth:`_AsyncBytesIO.write` * :func:`_open_bytesio` * :class:`AdbDeviceAsync` * :meth:`AdbDeviceAsync._clse` * :meth:`AdbDeviceAsync._filesync_flush` * :meth:`AdbDeviceAsync._filesync_read` * :meth:`AdbDeviceAsync._filesync_read_buffered` * :meth:`AdbDeviceAsync._filesync_read_until` * :meth:`AdbDeviceAsync._filesync_send` * :meth:`AdbDeviceAsync._okay` * :meth:`AdbDeviceAsync._open` * :meth:`AdbDeviceAsync._pull` * :meth:`AdbDeviceAsync._push` * :meth:`AdbDeviceAsync._read_until` * :meth:`AdbDeviceAsync._read_until_close` * :meth:`AdbDeviceAsync._service` * :meth:`AdbDeviceAsync._streaming_command` * :meth:`AdbDeviceAsync._streaming_service` * :attr:`AdbDeviceAsync.available` * :meth:`AdbDeviceAsync.close` * :meth:`AdbDeviceAsync.connect` * :meth:`AdbDeviceAsync.list` * :attr:`AdbDeviceAsync.max_chunk_size` * :meth:`AdbDeviceAsync.pull` * :meth:`AdbDeviceAsync.push` * :meth:`AdbDeviceAsync.root` * :meth:`AdbDeviceAsync.shell` * :meth:`AdbDeviceAsync.stat` * :meth:`AdbDeviceAsync.streaming_shell` * :class:`AdbDeviceTcpAsync` """ from contextlib import asynccontextmanager from io import BytesIO from asyncio import Lock, get_running_loop import logging import os import struct import time import aiofiles from . import constants from . import exceptions from .adb_message import AdbMessage, checksum, int_to_cmd, unpack from .transport.base_transport_async import BaseTransportAsync from .transport.tcp_transport_async import TcpTransportAsync from .hidden_helpers import DeviceFile, _AdbPacketStore, _AdbTransactionInfo, _FileSyncTransactionInfo, get_banner, get_files_to_push _LOGGER = logging.getLogger(__name__) class _AsyncBytesIO: """An async wrapper for `BytesIO`. Parameters ---------- bytesio : BytesIO The BytesIO object that is wrapped """ def __init__(self, bytesio): self._bytesio = bytesio async def read(self, size=-1): """Read data. Parameters ---------- size : int The size of the data to be read Returns ------- bytes The data that was read """ return self._bytesio.read(size) async def write(self, data): """Write data. Parameters ---------- data : bytes The data to be written """ self._bytesio.write(data) @asynccontextmanager async def _open_bytesio(stream, *args, **kwargs): # pylint: disable=unused-argument """An async context manager for a BytesIO object that does nothing. Parameters ---------- stream : BytesIO The BytesIO stream args : list Unused positional arguments kwargs : dict Unused keyword arguments Yields ------ _AsyncBytesIO The wrapped `stream` input parameter """ yield _AsyncBytesIO(stream) class _AdbIOManagerAsync(object): """A class for handling all ADB I/O. Notes ----- When the ``self._store_lock`` and ``self._transport_lock`` locks are held at the same time, it must always be the case that the ``self._transport_lock`` is acquired first. This ensures that there is no potential for deadlock. Parameters ---------- transport : BaseTransportAsync A transport for communicating with the device; must be an instance of a subclass of :class:`~adb_shell.transport.base_transport_async.BaseTransportAsync` Attributes ---------- _packet_store : _AdbPacketStore A store for holding packets that correspond to different ADB streams _store_lock : Lock A lock for protecting ``self._packet_store`` (this lock is never held for long) _transport : BaseTransportAsync A transport for communicating with the device; must be an instance of a subclass of :class:`~adb_shell.transport.base_transport_async.BaseTransportAsync` _transport_lock : Lock A lock for protecting ``self._transport`` """ def __init__(self, transport): self._packet_store = _AdbPacketStore() self._transport = transport self._store_lock = Lock() self._transport_lock = Lock() async def close(self): """Close the connection via the provided transport's ``close()`` method and clear the packet store. """ async with self._transport_lock: await self._transport.close() async with self._store_lock: self._packet_store.clear_all() async def connect(self, banner, rsa_keys, auth_timeout_s, auth_callback, adb_info): """Establish an ADB connection to the device. 1. Use the transport to establish a connection 2. Send a ``b'CNXN'`` message 3. Read the response from the device 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 ``banner2`` that we received 1. If the last ``arg0`` was not :const:`adb_shell.constants.AUTH_TOKEN`, raise an exception 2. Sign the last ``banner2`` and send it in an ``b'AUTH'`` message 3. Read the response from the device 4. If ``cmd`` is ``b'CNXN'``, we are done 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 ---------- banner : bytearray, bytes The hostname of the machine where the Python interpreter is currently running (:attr:`adb_shell.adb_device_async.AdbDeviceAsync._banner`) 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` auth_timeout_s : float, None The time in seconds to wait for a ``b'CNXN'`` authentication response auth_callback : function, None Function callback invoked when the connection needs to be accepted on the device adb_info : _AdbTransactionInfo Info and settings for this connection attempt Returns ------- bool Whether the connection was established maxdata : int Maximum amount of data in an ADB packet Raises ------ adb_shell.exceptions.DeviceAuthError Device authentication required, no keys available adb_shell.exceptions.InvalidResponseError Invalid auth response from the device """ async with self._transport_lock: # 0. Close the connection and clear the store await self._transport.close() async with self._store_lock: # We can release this lock because packets are only added to the store when the transport lock is held self._packet_store.clear_all() # 1. Use the transport to establish a connection await self._transport.connect(adb_info.transport_timeout_s) # 2. Send a ``b'CNXN'`` message msg = AdbMessage(constants.CNXN, constants.VERSION, constants.MAX_ADB_DATA, b'host::%s\0' % banner) await self._send(msg, adb_info) # 3. Read the response from the device cmd, arg0, maxdata, banner2 = await self._read_expected_packet_from_device([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: return True, maxdata # 5. If no ``rsa_keys`` are provided, raise an exception if not rsa_keys: await self._transport.close() raise exceptions.DeviceAuthError('Device authentication required, no keys available.') # 6. Loop through our keys, signing the last ``banner2`` 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._transport.close() raise exceptions.InvalidResponseError('Unknown AUTH response: %s %s %s' % (arg0, maxdata, banner2)) # 6.2. Sign the last ``banner2`` and send it in an ``b'AUTH'`` message signed_token = rsa_key.Sign(banner2) msg = AdbMessage(constants.AUTH, constants.AUTH_SIGNATURE, 0, signed_token) await self._send(msg, adb_info) # 6.3. Read the response from the device cmd, arg0, maxdata, banner2 = await self._read_expected_packet_from_device([constants.CNXN, constants.AUTH], adb_info) # 6.4. If ``cmd`` is ``b'CNXN'``, we are done if cmd == constants.CNXN: return True, maxdata # 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.transport_timeout_s = auth_timeout_s _, _, maxdata, _ = await self._read_expected_packet_from_device([constants.CNXN], adb_info) return True, maxdata async def read(self, expected_cmds, adb_info, allow_zeros=False): """Read packets from the device until we get an expected packet type. 1. See if the expected packet is in the packet store 2. While the time limit has not been exceeded: 1. See if the expected packet is in the packet store 2. Read a packet from the device. If it matches what we are looking for, we are done. If it corresponds to a different stream, add it to the store. 3. Raise a timeout exception 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 allow_zeros : bool Whether to allow the received ``arg0`` and ``arg1`` values to match with 0, in addition to ``adb_info.remote_id`` and ``adb_info.local_id``, respectively Returns ------- cmd : 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 data : bytes The data that was read Raises ------ adb_shell.exceptions.AdbTimeoutError Never got one of the expected responses """ # First, try reading from the store. This way, you won't be waiting for the transport if it isn't needed async with self._store_lock: # Recall that `arg0` from the device corresponds to `adb_info.remote_id` and `arg1` from the device corresponds to `adb_info.local_id` arg0_arg1 = self._packet_store.find(adb_info.remote_id, adb_info.local_id) if not allow_zeros else self._packet_store.find_allow_zeros(adb_info.remote_id, adb_info.local_id) while arg0_arg1: cmd, arg0, arg1, data = self._packet_store.get(arg0_arg1[0], arg0_arg1[1]) if cmd in expected_cmds: return cmd, arg0, arg1, data arg0_arg1 = self._packet_store.find(adb_info.remote_id, adb_info.local_id) if not allow_zeros else self._packet_store.find_allow_zeros(adb_info.remote_id, adb_info.local_id) # Start the timer start = time.time() while True: async with self._transport_lock: # Try reading from the store (again) in case a packet got added while waiting to acquire the transport lock async with self._store_lock: # Recall that `arg0` from the device corresponds to `adb_info.remote_id` and `arg1` from the device corresponds to `adb_info.local_id` arg0_arg1 = self._packet_store.find(adb_info.remote_id, adb_info.local_id) if not allow_zeros else self._packet_store.find_allow_zeros(adb_info.remote_id, adb_info.local_id) while arg0_arg1: cmd, arg0, arg1, data = self._packet_store.get(arg0_arg1[0], arg0_arg1[1]) if cmd in expected_cmds: return cmd, arg0, arg1, data arg0_arg1 = self._packet_store.find(adb_info.remote_id, adb_info.local_id) if not allow_zeros else self._packet_store.find_allow_zeros(adb_info.remote_id, adb_info.local_id) # Read from the device cmd, arg0, arg1, data = await self._read_packet_from_device(adb_info) if not adb_info.args_match(arg0, arg1, allow_zeros): # The packet is not a match -> put it in the store async with self._store_lock: self._packet_store.put(arg0, arg1, cmd, data) else: # The packet is a match for this `(adb_info.local_id, adb_info.remote_id)` pair if cmd == constants.CLSE: # Clear the entry in the store async with self._store_lock: self._packet_store.clear(arg0, arg1) # If `cmd` is a match, then we are done if cmd in expected_cmds: return cmd, arg0, arg1, data # Check if time is up if time.time() - start > adb_info.read_timeout_s: break # Timeout raise exceptions.AdbTimeoutError("Never got one of the expected responses: {} (transport_timeout_s = {}, read_timeout_s = {})".format(expected_cmds, adb_info.transport_timeout_s, adb_info.read_timeout_s)) async def send(self, msg, adb_info): """Send a message to the device. Parameters ---------- msg : AdbMessage The data that will be sent adb_info : _AdbTransactionInfo Info and settings for this ADB transaction """ async with self._transport_lock: await self._send(msg, adb_info) async def _read_expected_packet_from_device(self, expected_cmds, adb_info): """Read packets from the device until we get an expected packet type. 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 ------- cmd : 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 data : bytes The data that was read Raises ------ adb_shell.exceptions.AdbTimeoutError Never got one of the expected responses """ start = time.time() while True: cmd, arg0, arg1, data = await self._read_packet_from_device(adb_info) if cmd in expected_cmds: return cmd, arg0, arg1, data if time.time() - start > adb_info.read_timeout_s: # Timeout raise exceptions.AdbTimeoutError("Never got one of the expected responses: {} (transport_timeout_s = {}, read_timeout_s = {})".format(expected_cmds, adb_info.transport_timeout_s, adb_info.read_timeout_s)) async def _read_bytes_from_device(self, length, adb_info): """Read ``length`` bytes from the device. Parameters ---------- length : int We will read packets until we get this length of data adb_info : _AdbTransactionInfo Info and settings for this ADB transaction Returns ------- bytes The data that was read Raises ------ adb_shell.exceptions.AdbTimeoutError Did not read ``length`` bytes in time """ start = time.time() data = bytearray() while length > 0: temp = await self._transport.bulk_read(length, adb_info.transport_timeout_s) if temp: # Only log if `temp` is not empty _LOGGER.debug("bulk_read(%d): %.1000r", length, temp) data += temp length -= len(temp) if length == 0: break if time.time() - start > adb_info.read_timeout_s: # Timeout raise exceptions.AdbTimeoutError("Timeout: read {} of {} bytes (transport_timeout_s = {}, read_timeout_s = {})".format(len(data), len(data) + length, adb_info.transport_timeout_s, adb_info.read_timeout_s)) return bytes(data) async def _read_packet_from_device(self, adb_info): """Read a complete ADB packet (header + data) from the device. Parameters ---------- adb_info : _AdbTransactionInfo Info and settings for this ADB transaction Returns ------- cmd : 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 adb_shell.exceptions.InvalidChecksumError Received checksum does not match the expected checksum """ msg = await self._read_bytes_from_device(constants.MESSAGE_SIZE, adb_info) cmd, arg0, arg1, data_length, data_checksum = unpack(msg) command = constants.WIRE_TO_ID.get(cmd) if not command: raise exceptions.InvalidCommandError("Unknown command: %d = '%s' (arg0 = %d, arg1 = %d, msg = '%s')" % (cmd, int_to_cmd(cmd), arg0, arg1, msg)) if data_length == 0: return command, arg0, arg1, b"" data = await self._read_bytes_from_device(data_length, adb_info) actual_checksum = checksum(data) if actual_checksum != data_checksum: raise exceptions.InvalidChecksumError("Received checksum {} != {}".format(actual_checksum, data_checksum)) return command, arg0, arg1, 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 """ packed = msg.pack() _LOGGER.debug("bulk_write(%d): %r", len(packed), packed) await self._transport.bulk_write(packed, adb_info.transport_timeout_s) if msg.data: _LOGGER.debug("bulk_write(%d): %r", len(msg.data), msg.data) await self._transport.bulk_write(msg.data, adb_info.transport_timeout_s) class AdbDeviceAsync(object): """A class with methods for connecting to a device and executing ADB commands. Parameters ---------- transport : BaseTransportAsync A user-provided transport for communicating with the device; must be an instance of a subclass of :class:`~adb_shell.transport.base_transport_async.BaseTransportAsync` default_transport_timeout_s : float, None Default timeout in seconds for transport 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()`` Raises ------ adb_shell.exceptions.InvalidTransportError The passed ``transport`` is not an instance of a subclass of :class:`~adb_shell.transport.base_transport_async.BaseTransportAsync` 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 _default_transport_timeout_s : float, None Default timeout in seconds for transport packets, or ``None`` _io_manager : _AdbIOManagerAsync Used for handling all ADB I/O _local_id : int The local ID that is used for ADB transactions; the value is incremented each time and is always in the range ``[1, 2^32)`` _local_id_lock : Lock A lock for protecting ``_local_id``; this is never held for long _maxdata: int Maximum amount of data in an ADB packet _transport : BaseTransportAsync The transport that is used to connect to the device; must be a subclass of :class:`~adb_shell.transport.base_transport_async.BaseTransportAsync` """ def __init__(self, transport, default_transport_timeout_s=None, banner=None): if banner and not isinstance(banner, (bytes, bytearray)): self._banner = bytearray(banner, 'utf-8') else: self._banner = banner if not isinstance(transport, BaseTransportAsync): raise exceptions.InvalidTransportError("`transport` must be an instance of a subclass of `BaseTransportAsync`") self._io_manager = _AdbIOManagerAsync(transport) self._available = False self._default_transport_timeout_s = default_transport_timeout_s self._local_id = 0 self._local_id_lock = Lock() self._maxdata = constants.MAX_PUSH_DATA # ======================================================================= # # # # Properties & simple methods # # # # ======================================================================= # @property def available(self): """Whether or not an ADB connection to the device has been established. Returns ------- bool ``self._available`` """ return self._available @property def max_chunk_size(self): """Maximum chunk size for filesync operations Returns ------- int Minimum value based on :const:`adb_shell.constants.MAX_CHUNK_SIZE` and ``_max_data / 2``, fallback to legacy :const:`adb_shell.constants.MAX_PUSH_DATA` """ return min(constants.MAX_CHUNK_SIZE, self._maxdata // 2) or constants.MAX_PUSH_DATA def _get_transport_timeout_s(self, transport_timeout_s): """Use the provided ``transport_timeout_s`` if it is not ``None``; otherwise, use ``self._default_transport_timeout_s`` Parameters ---------- transport_timeout_s : float, None The potential transport timeout Returns ------- float ``transport_timeout_s`` if it is not ``None``; otherwise, ``self._default_transport_timeout_s`` """ return transport_timeout_s if transport_timeout_s is not None else self._default_transport_timeout_s # ======================================================================= # # # # Close & Connect # # # # ======================================================================= # async def close(self): """Close the connection via the provided transport's ``close()`` method. """ self._available = False await self._io_manager.close() async def connect(self, rsa_keys=None, transport_timeout_s=None, auth_timeout_s=constants.DEFAULT_AUTH_TIMEOUT_S, read_timeout_s=constants.DEFAULT_READ_TIMEOUT_S, auth_callback=None): """Establish an ADB connection to the device. See :meth:`_AdbIOManagerAsync.connect`. 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` transport_timeout_s : float, None Timeout in seconds for sending and receiving data, or ``None``; see :meth:`BaseTransportAsync.bulk_read() ` and :meth:`BaseTransportAsync.bulk_write() ` auth_timeout_s : float, None The time in seconds to wait for a ``b'CNXN'`` authentication response read_timeout_s : float The total time in seconds to wait for expected commands in :meth:`_AdbIOManagerAsync._read_expected_packet_from_device` 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`) """ # Get `self._banner` if it was not provided in the constructor if not self._banner: self._banner = await get_running_loop().run_in_executor(None, get_banner) # Instantiate the `_AdbTransactionInfo` adb_info = _AdbTransactionInfo(None, None, self._get_transport_timeout_s(transport_timeout_s), read_timeout_s, None) # Mark the device as unavailable self._available = False # Use the IO manager to connect self._available, self._maxdata = await self._io_manager.connect(self._banner, rsa_keys, auth_timeout_s, auth_callback, adb_info) return self._available # ======================================================================= # # # # Services # # # # ======================================================================= # async def _service(self, service, command, transport_timeout_s=None, read_timeout_s=constants.DEFAULT_READ_TIMEOUT_S, timeout_s=None, 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 transport_timeout_s : float, None Timeout in seconds for sending and receiving data, or ``None``; see :meth:`BaseTransportAsync.bulk_read() ` and :meth:`BaseTransportAsync.bulk_write() ` read_timeout_s : float The total time in seconds to wait for a ``b'CLSE'`` or ``b'OKAY'`` command in :meth:`_AdbIOManagerAsync.read` timeout_s : float, None The total time in seconds to wait for the ADB command to finish 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. """ if decode: return b''.join([x async for x in self._streaming_command(service, command, transport_timeout_s, read_timeout_s, timeout_s)]).decode('utf8', 'backslashreplace') return b''.join([x async for x in self._streaming_command(service, command, transport_timeout_s, read_timeout_s, timeout_s)]) async def _streaming_service(self, service, command, transport_timeout_s=None, read_timeout_s=constants.DEFAULT_READ_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 transport_timeout_s : float, None Timeout in seconds for sending and receiving data, or ``None``; see :meth:`BaseTransportAsync.bulk_read() ` and :meth:`BaseTransportAsync.bulk_write() ` read_timeout_s : float The total time in seconds to wait for a ``b'CLSE'`` or ``b'OKAY'`` command in :meth:`_AdbIOManagerAsync.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. """ stream = self._streaming_command(service, command, transport_timeout_s, read_timeout_s, None) if decode: async for line in (stream_line.decode('utf8', 'backslashreplace') async for stream_line in stream): yield line else: async for line in stream: yield line async def exec_out(self, command, transport_timeout_s=None, read_timeout_s=constants.DEFAULT_READ_TIMEOUT_S, timeout_s=None, decode=True): """Send an ADB ``exec-out`` command to the device. https://www.linux-magazine.com/Issues/2017/195/Ask-Klaus Parameters ---------- command : str The exec-out command that will be sent transport_timeout_s : float, None Timeout in seconds for sending and receiving data, or ``None``; see :meth:`BaseTransportAsync.bulk_read() ` and :meth:`BaseTransportAsync.bulk_write() ` read_timeout_s : float The total time in seconds to wait for a ``b'CLSE'`` or ``b'OKAY'`` command in :meth:`_AdbIOManagerAsync.read` timeout_s : float, None The total time in seconds to wait for the ADB command to finish decode : bool Whether to decode the output to utf8 before returning Returns ------- bytes, str The output of the ADB exec-out 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'exec', command.encode('utf8'), transport_timeout_s, read_timeout_s, timeout_s, decode) async def reboot(self, fastboot=False, transport_timeout_s=None, read_timeout_s=constants.DEFAULT_READ_TIMEOUT_S, timeout_s=None): """Reboot the device. Parameters ---------- fastboot : bool Whether to reboot the device into fastboot transport_timeout_s : float, None Timeout in seconds for sending and receiving data, or ``None``; see :meth:`BaseTransportAsync.bulk_read() ` and :meth:`BaseTransportAsync.bulk_write() ` read_timeout_s : float The total time in seconds to wait for a ``b'CLSE'`` or ``b'OKAY'`` command in :meth:`_AdbIOManager.read` timeout_s : float, None The total time in seconds to wait for the ADB command to finish """ 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._open(b'reboot:bootloader' if fastboot else b'reboot:', transport_timeout_s, read_timeout_s, timeout_s) async def root(self, transport_timeout_s=None, read_timeout_s=constants.DEFAULT_READ_TIMEOUT_S, timeout_s=None): """Gain root access. The device must be rooted in order for this to work. Parameters ---------- transport_timeout_s : float, None Timeout in seconds for sending and receiving data, or ``None``; see :meth:`BaseTransportAsync.bulk_read() ` and :meth:`BaseTransportAsync.bulk_write() ` read_timeout_s : float The total time in seconds to wait for a ``b'CLSE'`` or ``b'OKAY'`` command in :meth:`_AdbIOManagerAsync.read` timeout_s : float, None The total time in seconds to wait for the ADB command to finish """ 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'', transport_timeout_s, read_timeout_s, timeout_s, False) async def shell(self, command, transport_timeout_s=None, read_timeout_s=constants.DEFAULT_READ_TIMEOUT_S, timeout_s=None, decode=True): """Send an ADB shell command to the device. Parameters ---------- command : str The shell command that will be sent transport_timeout_s : float, None Timeout in seconds for sending and receiving data, or ``None``; see :meth:`BaseTransportAsync.bulk_read() ` and :meth:`BaseTransportAsync.bulk_write() ` read_timeout_s : float The total time in seconds to wait for a ``b'CLSE'`` or ``b'OKAY'`` command in :meth:`_AdbIOManagerAsync.read` timeout_s : float, None The total time in seconds to wait for the ADB command to finish 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'), transport_timeout_s, read_timeout_s, timeout_s, decode) async def streaming_shell(self, command, transport_timeout_s=None, read_timeout_s=constants.DEFAULT_READ_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 transport_timeout_s : float, None Timeout in seconds for sending and receiving data, or ``None``; see :meth:`BaseTransportAsync.bulk_read() ` and :meth:`BaseTransportAsync.bulk_write() ` read_timeout_s : float The total time in seconds to wait for a ``b'CLSE'`` or ``b'OKAY'`` command in :meth:`_AdbIOManagerAsync.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'), transport_timeout_s, read_timeout_s, decode): yield line # ======================================================================= # # # # FileSync # # # # ======================================================================= # async def list(self, device_path, transport_timeout_s=None, read_timeout_s=constants.DEFAULT_READ_TIMEOUT_S): """Return a directory listing of the given path. Parameters ---------- device_path : str Directory to list. transport_timeout_s : float, None Expected timeout for any part of the pull. read_timeout_s : float The total time in seconds to wait for a ``b'CLSE'`` or ``b'OKAY'`` command in :meth:`_AdbIOManagerAsync.read` Returns ------- files : list[DeviceFile] Filename, mode, size, and mtime info for the files in the directory """ if not device_path: raise exceptions.DevicePathInvalidError("Cannot list an empty device path") 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 = await self._open(b'sync:', transport_timeout_s, read_timeout_s, None) filesync_info = _FileSyncTransactionInfo(constants.FILESYNC_LIST_FORMAT, maxdata=self._maxdata) 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._clse(adb_info) return files async def pull(self, device_path, local_path, progress_callback=None, transport_timeout_s=None, read_timeout_s=constants.DEFAULT_READ_TIMEOUT_S): """Pull a file from the device. Parameters ---------- device_path : str The file on the device that will be pulled local_path : str, BytesIO The path or BytesIO stream where the file will be downloaded progress_callback : function, None Callback method that accepts ``device_path``, ``bytes_written``, and ``total_bytes`` transport_timeout_s : float, None Expected timeout for any part of the pull. read_timeout_s : float The total time in seconds to wait for a ``b'CLSE'`` or ``b'OKAY'`` command in :meth:`_AdbIOManagerAsync.read` """ if not device_path: raise exceptions.DevicePathInvalidError("Cannot pull from an empty device path") 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()`?)") opener = _open_bytesio if isinstance(local_path, BytesIO) else aiofiles.open async with opener(local_path, 'wb') as stream: adb_info = await self._open(b'sync:', transport_timeout_s, read_timeout_s, None) filesync_info = _FileSyncTransactionInfo(constants.FILESYNC_PULL_FORMAT, maxdata=self._maxdata) try: await self._pull(device_path, stream, progress_callback, adb_info, filesync_info) finally: await self._clse(adb_info) async def _pull(self, device_path, stream, progress_callback, adb_info, filesync_info): """Pull a file from the device into the file-like ``local_path``. Parameters ---------- device_path : str The file on the device that will be pulled stream : AsyncBufferedIOBase, _AsyncBytesIO File-like object for writing to progress_callback : function, None Callback method that accepts ``device_path``, ``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(device_path))[1] await self._filesync_send(constants.RECV, adb_info, filesync_info, data=device_path) async for cmd_id, _, data in self._filesync_read_until([constants.DATA], [constants.DONE], adb_info, filesync_info): if cmd_id == constants.DONE: break await stream.write(data) if progress_callback: try: await progress_callback(device_path, len(data), total_bytes) except: # noqa pylint: disable=bare-except pass async def push(self, local_path, device_path, st_mode=constants.DEFAULT_PUSH_MODE, mtime=0, progress_callback=None, transport_timeout_s=None, read_timeout_s=constants.DEFAULT_READ_TIMEOUT_S): """Push a file or directory to the device. Parameters ---------- local_path : str, BytesIO A filename, directory, or BytesIO stream to push to the device device_path : str Destination on the device to write to st_mode : int Stat mode for ``local_path`` mtime : int Modification time to set on the file progress_callback : function, None Callback method that accepts ``device_path``, ``bytes_written``, and ``total_bytes`` transport_timeout_s : float, None Expected timeout for any part of the push read_timeout_s : float The total time in seconds to wait for a ``b'CLSE'`` or ``b'OKAY'`` command in :meth:`_AdbIOManagerAsync.read` """ if not device_path: raise exceptions.DevicePathInvalidError("Cannot push to an empty device path") 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()`?)") local_path_is_dir, local_paths, device_paths = await get_running_loop().run_in_executor(None, get_files_to_push, local_path, device_path) if local_path_is_dir: await self.shell("mkdir " + device_path, transport_timeout_s, read_timeout_s) for _local_path, _device_path in zip(local_paths, device_paths): opener = _open_bytesio if isinstance(local_path, BytesIO) else aiofiles.open async with opener(_local_path, 'rb') as stream: adb_info = await self._open(b'sync:', transport_timeout_s, read_timeout_s, None) filesync_info = _FileSyncTransactionInfo(constants.FILESYNC_PUSH_FORMAT, maxdata=self._maxdata) await self._push(stream, _device_path, st_mode, mtime, progress_callback, adb_info, filesync_info) await self._clse(adb_info) async def _push(self, stream, device_path, st_mode, mtime, progress_callback, adb_info, filesync_info): """Push a file-like object to the device. Parameters ---------- stream : AsyncBufferedReader, _AsyncBytesIO File-like object for reading from device_path : str Destination on the device to write to st_mode : int Stat mode for the file mtime : int Modification time progress_callback : function, None Callback method that accepts ``device_path``, ``bytes_written``, and ``total_bytes`` adb_info : _AdbTransactionInfo Info and settings for this ADB transaction Raises ------ PushFailedError Raised on push failure. """ fileinfo = ('{},{}'.format(device_path, int(st_mode))).encode('utf-8') await self._filesync_send(constants.SEND, adb_info, filesync_info, data=fileinfo) if progress_callback: total_bytes = (await get_running_loop().run_in_executor(None, os.fstat, stream.fileno())).st_size while True: data = await stream.read(self.max_chunk_size) if data: await self._filesync_send(constants.DATA, adb_info, filesync_info, data=data) if progress_callback: try: await progress_callback(device_path, len(data), total_bytes) except: # noqa pylint: disable=bare-except pass 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_path, transport_timeout_s=None, read_timeout_s=constants.DEFAULT_READ_TIMEOUT_S): """Get a file's ``stat()`` information. Parameters ---------- device_path : str The file on the device for which we will get information. transport_timeout_s : float, None Expected timeout for any part of the pull. read_timeout_s : float The total time in seconds to wait for a ``b'CLSE'`` or ``b'OKAY'`` command in :meth:`_AdbIOManagerAsync.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 device_path: raise exceptions.DevicePathInvalidError("Cannot stat an empty device path") 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 = await self._open(b'sync:', transport_timeout_s, read_timeout_s, None) filesync_info = _FileSyncTransactionInfo(constants.FILESYNC_STAT_FORMAT, maxdata=self._maxdata) await self._filesync_send(constants.STAT, adb_info, filesync_info, data=device_path) _, (mode, size, mtime), _ = await self._filesync_read([constants.STAT], adb_info, filesync_info) await self._clse(adb_info) return mode, size, mtime # ======================================================================= # # # # Hidden Methods: send packets # # # # ======================================================================= # async def _clse(self, adb_info): """Send a ``b'CLSE'`` message and then read 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._io_manager.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._io_manager.send(msg, adb_info) # ======================================================================= # # # # Hidden Methods # # # # ======================================================================= # async def _open(self, destination, transport_timeout_s, read_timeout_s, timeout_s): """Opens a new connection to the device via an ``b'OPEN'`` message. 1. :meth:`~_AdbIOManagerAsync.send` an ``b'OPEN'`` command to the device that specifies the ``local_id`` 2. :meth:`~_AdbIOManagerAsync.read` the response from the device and fill in the ``adb_info.remote_id`` attribute Parameters ---------- destination : bytes ``b'SERVICE:COMMAND'`` transport_timeout_s : float, None Timeout in seconds for sending and receiving data, or ``None``; see :meth:`BaseTransportAsync.bulk_read() ` and :meth:`BaseTransportAsync.bulk_write() ` read_timeout_s : float The total time in seconds to wait for a ``b'CLSE'`` or ``b'OKAY'`` command in :meth:`_AdbIOManagerAsync.read` timeout_s : float, None The total time in seconds to wait for the ADB command to finish Returns ------- adb_info : _AdbTransactionInfo Info and settings for this ADB transaction """ async with self._local_id_lock: self._local_id += 1 if self._local_id == 2**32: self._local_id = 1 adb_info = _AdbTransactionInfo(self._local_id, None, self._get_transport_timeout_s(transport_timeout_s), read_timeout_s, timeout_s) msg = AdbMessage(constants.OPEN, adb_info.local_id, 0, destination + b'\0') await self._io_manager.send(msg, adb_info) _, adb_info.remote_id, _, _ = await self._io_manager.read([constants.OKAY], adb_info) return adb_info async def _read_until(self, expected_cmds, adb_info): """Read a packet, acknowledging any write packets. 1. Read data via :meth:`_AdbIOManagerAsync.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:`_AdbIOManagerAsync.read` Parameters ---------- expected_cmds : list[bytes] :meth:`_AdbIOManagerAsync.read` will 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:`_AdbIOManagerAsync.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:`_AdbIOManagerAsync.read` """ cmd, _, _, data = await self._io_manager.read(expected_cmds, adb_info, allow_zeros=True) # 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. 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` """ start = time.time() 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._io_manager.send(msg, adb_info) break yield data # Make sure the ADB command has not timed out if adb_info.timeout_s is not None and time.time() - start > adb_info.timeout_s: raise exceptions.AdbTimeoutError("The command did not complete within {} seconds".format(adb_info.timeout_s)) async def _streaming_command(self, service, command, transport_timeout_s, read_timeout_s, timeout_s): """One complete set of 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 transport_timeout_s : float, None Timeout in seconds for sending and receiving data, or ``None``; see :meth:`BaseTransportAsync.bulk_read() ` and :meth:`BaseTransportAsync.bulk_write() ` read_timeout_s : float The total time in seconds to wait for a ``b'CLSE'`` or ``b'OKAY'`` command in :meth:`_AdbIOManagerAsync.read` timeout_s : float, None The total time in seconds to wait for the ADB command to finish Yields ------ bytes The responses from the service. """ adb_info = await self._open(b'%s:%s' % (service, command), transport_timeout_s, read_timeout_s, timeout_s) async for data in self._read_until_close(adb_info): yield data # ======================================================================= # # # # 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 """ # Send the buffer msg = AdbMessage(constants.WRTE, adb_info.local_id, adb_info.remote_id, filesync_info.send_buffer[:filesync_info.send_idx]) await self._io_manager.send(msg, adb_info) # Expect an 'OKAY' in response await self._read_until([constants.OKAY], adb_info) # Reset the send index filesync_info.send_idx = 0 async def _filesync_read(self, expected_ids, adb_info, filesync_info): """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 Returns ------- command_id : bytes The received header ID tuple The contents of the header data : bytearray, None The received data, or ``None`` if the command ID is :const:`adb_shell.constants.STAT` 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]] # Whether there is data to read read_data = command_id != constants.STAT if read_data: # Header is (ID, ..., size) --> read the data data = await self._filesync_read_buffered(header[-1], adb_info, filesync_info) else: # No data to be read data = bytearray() if command_id not in expected_ids: if command_id == constants.FAIL: reason = data.decode('utf-8', errors='backslashreplace') 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 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=None): """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, None Optionally override size from len(data). """ if not isinstance(data, bytes): data = data.encode('utf8') if size is None: 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) 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_transport_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 _default_transport_timeout_s : float, None Default timeout in seconds for TCP packets, or ``None`` _local_id : int The local ID that is used for ADB transactions; the value is incremented each time and is always in the range ``[1, 2^32)`` _maxdata : int Maximum amount of data in an ADB packet _transport : TcpTransportAsync The transport that is used to connect to the device """ def __init__(self, host, port=5555, default_transport_timeout_s=None, banner=None): transport = TcpTransportAsync(host, port) super(AdbDeviceTcpAsync, self).__init__(transport, default_transport_timeout_s, banner) ================================================ FILE: adb_shell/adb_message.py ================================================ # Copyright (c) 2021 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. """Functions and an :class:`AdbMessage` class for packing and unpacking ADB messages. .. rubric:: Contents * :class:`AdbMessage` * :attr:`AdbMessage.checksum` * :meth:`AdbMessage.pack` * :func:`checksum` * :func:`int_to_cmd` * :func:`unpack` """ import struct from . import constants def checksum(data): """Calculate the checksum of the provided data. Parameters ---------- data : bytearray, bytes, str The data Returns ------- int The checksum """ # The checksum is just a sum of all the bytes. I swear. if isinstance(data, bytearray): total = sum(data) elif isinstance(data, bytes): if data and isinstance(data[0], bytes): # Python 2 bytes (str) index as single-character strings. total = sum((ord(d) for d in data)) # pragma: no cover else: # Python 3 bytes index as numbers (and PY2 empty strings sum() to 0) total = sum(data) else: # Unicode strings (should never see?) total = sum((ord(d) for d in data)) return total & 0xFFFFFFFF def int_to_cmd(n): """Convert from an integer (4 bytes) to an ADB command. Parameters ---------- n : int The integer that will be converted to an ADB command Returns ------- str The ADB command (e.g., ``'CNXN'``) """ return ''.join(chr((n >> (i * 8)) % 256) for i in range(4)).encode('utf-8') def unpack(message): """Unpack a received ADB message. Parameters ---------- message : bytes The received message Returns ------- cmd : int The ADB command arg0 : int TODO arg1 : int TODO data_length : int The length of the message's data data_checksum : int The checksum of the message's data Raises ------ ValueError Unable to unpack the ADB command. """ try: cmd, arg0, arg1, data_length, data_checksum, _ = struct.unpack(constants.MESSAGE_FORMAT, message) except struct.error as e: raise ValueError('Unable to unpack ADB command. (length={})'.format(len(message)), constants.MESSAGE_FORMAT, message, e) return cmd, arg0, arg1, data_length, data_checksum class AdbMessage(object): """A helper class for packing ADB messages. Parameters ---------- command : bytes A command; examples used in this package include :const:`adb_shell.constants.AUTH`, :const:`adb_shell.constants.CNXN`, :const:`adb_shell.constants.CLSE`, :const:`adb_shell.constants.OPEN`, and :const:`adb_shell.constants.OKAY` arg0 : int Usually the local ID, but :meth:`~adb_shell.adb_device.AdbDevice.connect` and :meth:`~adb_shell.adb_device_async.AdbDeviceAsync.connect` provide :const:`adb_shell.constants.VERSION`, :const:`adb_shell.constants.AUTH_SIGNATURE`, and :const:`adb_shell.constants.AUTH_RSAPUBLICKEY` arg1 : int Usually the remote ID, but :meth:`~adb_shell.adb_device.AdbDevice.connect` and :meth:`~adb_shell.adb_device_async.AdbDeviceAsync.connect` provide :const:`adb_shell.constants.MAX_ADB_DATA` data : bytes The data that will be sent Attributes ---------- arg0 : int Usually the local ID, but :meth:`~adb_shell.adb_device.AdbDevice.connect` and :meth:`~adb_shell.adb_device_async.AdbDeviceAsync.connect` provide :const:`adb_shell.constants.VERSION`, :const:`adb_shell.constants.AUTH_SIGNATURE`, and :const:`adb_shell.constants.AUTH_RSAPUBLICKEY` arg1 : int Usually the remote ID, but :meth:`~adb_shell.adb_device.AdbDevice.connect` and :meth:`~adb_shell.adb_device_async.AdbDeviceAsync.connect` provide :const:`adb_shell.constants.MAX_ADB_DATA` command : int The input parameter ``command`` converted to an integer via :const:`adb_shell.constants.ID_TO_WIRE` data : bytes The data that will be sent magic : int ``self.command`` with its bits flipped; in other words, ``self.command + self.magic == 2**32 - 1`` """ def __init__(self, command, arg0, arg1, data=b''): self.command = constants.ID_TO_WIRE[command] self.magic = self.command ^ 0xFFFFFFFF self.arg0 = arg0 self.arg1 = arg1 self.data = data def pack(self): """Returns this message in an over-the-wire format. Returns ------- bytes The message packed into the format required by ADB """ return struct.pack(constants.MESSAGE_FORMAT, self.command, self.arg0, self.arg1, len(self.data), self.checksum, self.magic) @property def checksum(self): """Return ``checksum(self.data)`` Returns ------- int The checksum of ``self.data`` """ return checksum(self.data) ================================================ FILE: adb_shell/auth/__init__.py ================================================ # Copyright (c) 2021 Jeff Irion and contributors # # This file is part of the adb-shell package. ================================================ FILE: adb_shell/auth/keygen.py ================================================ # Copyright (c) 2021 Jeff Irion and contributors # # This file is part of the adb-shell package. It was originally written by # @joeleong, and it was obtained from: https://github.com/google/python-adb/pull/144 """This file implements encoding and decoding logic for Android's custom RSA public key binary format. Public keys are stored as a sequence of little-endian 32 bit words. Note that Android only supports little-endian processors, so we don't do any byte order conversions when parsing the binary struct. Structure from: https://github.com/aosp-mirror/platform_system_core/blob/c55fab4a59cfa461857c6a61d8a0f1ae4591900c/libcrypto_utils/android_pubkey.c .. code-block:: c typedef struct RSAPublicKey { // Modulus length. This must be ANDROID_PUBKEY_MODULUS_SIZE_WORDS uint32_t modulus_size_words; // Precomputed montgomery parameter: -1 / n[0] mod 2^32 uint32_t n0inv; // RSA modulus as a little-endian array uint8_t modulus[ANDROID_PUBKEY_MODULUS_SIZE]; // Montgomery parameter R^2 as a little-endian array of little-endian words uint8_t rr[ANDROID_PUBKEY_MODULUS_SIZE]; // RSA modulus: 3 or 65537 uint32_t exponent; } RSAPublicKey; .. rubric:: Contents * :func:`_to_bytes` * :func:`decode_pubkey` * :func:`decode_pubkey_file` * :func:`encode_pubkey` * :func:`get_user_info` * :func:`keygen` * :func:`write_public_keyfile` """ import os import base64 import logging import socket import struct import sys from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa _LOGGER = logging.getLogger(__name__) if sys.version_info[0] == 2: # pragma: no cover FileNotFoundError = IOError # pylint: disable=redefined-builtin #: Size of an RSA modulus such as an encrypted block or a signature. ANDROID_PUBKEY_MODULUS_SIZE = 2048 // 8 #: Python representation of "struct RSAPublicKey" ANDROID_RSAPUBLICKEY_STRUCT = ( '<' # Little-endian 'L' # uint32_t modulus_size_words; 'L' # uint32_t n0inv; '{modulus_size}s' # uint8_t modulus[ANDROID_PUBKEY_MODULUS_SIZE]; '{modulus_size}s' # uint8_t rr[ANDROID_PUBKEY_MODULUS_SIZE]; 'L' # uint32_t exponent; ).format(modulus_size=ANDROID_PUBKEY_MODULUS_SIZE) #: Size of the RSA modulus in words. ANDROID_PUBKEY_MODULUS_SIZE_WORDS = ANDROID_PUBKEY_MODULUS_SIZE // 4 def _to_bytes(n, length, endianess='big'): """Partial python2 compatibility with int.to_bytes https://stackoverflow.com/a/20793663 Parameters ---------- n : TODO TODO length : TODO TODO endianess : str, TODO TODO Returns ------- TODO TODO """ if not hasattr(n, 'to_bytes'): h = '{:x}'.format(n) s = ('0' * (len(h) % 2) + h).zfill(length * 2).decode('hex') return s if endianess == 'big' else s[::-1] return n.to_bytes(length, endianess) def decode_pubkey(public_key): """Decode a public RSA key stored in Android's custom binary format. Parameters ---------- public_key : TODO TODO """ binary_key_data = base64.b64decode(public_key) modulus_size_words, n0inv, modulus_bytes, rr_bytes, exponent = struct.unpack(ANDROID_RSAPUBLICKEY_STRUCT, binary_key_data) assert modulus_size_words == ANDROID_PUBKEY_MODULUS_SIZE_WORDS modulus = reversed(modulus_bytes) rr = reversed(rr_bytes) _LOGGER.debug('modulus_size_words: %s', hex(modulus_size_words)) _LOGGER.debug('n0inv: %s', hex(n0inv)) _LOGGER.debug('modulus: %s', ':'.join((hex(m) for m in modulus))) _LOGGER.debug('rr: %s', ':'.join((hex(r) for r in rr))) _LOGGER.debug('exponent: %s', hex(exponent)) def decode_pubkey_file(public_key_path): """TODO Parameters ---------- public_key_path : str TODO """ with open(public_key_path, 'rb') as fd: decode_pubkey(fd.read()) def encode_pubkey(private_key_path): """Encodes a public RSA key into Android's custom binary format. Parameters ---------- private_key_path : str TODO Returns ------- TODO TODO """ with open(private_key_path, 'rb') as key_file: key = serialization.load_pem_private_key(key_file.read(), password=None, backend=default_backend()).private_numbers().public_numbers # Compute and store n0inv = -1 / N[0] mod 2^32. # BN_set_bit(r32, 32) r32 = 1 << 32 # BN_mod(n0inv, key->n, r32, ctx) n0inv = key.n % r32 # BN_mod_inverse(n0inv, n0inv, r32, ctx) n0inv = rsa._modinv(n0inv, r32) # pylint: disable=protected-access # BN_sub(n0inv, r32, n0inv) n0inv = r32 - n0inv # Compute and store rr = (2^(rsa_size)) ^ 2 mod N. # BN_set_bit(rr, ANDROID_PUBKEY_MODULUS_SIZE * 8) rr = 1 << (ANDROID_PUBKEY_MODULUS_SIZE * 8) # BN_mod_sqr(rr, rr, key->n, ctx) rr = (rr ** 2) % key.n return struct.pack( ANDROID_RSAPUBLICKEY_STRUCT, ANDROID_PUBKEY_MODULUS_SIZE_WORDS, n0inv, _to_bytes(key.n, ANDROID_PUBKEY_MODULUS_SIZE, 'little'), _to_bytes(rr, ANDROID_PUBKEY_MODULUS_SIZE, 'little'), key.e ) def get_user_info(): """TODO Returns ------- str ``' @`` """ try: username = os.getlogin() except (FileNotFoundError, OSError): username = 'unknown' if not username: username = 'unknown' hostname = socket.gethostname() if not hostname: hostname = 'unknown' return ' ' + username + '@' + hostname def write_public_keyfile(private_key_path, public_key_path): """Write a public keyfile to ``public_key_path`` in Android's custom RSA public key format given a path to a private keyfile. Parameters ---------- private_key_path : TODO TODO public_key_path : TODO TODO """ public_key = encode_pubkey(private_key_path) assert len(public_key) == struct.calcsize(ANDROID_RSAPUBLICKEY_STRUCT) with open(public_key_path, 'wb') as public_key_file: public_key_file.write(base64.b64encode(public_key)) public_key_file.write(get_user_info().encode()) def keygen(filepath): """Generate an ADB public/private key pair. * The private key is stored in ``filepath``. * The public key is stored in ``filepath + '.pub'`` (Existing files will be overwritten.) Parameters ---------- filepath : str File path to write the private/public keypair """ private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048, backend=default_backend()) with open(filepath, 'wb') as private_key_file: private_key_file.write(private_key.private_bytes(encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption())) write_public_keyfile(filepath, filepath + '.pub') ================================================ FILE: adb_shell/auth/sign_cryptography.py ================================================ # Copyright 2014 Google Inc. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ADB authentication using the ``cryptography`` package. .. rubric:: Contents * :class:`CryptographySigner` * :meth:`CryptographySigner.GetPublicKey` * :meth:`CryptographySigner.Sign` """ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives.asymmetric import utils # pylint: disable=abstract-method class CryptographySigner(object): """AuthSigner using cryptography.io. Parameters ---------- rsa_key_path : str The path to the private key. Attributes ---------- public_key : str The contents of the public key file rsa_key : cryptography.hazmat.backends.openssl.rsa._RSAPrivateKey The loaded private key """ def __init__(self, rsa_key_path): with open(rsa_key_path + '.pub', 'rb') as rsa_pub_file: self.public_key = rsa_pub_file.read() with open(rsa_key_path, 'rb') as rsa_prv_file: self.rsa_key = serialization.load_pem_private_key(rsa_prv_file.read(), None, default_backend()) def Sign(self, data): """Signs given data using a private key. Parameters ---------- data : TODO TODO Returns ------- TODO The signed ``data`` """ return self.rsa_key.sign(data, padding.PKCS1v15(), utils.Prehashed(hashes.SHA1())) def GetPublicKey(self): """Returns the public key in PEM format without headers or newlines. Returns ------- self.public_key : str The contents of the public key file """ return self.public_key ================================================ FILE: adb_shell/auth/sign_pycryptodome.py ================================================ # Copyright 2014 Google Inc. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ADB authentication using ``pycryptodome``. .. rubric:: Contents * :class:`PycryptodomeAuthSigner` * :meth:`PycryptodomeAuthSigner.GetPublicKey` * :meth:`PycryptodomeAuthSigner.Sign` """ from Crypto.Hash import SHA256 from Crypto.PublicKey import RSA from Crypto.Signature import pkcs1_15 class PycryptodomeAuthSigner(object): """AuthSigner using the pycryptodome package. Parameters ---------- rsa_key_path : str, None The path to the private key Attributes ---------- public_key : str The contents of the public key file rsa_key : Crypto.PublicKey.RSA.RsaKey The contents of theprivate key """ def __init__(self, rsa_key_path=None): super(PycryptodomeAuthSigner, self).__init__() if rsa_key_path: with open(rsa_key_path + '.pub', 'rb') as rsa_pub_file: self.public_key = rsa_pub_file.read() with open(rsa_key_path, 'rb') as rsa_priv_file: self.rsa_key = RSA.import_key(rsa_priv_file.read()) def Sign(self, data): """Signs given data using a private key. Parameters ---------- data : bytes, bytearray The data to be signed Returns ------- bytes The signed ``data`` """ h = SHA256.new(data) return pkcs1_15.new(self.rsa_key).sign(h) def GetPublicKey(self): """Returns the public key in PEM format without headers or newlines. Returns ------- self.public_key : str The contents of the public key file """ return self.public_key ================================================ FILE: adb_shell/auth/sign_pythonrsa.py ================================================ # Copyright 2014 Google Inc. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ADB authentication using the ``rsa`` package. .. rubric:: Contents * :class:`_Accum` * :meth:`_Accum.digest` * :meth:`_Accum.update` * :func:`_load_rsa_private_key` * :class:`PythonRSASigner` * :meth:`PythonRSASigner.FromRSAKeyPath` * :meth:`PythonRSASigner.GetPublicKey` * :meth:`PythonRSASigner.Sign` """ from pyasn1.codec.der import decoder from pyasn1.type import univ import rsa from rsa import pkcs1 class _Accum(object): """A fake hashing algorithm. The Python ``rsa`` lib hashes all messages it signs. ADB does it already, we just need to slap a signature on top of already hashed message. Introduce a "fake" hashing algo for this. Attributes ---------- _buf : bytes A buffer for storing data before it is signed """ def __init__(self): self._buf = b'' def update(self, msg): """Update this hash object's state with the provided ``msg``. Parameters ---------- msg : bytes The message to be appended to ``self._buf`` """ self._buf += msg def digest(self): """Return the digest value as a string of binary data. Returns ------- self._buf : bytes ``self._buf`` """ return self._buf pkcs1.HASH_METHODS['SHA-1-PREHASHED'] = _Accum pkcs1.HASH_ASN1['SHA-1-PREHASHED'] = pkcs1.HASH_ASN1['SHA-1'] def _load_rsa_private_key(pem): """PEM encoded PKCS#8 private key -> ``rsa.PrivateKey``. ADB uses private RSA keys in pkcs#8 format. The ``rsa`` library doesn't support them natively. Do some ASN unwrapping to extract naked RSA key (in der-encoded form). See: * https://www.ietf.org/rfc/rfc2313.txt * http://superuser.com/a/606266 Parameters ---------- pem : str The private key to be loaded Returns ------- rsa.key.PrivateKey The loaded private key """ try: der = rsa.pem.load_pem(pem, 'PRIVATE KEY') keyinfo, _ = decoder.decode(der) if keyinfo[1][0] != univ.ObjectIdentifier('1.2.840.113549.1.1.1'): raise ValueError('Not a DER-encoded OpenSSL private RSA key') private_key_der = keyinfo[2].asOctets() except IndexError: raise ValueError('Not a DER-encoded OpenSSL private RSA key') return rsa.PrivateKey.load_pkcs1(private_key_der, format='DER') class PythonRSASigner(object): """Implements :class:`adb_protocol.AuthSigner` using http://stuvel.eu/rsa. Parameters ---------- pub : str, None The contents of the public key file priv : str, None The contents of the private key file Attributes ---------- priv_key : rsa.key.PrivateKey The loaded private key pub_key : str, None The contents of the public key file """ def __init__(self, pub=None, priv=None): self.priv_key = _load_rsa_private_key(priv) self.pub_key = pub @classmethod def FromRSAKeyPath(cls, rsa_key_path): """Create a :class:`PythonRSASigner` instance using the provided private key. Parameters ---------- rsa_key_path : str The path to the private key; the public key must be ``rsa_key_path + '.pub'``. Returns ------- PythonRSASigner A :class:`PythonRSASigner` with private key ``rsa_key_path`` and public key ``rsa_key_path + '.pub'`` """ with open(rsa_key_path + '.pub') as f: pub = f.read() with open(rsa_key_path) as f: priv = f.read() return cls(pub, priv) def Sign(self, data): """Signs given data using a private key. Parameters ---------- data : bytes The data to be signed Returns ------- bytes The signed ``data`` """ return rsa.sign(data, self.priv_key, 'SHA-1-PREHASHED') def GetPublicKey(self): """Returns the public key in PEM format without headers or newlines. Returns ------- self.pub_key : str, None The contents of the public key file, or ``None`` if a public key was not provided. """ return self.pub_key ================================================ FILE: adb_shell/constants.py ================================================ # Copyright (c) 2021 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. """Constants used throughout the code. """ import stat import struct #: From adb.h CLASS = 0xFF #: From adb.h SUBCLASS = 0x42 #: From adb.h PROTOCOL = 0x01 #: ADB protocol version. VERSION = 0x01000000 #: Maximum amount of data in an ADB packet. According to: https://android.googlesource.com/platform/system/core/+/master/adb/adb.h MAX_ADB_DATA = 1024 * 1024 MAX_LEGACY_ADB_DATA = 4 * 1024 #: Maximum size of a filesync DATA packet. Default size. MAX_PUSH_DATA = 2 * 1024 #: Maximum chunk size. According to https://android.googlesource.com/platform/system/core/+/master/adb/SYNC.TXT MAX_CHUNK_SIZE = 64 * 1024 #: Default mode for pushed files. DEFAULT_PUSH_MODE = stat.S_IFREG | stat.S_IRWXU | stat.S_IRWXG #: AUTH constant for ``arg0`` AUTH_TOKEN = 1 #: AUTH constant for ``arg0`` AUTH_SIGNATURE = 2 #: AUTH constant for ``arg0`` AUTH_RSAPUBLICKEY = 3 AUTH = b'AUTH' CLSE = b'CLSE' CNXN = b'CNXN' FAIL = b'FAIL' OKAY = b'OKAY' OPEN = b'OPEN' SYNC = b'SYNC' WRTE = b'WRTE' DATA = b'DATA' DENT = b'DENT' DONE = b'DONE' LIST = b'LIST' QUIT = b'QUIT' RECV = b'RECV' SEND = b'SEND' STAT = b'STAT' #: Commands that are recognized by :meth:`adb_shell.adb_device._AdbIOManager._read_packet_from_device` and :meth:`adb_shell.adb_device_async._AdbIOManagerAsync._read_packet_from_device` IDS = (AUTH, CLSE, CNXN, OKAY, OPEN, SYNC, WRTE) #: A dictionary where the keys are the commands in :const:`IDS` and the values are the keys converted to integers ID_TO_WIRE = {cmd_id: sum(c << (i * 8) for i, c in enumerate(bytearray(cmd_id))) for cmd_id in IDS} #: A dictionary where the keys are integers and the values are their corresponding commands (type = bytes) from :const:`IDS` WIRE_TO_ID = {wire: cmd_id for cmd_id, wire in ID_TO_WIRE.items()} #: Commands that are recognized by :meth:`adb_shell.adb_device.AdbDevice._filesync_read` and :meth:`adb_shell.adb_device_async.AdbDeviceAsync._filesync_read` FILESYNC_IDS = (DATA, DENT, DONE, FAIL, LIST, OKAY, QUIT, RECV, SEND, STAT) #: A dictionary where the keys are the commands in :const:`FILESYNC_IDS` and the values are the keys converted to integers FILESYNC_ID_TO_WIRE = {cmd_id: sum(c << (i * 8) for i, c in enumerate(bytearray(cmd_id))) for cmd_id in FILESYNC_IDS} #: A dictionary where the keys are integers and the values are their corresponding commands (type = bytes) from :const:`FILESYNC_IDS` FILESYNC_WIRE_TO_ID = {wire: cmd_id for cmd_id, wire in FILESYNC_ID_TO_WIRE.items()} #: An ADB message is 6 words in little-endian. MESSAGE_FORMAT = b'<6I' #: The format for FileSync "list" messages FILESYNC_LIST_FORMAT = b'<5I' #: The format for FileSync "pull" messages FILESYNC_PULL_FORMAT = b'<2I' #: The format for FileSync "push" messages FILESYNC_PUSH_FORMAT = b'<2I' #: The format for FileSync "stat" messages FILESYNC_STAT_FORMAT = b'<4I' #: The size of an ADB message MESSAGE_SIZE = struct.calcsize(MESSAGE_FORMAT) #: Default authentication timeout (in s) for :meth:`adb_shell.adb_device.AdbDevice.connect` and :meth:`adb_shell.adb_device_async.AdbDeviceAsync.connect` DEFAULT_AUTH_TIMEOUT_S = 10. #: Default total timeout (in s) for reading data from the device DEFAULT_READ_TIMEOUT_S = 10. ================================================ FILE: adb_shell/exceptions.py ================================================ # Copyright (c) 2021 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. """ADB-related exceptions. """ class AdbCommandFailureException(Exception): """A ``b'FAIL'`` packet was received. """ class AdbConnectionError(Exception): """ADB command not sent because a connection to the device has not been established. """ class AdbTimeoutError(Exception): """ADB command did not complete within the specified time. """ class DeviceAuthError(Exception): """Device authentication failed. """ def __init__(self, message, *args): message %= args super(DeviceAuthError, self).__init__(message, *args) class InvalidChecksumError(Exception): """Checksum of data didn't match expected checksum. """ class InvalidCommandError(Exception): """Got an invalid command. """ class InvalidTransportError(Exception): """The provided transport does not implement the necessary methods: ``close``, ``connect``, ``bulk_read``, and ``bulk_write``. """ class InvalidResponseError(Exception): """Got an invalid response to our command. """ class DevicePathInvalidError(Exception): """A file command was passed an invalid path. """ class PushFailedError(Exception): """Pushing a file failed for some reason. """ class TcpTimeoutException(Exception): """TCP connection timed read/write operation exceeded the allowed time. """ class UsbDeviceNotFoundError(Exception): """TODO """ class UsbReadFailedError(Exception): """TODO Parameters ---------- msg : str The error message usb_error : libusb1.USBError An exception from ``libusb1`` Attributes ---------- usb_error : libusb1.USBError An exception from ``libusb1`` """ def __init__(self, msg, usb_error): super(UsbReadFailedError, self).__init__(msg, usb_error) self.usb_error = usb_error def __str__(self): return '%s: %s' % self.args class UsbWriteFailedError(Exception): """:meth:`adb_shell.transport.usb_transport.UsbTransport.bulk_write` failed. """ ================================================ FILE: adb_shell/hidden_helpers.py ================================================ # Copyright (c) 2021 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:`_AdbPacketStore` * :meth:`_AdbPacketStore.__contains__` * :meth:`_AdbPacketStore.__len__` * :meth:`_AdbPacketStore.clear` * :meth:`_AdbPacketStore.clear_all` * :meth:`_AdbPacketStore.find` * :meth:`_AdbPacketStore.find_allow_zeros` * :meth:`_AdbPacketStore.get` * :meth:`_AdbPacketStore.put` * :class:`_AdbTransactionInfo` * :meth:`_AdbTransactionInfo.args_match` * :class:`_FileSyncTransactionInfo` * :meth:`_FileSyncTransactionInfo.can_add_to_send_buffer` * :func:`get_banner` * :func:`get_files_to_push` """ from collections import namedtuple from io import BytesIO import os import socket import struct try: from asyncio import Queue except ImportError: # pragma: no cover try: from queue import Queue except ImportError: from Queue import Queue from . import constants DeviceFile = namedtuple('DeviceFile', ['filename', 'mode', 'size', 'mtime']) def get_files_to_push(local_path, device_path): """Get a list of the file(s) to push. Parameters ---------- local_path : str A path to a local file or directory device_path : str A path to a file or directory on the device Returns ------- local_path_is_dir : bool Whether or not ``local_path`` is a directory local_paths : list[str] A list of the file(s) to push device_paths : list[str] A list of destination paths on the device that corresponds to ``local_paths`` """ local_path_is_dir = not isinstance(local_path, BytesIO) and os.path.isdir(local_path) local_paths = [local_path] if not local_path_is_dir else os.listdir(local_path) device_paths = [device_path] if not local_path_is_dir else [device_path + '/' + f for f in local_paths] return local_path_is_dir, local_paths, device_paths def get_banner(): """Get the ``banner`` that will be signed in :meth:`adb_shell.adb_device.AdbDevice.connect` / :meth:`adb_shell.adb_device_async.AdbDeviceAsync.connect`. Returns ------- bytearray The hostname, or "unknown" if it could not be determined """ try: return bytearray(socket.gethostname(), 'utf-8') except: # noqa pylint: disable=bare-except return bytearray('unknown', 'utf-8') class _AdbTransactionInfo(object): # pylint: disable=too-few-public-methods """A class for storing info and settings used during a single ADB "transaction." Note that if ``timeout_s`` is not ``None``, then: :: self.transport_timeout_s <= self.read_timeout_s <= self.timeout_s If ``timeout_s`` is ``None``, the first inequality still applies. Parameters ---------- local_id : int The ID for the sender (i.e., the device running this code) remote_id : int The ID for the recipient transport_timeout_s : float, None Timeout in seconds for sending and receiving data, or ``None``; see :meth:`BaseTransport.bulk_read() `, :meth:`BaseTransport.bulk_write() `, :meth:`BaseTransportAsync.bulk_read() `, and :meth:`BaseTransportAsync.bulk_write() ` read_timeout_s : float The total time in seconds to wait for data and packets from the device timeout_s : float, None The total time in seconds to wait for the ADB command to finish Attributes ---------- local_id : int The ID for the sender (i.e., the device running this code) read_timeout_s : float The total time in seconds to wait for data and packets from the device remote_id : int The ID for the recipient timeout_s : float, None The total time in seconds to wait for the ADB command to finish transport_timeout_s : float, None Timeout in seconds for sending and receiving data, or ``None``; see :meth:`BaseTransport.bulk_read() `, :meth:`BaseTransport.bulk_write() `, :meth:`BaseTransportAsync.bulk_read() `, and :meth:`BaseTransportAsync.bulk_write() ` """ def __init__(self, local_id, remote_id, transport_timeout_s=None, read_timeout_s=constants.DEFAULT_READ_TIMEOUT_S, timeout_s=None): self.local_id = local_id self.remote_id = remote_id self.timeout_s = timeout_s self.read_timeout_s = read_timeout_s if self.timeout_s is None else min(read_timeout_s, self.timeout_s) self.transport_timeout_s = self.read_timeout_s if transport_timeout_s is None else min(transport_timeout_s, self.read_timeout_s) def args_match(self, arg0, arg1, allow_zeros=False): """Check if ``arg0`` and ``arg1`` match this object's ``remote_id`` and ``local_id`` attributes, respectively. Parameters ---------- arg0 : int The ``arg0`` value from an ADB packet, which will be compared to this object's ``remote_id`` attribute arg1 : int The ``arg1`` value from an ADB packet, which will be compared to this object's ``local_id`` attribute allow_zeros : bool Whether to check if ``arg0`` and ``arg1`` match 0, in addition to this object's ``local_id`` and ``remote_id`` attributes Returns ------- bool Whether ``arg0`` and ``arg1`` match this object's ``local_id`` and ``remote_id`` attributes """ if not allow_zeros: return arg1 == self.local_id and (self.remote_id is None or arg0 == self.remote_id) # https://github.com/JeffLIrion/adb_shell/blob/17540be9b3b84637aca9b994ae3e0b35d02b1a03/adb_shell/adb_device.py#L923-L929 return arg1 in (0, self.local_id) and (self.remote_id is None or arg0 in (0, self.remote_id)) 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 maxdata: int Maximum amount of data in an ADB packet Attributes ---------- _maxdata: int Maximum amount of data in an ADB packet 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, maxdata=constants.MAX_ADB_DATA): self.send_buffer = bytearray(maxdata) self.send_idx = 0 self.recv_buffer = bytearray() self.recv_message_format = recv_message_format self.recv_message_size = struct.calcsize(recv_message_format) self._maxdata = maxdata 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 < self._maxdata class _AdbPacketStore(object): """A class for storing ADB packets. This class is used to support multiple streams. Attributes ---------- _dict : dict[int: dict[int: Queue]] A dictionary of dictionaries of queues. The first (outer) dictionary keys are the ``arg1`` return values from the :meth:`adb_shell.adb_device._AdbIOManager._read_packet_from_device` and :meth:`adb_shell.adb_device_async._AdbIOManagerAsync._read_packet_from_device` methods. The second (inner) dictionary keys are the ``arg0`` return values from those methods. And the values of this inner dictionary are queues of ``(cmd, data)`` tuples. """ def __init__(self): self._dict = {} def __contains__(self, value): """Check if there are any entries in a queue for the specified value. Note that ``None`` is used as a wildcard. Parameters ---------- value : tuple[int, int] An ``(arg0, arg1)`` pair; either or both values can be ``None`` Returns ------- bool Whether the ``(arg0, arg1)`` tuple has any corresponding queue entries """ return bool(self.find(value[0], value[1])) def __len__(self): """Get the number of non-empty queues. Returns ------- int The number of non-empty queues """ return sum(not val0.empty() for val1 in self._dict.values() for val0 in val1.values()) def clear(self, arg0, arg1): """Delete the entry for ``(arg0, arg1)``, if it exists. Parameters ---------- arg0 : int The ``arg0`` return value from the :meth:`adb_shell.adb_device._AdbIOManager._read_packet_from_device` and :meth:`adb_shell.adb_device_async._AdbIOManagerAsync._read_packet_from_device` methods arg1 : int The ``arg1`` return value from the :meth:`adb_shell.adb_device._AdbIOManager._read_packet_from_device` and :meth:`adb_shell.adb_device_async._AdbIOManagerAsync._read_packet_from_device` methods """ if arg1 in self._dict and arg0 in self._dict[arg1]: del self._dict[arg1][arg0] if not self._dict[arg1]: # `self._dict[arg1]` is an empty dictionary now, so delete it del self._dict[arg1] def clear_all(self): """Clear all the entries.""" self._dict = {} def find(self, arg0, arg1): """Find the entry corresponding to ``arg0`` and ``arg1``. Parameters ---------- arg0 : int, None The ``arg0`` value that we are looking for; ``None`` serves as a wildcard arg1 : int, None The ``arg1`` value that we are looking for; ``None`` serves as a wildcard Returns ------- tuple[int, int], None The ``(arg0, arg1)`` pair that was found in the dictionary of dictionaries, or ``None`` if no match was found """ if not self._dict: return None if arg1 is None: if arg0 is None: # `value = (None, None)` -> search for any non-empty queue return next(((key0, key1) for key1, val1 in self._dict.items() for key0, val0 in val1.items() if not val0.empty()), None) # Search for a non-empty queue with a key of `arg0 == value[0]` return next(((arg0, key1) for key1, val1 in self._dict.items() for key0, val0 in val1.items() if key0 == arg0 and not val0.empty()), None) if arg1 not in self._dict: return None if arg0 is None: # Look for a non-empty queue in the `self._dict[value[1]]` dictionary return next(((key0, arg1) for key0, val0 in self._dict[arg1].items() if not val0.empty()), None) if arg0 in self._dict[arg1] and not self._dict[arg1][arg0].empty(): return (arg0, arg1) return None def find_allow_zeros(self, arg0, arg1): """Find the entry corresponding to (``arg0`` or 0) and (``arg1`` or 0). Parameters ---------- arg0 : int, None The ``arg0`` value that we are looking for; ``None`` serves as a wildcard arg1 : int, None The ``arg1`` value that we are looking for; ``None`` serves as a wildcard Returns ------- tuple[int, int], None The first matching ``(arg0, arg1)`` pair that was found in the dictionary of dictionaries, or ``None`` if no match was found """ for arg0_, arg1_ in ((arg0, arg1), (arg0, 0), (0, arg1), (0, 0)): arg0_arg1 = self.find(arg0_, arg1_) if arg0_arg1: return arg0_arg1 return None def get(self, arg0, arg1): """Get the next entry from the queue for ``arg0`` and ``arg1``. This function assumes you have already checked that ``(arg0, arg1) in self``. Parameters ---------- arg0 : int, None The ``arg0`` return value from the :meth:`adb_shell.adb_device._AdbIOManager._read_packet_from_device` and :meth:`adb_shell.adb_device_async._AdbIOManagerAsync._read_packet_from_device` methods; ``None`` serves as a wildcard arg1 : int, None The ``arg1`` return value from the :meth:`adb_shell.adb_device._AdbIOManager._read_packet_from_device` and :meth:`adb_shell.adb_device_async._AdbIOManagerAsync._read_packet_from_device` methods; ``None`` serves as a wildcard Returns ------- cmd : bytes The ADB packet's command arg0 : int The ``arg0`` value from the returned packet arg1 : int The ``arg1`` value from the returned packet data : bytes The ADB packet's data """ if arg0 is None or arg1 is None: arg0, arg1 = self.find(arg0, arg1) # Get the data from the queue cmd, data = self._dict[arg1][arg0].get_nowait() # If this is a `CLSE` packet, then clear the entry in the store if cmd == constants.CLSE: self.clear(arg0, arg1) return cmd, arg0, arg1, data def put(self, arg0, arg1, cmd, data): """Add an entry to the queue for ``arg0`` and ``arg1``. Note that a new dictionary entry will not be created if ``cmd == constants.CLSE``. Parameters ---------- arg0 : int The ``arg0`` return value from the :meth:`adb_shell.adb_device._AdbIOManager._read_packet_from_device` and :meth:`adb_shell.adb_device_async._AdbIOManagerAsync._read_packet_from_device` methods arg1 : int The ``arg1`` return value from the :meth:`adb_shell.adb_device._AdbIOManager._read_packet_from_device` and :meth:`adb_shell.adb_device_async._AdbIOManagerAsync._read_packet_from_device` methods cmd : bytes The ADB packet's command data : bytes The ADB packet's data """ if arg1 in self._dict: if arg0 not in self._dict[arg1]: if cmd == constants.CLSE: return # Create the `arg0` entry in the `arg1` dict self._dict[arg1][arg0] = Queue() else: if cmd == constants.CLSE: return # Create the `arg1` entry with a new dict self._dict[arg1] = {arg0: Queue()} # Put the data into the queue self._dict[arg1][arg0].put_nowait((cmd, data)) ================================================ FILE: adb_shell/transport/__init__.py ================================================ ================================================ FILE: adb_shell/transport/base_transport.py ================================================ # Copyright (c) 2021 Jeff Irion and contributors # # This file is part of the adb-shell package. """A base class for transports used to communicate with a device. * :class:`BaseTransport` * :meth:`BaseTransport.bulk_read` * :meth:`BaseTransport.bulk_write` * :meth:`BaseTransport.close` * :meth:`BaseTransport.connect` """ try: from abc import ABC, abstractmethod except ImportError: # pragma: no cover from abc import ABCMeta, abstractmethod class ABC(object): # pylint: disable=too-few-public-methods """A Python2-compatible `ABC` class. """ __metaclass__ = ABCMeta class BaseTransport(ABC): """A base transport class. """ @abstractmethod def close(self): """Close the connection. """ @abstractmethod def connect(self, transport_timeout_s): """Create a connection to the device. Parameters ---------- transport_timeout_s : float, None A connection timeout """ @abstractmethod def bulk_read(self, numbytes, transport_timeout_s): """Read data from the device. Parameters ---------- numbytes : int The maximum amount of data to be received transport_timeout_s : float, None A timeout for the read operation Returns ------- bytes The received data """ @abstractmethod def bulk_write(self, data, transport_timeout_s): """Send data to the device. Parameters ---------- data : bytes The data to be sent transport_timeout_s : float, None A timeout for the write operation Returns ------- int The number of bytes sent """ ================================================ FILE: adb_shell/transport/base_transport_async.py ================================================ # Copyright (c) 2021 Jeff Irion and contributors # # This file is part of the adb-shell package. """A base class for transports used to communicate with a device. * :class:`BaseTransportAsync` * :meth:`BaseTransportAsync.bulk_read` * :meth:`BaseTransportAsync.bulk_write` * :meth:`BaseTransportAsync.close` * :meth:`BaseTransportAsync.connect` """ from abc import ABC, abstractmethod class BaseTransportAsync(ABC): """A base transport class. """ @abstractmethod async def close(self): """Close the connection. """ @abstractmethod async def connect(self, transport_timeout_s): """Create a connection to the device. Parameters ---------- transport_timeout_s : float, None A connection timeout """ @abstractmethod async def bulk_read(self, numbytes, transport_timeout_s): """Read data from the device. Parameters ---------- numbytes : int The maximum amount of data to be received transport_timeout_s : float, None A timeout for the read operation Returns ------- bytes The received data """ @abstractmethod async def bulk_write(self, data, transport_timeout_s): """Send data to the device. Parameters ---------- data : bytes The data to be sent transport_timeout_s : float, None A timeout for the write operation Returns ------- int The number of bytes sent """ ================================================ FILE: adb_shell/transport/tcp_transport.py ================================================ # Copyright (c) 2021 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. """A class for creating a socket connection with the device and sending and receiving data. * :class:`TcpTransport` * :meth:`TcpTransport.bulk_read` * :meth:`TcpTransport.bulk_write` * :meth:`TcpTransport.close` * :meth:`TcpTransport.connect` """ import select import socket from .base_transport import BaseTransport from ..exceptions import TcpTimeoutException class TcpTransport(BaseTransport): """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) Attributes ---------- _connection : socket.socket, None A socket connection to the device _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) """ def __init__(self, host, port=5555): self._host = host self._port = port self._connection = None def close(self): """Close the socket connection. """ if self._connection: try: self._connection.shutdown(socket.SHUT_RDWR) except OSError: pass self._connection.close() self._connection = None def connect(self, transport_timeout_s): """Create a socket connection to the device. Parameters ---------- transport_timeout_s : float, None Set the timeout on the socket instance """ self._connection = socket.create_connection((self._host, self._port), timeout=transport_timeout_s) if transport_timeout_s: # Put the socket in non-blocking mode # https://docs.python.org/3/library/socket.html#socket.socket.settimeout self._connection.setblocking(False) def bulk_read(self, numbytes, transport_timeout_s): """Receive data from the socket. Parameters ---------- numbytes : int The maximum amount of data to be received transport_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. """ readable, _, _ = select.select([self._connection], [], [], transport_timeout_s) if readable: return self._connection.recv(numbytes) msg = 'Reading from {}:{} timed out ({} seconds)'.format(self._host, self._port, transport_timeout_s) raise TcpTimeoutException(msg) def bulk_write(self, data, transport_timeout_s): """Send data to the socket. Parameters ---------- data : bytes The data to be sent transport_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. """ _, writeable, _ = select.select([], [self._connection], [], transport_timeout_s) if writeable: return self._connection.send(data) msg = 'Sending data to {}:{} timed out after {} seconds. No data was sent.'.format(self._host, self._port, transport_timeout_s) raise TcpTimeoutException(msg) ================================================ FILE: adb_shell/transport/tcp_transport_async.py ================================================ # Copyright (c) 2021 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:`TcpTransportAsync` * :meth:`TcpTransportAsync.bulk_read` * :meth:`TcpTransportAsync.bulk_write` * :meth:`TcpTransportAsync.close` * :meth:`TcpTransportAsync.connect` """ import asyncio import async_timeout from .base_transport_async import BaseTransportAsync from ..exceptions import TcpTimeoutException class TcpTransportAsync(BaseTransportAsync): """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) Attributes ---------- _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 Object for reading data from the socket _writer : StreamWriter, None Object for writing data to the socket """ def __init__(self, host, port=5555): self._host = host self._port = port 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, transport_timeout_s): """Create a socket connection to the device. Parameters ---------- transport_timeout_s : float, None Timeout for connecting to the socket; if it is ``None``, then it will block until the operation completes """ try: async with async_timeout.timeout(transport_timeout_s): self._reader, self._writer = await asyncio.open_connection(self._host, self._port) except asyncio.TimeoutError as exc: msg = 'Connecting to {}:{} timed out ({} seconds)'.format(self._host, self._port, transport_timeout_s) raise TcpTimeoutException(msg) from exc async def bulk_read(self, numbytes, transport_timeout_s): """Receive data from the socket. Parameters ---------- numbytes : int The maximum amount of data to be received transport_timeout_s : float, None Timeout for reading data from the socket; if it is ``None``, then it will block until the read operation completes Returns ------- bytes The received data Raises ------ TcpTimeoutException Reading timed out. """ try: async with async_timeout.timeout(transport_timeout_s): return await self._reader.read(numbytes) except asyncio.TimeoutError as exc: msg = 'Reading from {}:{} timed out ({} seconds)'.format(self._host, self._port, transport_timeout_s) raise TcpTimeoutException(msg) from exc async def bulk_write(self, data, transport_timeout_s): """Send data to the socket. Parameters ---------- data : bytes The data to be sent transport_timeout_s : float, None Timeout for writing data to the socket; if it is ``None``, then it will block until the write operation completes Returns ------- int The number of bytes sent Raises ------ TcpTimeoutException Sending data timed out. No data was sent. """ try: self._writer.write(data) async with async_timeout.timeout(transport_timeout_s): await self._writer.drain() return len(data) except asyncio.TimeoutError as exc: msg = 'Sending data to {}:{} timed out after {} seconds. No data was sent.'.format(self._host, self._port, transport_timeout_s) raise TcpTimeoutException(msg) from exc ================================================ FILE: adb_shell/transport/usb_transport.py ================================================ # Copyright (c) 2021 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. """A class for creating a USB connection with the device and sending and receiving data. .. warning:: USB support is an experimental feature. * :func:`get_interface` * :func:`interface_matcher` * :class:`UsbTransport` * :meth:`UsbTransport._find` * :meth:`UsbTransport._find_and_open` * :meth:`UsbTransport._find_devices` * :meth:`UsbTransport._find_first` * :meth:`UsbTransport._flush_buffers` * :meth:`UsbTransport._open` * :meth:`UsbTransport._port_path_matcher` * :meth:`UsbTransport._serial_matcher` * :meth:`UsbTransport._timeout` * :meth:`UsbTransport.bulk_read` * :meth:`UsbTransport.bulk_write` * :meth:`UsbTransport.close` * :meth:`UsbTransport.connect` * :attr:`UsbTransport.port_path` * :attr:`UsbTransport.serial_number` * :attr:`UsbTransport.usb_info` """ import logging import platform import re import threading import warnings import weakref import usb1 from .base_transport import BaseTransport from .. import exceptions #: Default timeout DEFAULT_TIMEOUT_S = 10 SYSFS_PORT_SPLIT_RE = re.compile("[,/:.-]") _LOGGER = logging.getLogger(__name__) CLASS = usb1.CLASS_VENDOR_SPEC # pylint: disable=no-member SUBCLASS = 0x42 PROTOCOL = 0x01 def get_interface(setting): # pragma: no cover """Get the class, subclass, and protocol for the given USB setting. Parameters ---------- setting : TODO TODO Returns ------- TODO TODO TODO TODO TODO TODO """ return (setting.getClass(), setting.getSubClass(), setting.getProtocol()) def interface_matcher(clazz, subclass, protocol): # pragma: no cover """Returns a matcher that returns the setting with the given interface. Parameters ---------- clazz : TODO TODO subclass : TODO TODO protocol : TODO TODO Returns ------- matcher : function TODO """ interface = (clazz, subclass, protocol) def matcher(device): """TODO Parameters ---------- device : TODO TODO Returns ------- TODO, None TODO """ for setting in device.iterSettings(): if get_interface(setting) == interface: return setting return None return matcher class UsbTransport(BaseTransport): # pragma: no cover """USB communication object. Not thread-safe. Handles reading and writing over USB with the proper endpoints, exceptions, and interface claiming. Parameters ---------- device : usb1.USBDevice libusb_device to connect to. setting : usb1.USBInterfaceSetting libusb setting with the correct endpoints to communicate with. usb_info : TODO, None String describing the usb path/serial/device, for debugging. default_transport_timeout_s : TODO, None Timeout in seconds for all I/O. Attributes ---------- _default_transport_timeout_s : TODO, None Timeout in seconds for all I/O. _device : TODO libusb_device to connect to. _transport : TODO TODO _interface_number : TODO TODO _max_read_packet_len : TODO TODO _read_endpoint : TODO TODO _setting : TODO libusb setting with the correct endpoints to communicate with. _usb_info : TODO String describing the usb path/serial/device, for debugging. _write_endpoint : TODO, None TODO """ # We maintain an idempotent `usb1` context object to ensure that device # objects we hand back to callers can be used while this class exists USB1_CTX = usb1.USBContext() USB1_CTX.open() _HANDLE_CACHE = weakref.WeakValueDictionary() _HANDLE_CACHE_LOCK = threading.Lock() def __init__(self, device, setting, usb_info=None, default_transport_timeout_s=None): self._setting = setting self._device = device self._transport = None self._interface_number = None self._read_endpoint = None self._write_endpoint = None self._usb_info = usb_info or '' self._default_transport_timeout_s = default_transport_timeout_s if default_transport_timeout_s is not None else DEFAULT_TIMEOUT_S self._max_read_packet_len = 0 def close(self): """Close the USB connection. """ if self._transport is None: return try: self._transport.releaseInterface(self._interface_number) self._transport.close() except usb1.USBError: _LOGGER.info('USBError while closing transport %s: ', self.usb_info, exc_info=True) finally: self._transport = None def connect(self, transport_timeout_s=None): """Create a USB connection to the device. Parameters ---------- transport_timeout_s : float, None Set the timeout on the USB instance """ read_endpoint = None write_endpoint = None for endpoint in self._setting.iterEndpoints(): address = endpoint.getAddress() if address & usb1.ENDPOINT_DIR_MASK: # pylint: disable=no-member read_endpoint = address # max_read_packet_len = endpoint.getMaxPacketSize() else: write_endpoint = address assert read_endpoint is not None assert write_endpoint is not None transport = self._device.open() iface_number = self._setting.getNumber() try: if (platform.system() != 'Windows' and transport.kernelDriverActive(iface_number)): transport.detachKernelDriver(iface_number) except usb1.USBErrorNotFound: # pylint: disable=no-member warnings.warn('Kernel driver not found for interface: %s.', iface_number) # # When this object is deleted, make sure it's closed. # weakref.ref(self, self.close) self._transport = transport self._read_endpoint = read_endpoint self._write_endpoint = write_endpoint self._interface_number = iface_number self._transport.claimInterface(self._interface_number) def bulk_read(self, numbytes, transport_timeout_s=None): """Receive data from the USB device. Parameters ---------- numbytes : int The maximum amount of data to be received transport_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 ------ adb_shell.exceptions.UsbReadFailedError Could not receive data """ if self._transport is None: raise exceptions.UsbReadFailedError('This transport has been closed, probably due to another being opened.', None) try: # python-libusb1 > 1.6 exposes bytearray()s now instead of bytes/str. # To support older and newer versions, we ensure everything's bytearray() # from here on out. return bytes(self._transport.bulkRead(self._read_endpoint, numbytes, timeout=self._timeout_ms(transport_timeout_s))) except usb1.USBError as e: raise exceptions.UsbReadFailedError('Could not receive data from %s (timeout %sms)' % (self.usb_info, self._timeout_ms(transport_timeout_s)), e) def bulk_write(self, data, transport_timeout_s=None): """Send data to the USB device. Parameters ---------- data : bytes The data to be sent transport_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 ------ adb_shell.exceptions.UsbWriteFailedError This transport has been closed, probably due to another being opened adb_shell.exceptions.UsbWriteFailedError Could not send data """ if self._transport is None: raise exceptions.UsbWriteFailedError('This transport has been closed, probably due to another being opened.', None) try: return self._transport.bulkWrite(self._write_endpoint, data, timeout=self._timeout_ms(transport_timeout_s)) except usb1.USBError as e: raise exceptions.UsbWriteFailedError('Could not send data to %s (timeout %sms)' % (self.usb_info, self._timeout_ms(transport_timeout_s)), e) def _open(self): """Opens the USB device for this setting, and claims the interface. """ # Make sure we close any previous transport open to this usb device. port_path = tuple(self.port_path) with self._HANDLE_CACHE_LOCK: old_transport = self._HANDLE_CACHE.get(port_path) if old_transport is not None: old_transport.Close() self._read_endpoint = None self._write_endpoint = None for endpoint in self._setting.iterEndpoints(): address = endpoint.getAddress() if address & usb1.USB_ENDPOINT_DIR_MASK: # pylint: disable=no-member self._read_endpoint = address self._max_read_packet_len = endpoint.getMaxPacketSize() else: self._write_endpoint = address assert self._read_endpoint is not None assert self._write_endpoint is not None transport = self._device.open() iface_number = self._setting.getNumber() try: if (platform.system() != 'Windows' and transport.kernelDriverActive(iface_number)): transport.detachKernelDriver(iface_number) except usb1.USBErrorNotFound: # pylint: disable=no-member warnings.warn('Kernel driver not found for interface: %s.', iface_number) transport.claimInterface(iface_number) self._transport = transport self._interface_number = iface_number with self._HANDLE_CACHE_LOCK: self._HANDLE_CACHE[port_path] = self # When this object is deleted, make sure it's closed. weakref.ref(self, self.close) def _timeout_ms(self, transport_timeout_s): """TODO Returns ------- TODO TODO """ return int(transport_timeout_s * 1000 if transport_timeout_s is not None else self._default_transport_timeout_s * 1000) def _flush_buffers(self): """TODO Raises ------ adb_shell.exceptions.UsbReadFailedError TODO """ while True: try: self.bulk_read(self._max_read_packet_len, transport_timeout_s=10) except exceptions.UsbReadFailedError as e: if isinstance(e.usb_error, usb1.USBErrorTimeout): # pylint: disable=no-member break raise # ======================================================================= # # # # Properties # # # # ======================================================================= # @property def port_path(self): """TODO Returns ------- TODO TODO """ return [self._device.getBusNumber()] + self._device.getPortNumberList() @property def serial_number(self): """TODO Returns ------- TODO TODO """ return self._device.getSerialNumber() @property def usb_info(self): """TODO Returns ------- TODO TODO """ try: sn = self.serial_number except usb1.USBError: sn = '' if sn and sn != self._usb_info: return '%s %s' % (self._usb_info, sn) return self._usb_info # ======================================================================= # # # # Matchers # # # # ======================================================================= # @classmethod def _port_path_matcher(cls, port_path): """Returns a device matcher for the given port path. Parameters ---------- port_path : TODO TODO Returns ------- function TODO """ if isinstance(port_path, str): # Convert from sysfs path to port_path. port_path = [int(part) for part in SYSFS_PORT_SPLIT_RE.split(port_path)] return lambda device: device.port_path == port_path @classmethod def _serial_matcher(cls, serial): """Returns a device matcher for the given serial. Parameters ---------- serial : TODO TODO Returns ------- function TODO """ return lambda device: device.serial_number == serial # ======================================================================= # # # # Finders # # # # ======================================================================= # @classmethod def _find(cls, setting_matcher, port_path=None, serial=None, default_transport_timeout_s=None): """Gets the first device that matches according to the keyword args. Parameters ---------- setting_matcher : TODO TODO port_path : TODO, None TODO serial : TODO, None TODO default_transport_timeout_s : TODO, None TODO Returns ------- TODO TODO """ if port_path: device_matcher = cls._port_path_matcher(port_path) usb_info = port_path elif serial: device_matcher = cls._serial_matcher(serial) usb_info = serial else: device_matcher = None usb_info = 'first' return cls._find_first(setting_matcher, device_matcher, usb_info=usb_info, default_transport_timeout_s=default_transport_timeout_s) @classmethod def _find_and_open(cls, setting_matcher, port_path=None, serial=None, default_transport_timeout_s=None): """TODO Parameters ---------- setting_matcher : TODO TODO port_path : TODO, None TODO serial : TODO, None TODO default_transport_timeout_s : TODO, None TODO Returns ------- dev : TODO TODO """ dev = cls._find(setting_matcher, port_path=port_path, serial=serial, default_transport_timeout_s=default_transport_timeout_s) dev._open() # pylint: disable=protected-access dev._flush_buffers() # pylint: disable=protected-access return dev @classmethod def _find_devices(cls, setting_matcher, device_matcher=None, usb_info='', default_transport_timeout_s=None): """_find and yield the devices that match. Parameters ---------- setting_matcher : TODO Function that returns the setting to use given a ``usb1.USBDevice``, or ``None`` if the device doesn't have a valid setting. device_matcher : TODO, None Function that returns ``True`` if the given ``UsbTransport`` is valid. ``None`` to match any device. usb_info : str Info string describing device(s). default_transport_timeout_s : TODO, None Default timeout of commands in seconds. Yields ------ TODO UsbTransport instances """ for device in cls.USB1_CTX.getDeviceIterator(skip_on_error=True): setting = setting_matcher(device) if setting is None: continue transport = cls(device, setting, usb_info=usb_info, default_transport_timeout_s=default_transport_timeout_s) if device_matcher is None or device_matcher(transport): yield transport @classmethod def _find_first(cls, setting_matcher, device_matcher=None, usb_info='', default_transport_timeout_s=None): """Find and return the first matching device. Parameters ---------- setting_matcher : TODO Function that returns the setting to use given a ``usb1.USBDevice``, or ``None`` if the device doesn't have a valid setting. device_matcher : TODO Function that returns ``True`` if the given ``UsbTransport`` is valid. ``None`` to match any device. usb_info : str Info string describing device(s). default_transport_timeout_s : TODO, None Default timeout of commands in seconds. Returns ------- TODO An instance of `UsbTransport` Raises ------ adb_shell.exceptions.DeviceNotFoundError Raised if the device is not available. """ try: return next(cls._find_devices(setting_matcher, device_matcher=device_matcher, usb_info=usb_info, default_transport_timeout_s=default_transport_timeout_s)) except StopIteration: raise exceptions.UsbDeviceNotFoundError('No device available, or it is in the wrong configuration.') @classmethod def find_adb(cls, serial=None, port_path=None, default_transport_timeout_s=None): """TODO Parameters ---------- serial : TODO TODO port_path : TODO TODO default_transport_timeout_s : TODO, None Default timeout of commands in seconds. Returns ------- UsbTransport TODO """ return cls._find( interface_matcher(CLASS, SUBCLASS, PROTOCOL), serial=serial, port_path=port_path, default_transport_timeout_s=default_transport_timeout_s ) @classmethod def find_all_adb_devices(cls, default_transport_timeout_s=None): """Find all ADB devices attached via USB. Parameters ---------- default_transport_timeout_s : TODO, None Default timeout of commands in seconds. Returns ------- generator A generator which yields each ADB device attached via USB. """ yield from cls._find_devices(interface_matcher(CLASS, SUBCLASS, PROTOCOL), default_transport_timeout_s=default_transport_timeout_s) ================================================ FILE: docs/Makefile ================================================ # Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SPHINXPROJ = adb_shell SOURCEDIR = source BUILDDIR = build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) ================================================ FILE: docs/make.bat ================================================ @ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=source set BUILDDIR=build set SPHINXPROJ=adb_shell if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% :end popd ================================================ FILE: docs/requirements.txt ================================================ # Standard requirements sphinx sphinx-rtd-theme # Specific requirements for this project adb-shell[async,usb] pycryptodome ================================================ FILE: docs/source/adb_shell.adb_device.rst ================================================ adb\_shell.adb\_device module ============================= .. automodule:: adb_shell.adb_device :members: :undoc-members: :show-inheritance: ================================================ FILE: docs/source/adb_shell.adb_device_async.rst ================================================ adb\_shell.adb\_device\_async module ==================================== .. automodule:: adb_shell.adb_device_async :members: :undoc-members: :show-inheritance: ================================================ FILE: docs/source/adb_shell.adb_message.rst ================================================ adb\_shell.adb\_message module ============================== .. automodule:: adb_shell.adb_message :members: :undoc-members: :show-inheritance: ================================================ FILE: docs/source/adb_shell.auth.keygen.rst ================================================ adb\_shell.auth.keygen module ============================= .. automodule:: adb_shell.auth.keygen :members: :undoc-members: :show-inheritance: ================================================ FILE: docs/source/adb_shell.auth.rst ================================================ adb\_shell.auth package ======================= Submodules ---------- .. toctree:: adb_shell.auth.keygen adb_shell.auth.sign_cryptography adb_shell.auth.sign_pycryptodome adb_shell.auth.sign_pythonrsa Module contents --------------- .. automodule:: adb_shell.auth :members: :undoc-members: :show-inheritance: ================================================ FILE: docs/source/adb_shell.auth.sign_cryptography.rst ================================================ adb\_shell.auth.sign\_cryptography module ========================================= .. automodule:: adb_shell.auth.sign_cryptography :members: :undoc-members: :show-inheritance: ================================================ FILE: docs/source/adb_shell.auth.sign_pycryptodome.rst ================================================ adb\_shell.auth.sign\_pycryptodome module ========================================= .. automodule:: adb_shell.auth.sign_pycryptodome :members: :undoc-members: :show-inheritance: ================================================ FILE: docs/source/adb_shell.auth.sign_pythonrsa.rst ================================================ adb\_shell.auth.sign\_pythonrsa module ====================================== .. automodule:: adb_shell.auth.sign_pythonrsa :members: :undoc-members: :show-inheritance: ================================================ FILE: docs/source/adb_shell.constants.rst ================================================ adb\_shell.constants module =========================== .. automodule:: adb_shell.constants :members: :undoc-members: :show-inheritance: ================================================ FILE: docs/source/adb_shell.exceptions.rst ================================================ adb\_shell.exceptions module ============================ .. automodule:: adb_shell.exceptions :members: :undoc-members: :show-inheritance: ================================================ FILE: docs/source/adb_shell.hidden_helpers.rst ================================================ adb\_shell.hidden\_helpers module ================================= .. automodule:: adb_shell.hidden_helpers :members: :undoc-members: :show-inheritance: ================================================ FILE: docs/source/adb_shell.rst ================================================ adb\_shell package ================== Subpackages ----------- .. toctree:: adb_shell.auth adb_shell.transport 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 --------------- .. automodule:: adb_shell :members: :undoc-members: :show-inheritance: ================================================ FILE: docs/source/adb_shell.transport.base_transport.rst ================================================ adb\_shell.transport.base\_transport module =========================================== .. automodule:: adb_shell.transport.base_transport :members: :undoc-members: :show-inheritance: ================================================ FILE: docs/source/adb_shell.transport.base_transport_async.rst ================================================ adb\_shell.transport.base\_transport\_async module ================================================== .. automodule:: adb_shell.transport.base_transport_async :members: :undoc-members: :show-inheritance: ================================================ FILE: docs/source/adb_shell.transport.rst ================================================ adb\_shell.transport package ============================ Submodules ---------- .. toctree:: adb_shell.transport.base_transport adb_shell.transport.base_transport_async adb_shell.transport.tcp_transport adb_shell.transport.tcp_transport_async adb_shell.transport.usb_transport Module contents --------------- .. automodule:: adb_shell.transport :members: :undoc-members: :show-inheritance: ================================================ FILE: docs/source/adb_shell.transport.tcp_transport.rst ================================================ adb\_shell.transport.tcp\_transport module ========================================== .. automodule:: adb_shell.transport.tcp_transport :members: :undoc-members: :show-inheritance: ================================================ FILE: docs/source/adb_shell.transport.tcp_transport_async.rst ================================================ adb\_shell.transport.tcp\_transport\_async module ================================================= .. automodule:: adb_shell.transport.tcp_transport_async :members: :undoc-members: :show-inheritance: ================================================ FILE: docs/source/adb_shell.transport.usb_transport.rst ================================================ adb\_shell.transport.usb\_transport module ========================================== .. automodule:: adb_shell.transport.usb_transport :members: :undoc-members: :show-inheritance: ================================================ FILE: docs/source/conf.py ================================================ # -*- coding: utf-8 -*- # # Configuration file for the Sphinx documentation builder. # # This file does only contain a selection of the most common options. For a # full list see the documentation: # http://www.sphinx-doc.org/en/master/config # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # import os import sys sys.path.insert(0, os.path.abspath('../..')) import sphinx_rtd_theme # -- Project information ----------------------------------------------------- project = 'adb_shell' copyright = '2021, Jeff Irion and contributors' author = 'Jeff Irion' # The short X.Y version version = '0.4.4' # The full version, including alpha/beta/rc tags release = '0.4.4' # -- General configuration --------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.todo', 'sphinx.ext.mathjax', 'sphinx.ext.viewcode', 'sphinx.ext.autodoc', 'sphinx.ext.napoleon' ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The master toctree document. master_doc = 'index' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path . exclude_patterns = [] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' autodoc_mock_imports = ['libusb1', 'usb1'] autodoc_default_options = {'members': True, 'undoc-members': True, 'private-members': True, 'show-inheritance': True} # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # # html_theme_options = {} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Custom sidebar templates, must be a dictionary that maps document names # to template names. # # The default sidebars (for documents that don't match any pattern) are # defined by theme itself. Builtin themes are using these templates by # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', # 'searchbox.html']``. # # html_sidebars = {} # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. htmlhelp_basename = 'AdbShellDoc' # -- Options for LaTeX output ------------------------------------------------ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'AdbShell.tex', 'ADB Shell Documentation', 'Jeff Irion', 'manual'), ] # -- Options for manual page output ------------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'adb_shell', 'ADB Shell Documentation', [author], 1) ] # -- Options for Texinfo output ---------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'AdbShell', 'ADB Shell Documentation', author, 'AdbShell', 'One line description of project.', 'Miscellaneous'), ] # -- Extension configuration ------------------------------------------------- # -- Options for todo extension ---------------------------------------------- # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True ================================================ FILE: docs/source/index.rst ================================================ .. Jeff Irion's Python package documentation master file, created by sphinx-quickstart on Mon Sep 05 22:06:10 2016. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. adb\_shell Documentation ======================== .. toctree:: :hidden: self modules.rst .. include:: ../../README.rst :start-line: 15 Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` ================================================ FILE: docs/source/modules.rst ================================================ adb_shell ========= .. toctree:: :maxdepth: 4 adb_shell ================================================ FILE: scripts/bumpversion.sh ================================================ #!/bin/bash # Make sure there is only 1 argument passed if [ "$#" -ne 1 ]; then echo "You must provide a new version" exit 1 fi # Make sure the new version is not empty if [ -z "$1" ]; then echo "You must provide a non-empty version" exit 1 fi # get the directory of this script DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" # get the package name PACKAGE=$($DIR/get_package_name.sh) # get the current version VERSION=$($DIR/get_version.sh) # Announce the version bump echo "Bumping the version from $VERSION to $1" # __init__.py sed -i "s|__version__ = \"$VERSION\"|__version__ = \"$1\"|g" $DIR/../$PACKAGE/__init__.py # setup.py sed -i "s|version=\"$VERSION\",|version=\"$1\",|g" $DIR/../setup.py # conf.py sed -i "s|version = '$VERSION'|version = '$1'|g" $DIR/../docs/source/conf.py sed -i "s|release = '$VERSION'|release = '$1'|g" $DIR/../docs/source/conf.py ================================================ FILE: scripts/get_package_name.sh ================================================ #!/bin/bash set -e # get the directory of this script DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" RSTRIP='"*' LSTRIP='*"' # get the package name PACKAGE_LINE=$(grep "name=" $DIR/../setup.py || echo '') PACKAGE_TEMP=${PACKAGE_LINE%$RSTRIP} PACKAGE=${PACKAGE_TEMP##$LSTRIP} # Make sure `PACKAGE` is not empty if [ -z "$PACKAGE" ]; then echo "Package name could not be determined" >&2 exit 1 fi echo "$PACKAGE" ================================================ FILE: scripts/get_version.sh ================================================ #!/bin/bash set -e # get the directory of this script DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" RSTRIP='"*' LSTRIP='*"' # get the package name PACKAGE=$($DIR/get_package_name.sh) # get the current version VERSION_LINE=$(grep "__version__" "$DIR/../$PACKAGE/__init__.py" || echo '') VERSION_TEMP=${VERSION_LINE%'"'} VERSION=${VERSION_TEMP##$LSTRIP} # Make sure `VERSION` is not empty if [ -z "$VERSION" ]; then echo "Version could not be determined" >&2 exit 1 fi echo "$VERSION" ================================================ FILE: scripts/git_retag.sh ================================================ #!/bin/bash # get the directory of this script DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" # get the package name PACKAGE=$($DIR/get_package_name.sh) # get the current version VERSION=$($DIR/get_version.sh) # Announce the tag echo "Re-tagging v$VERSION" cd $DIR/.. # https://stackoverflow.com/a/8044605 git push origin ":refs/tags/v$VERSION" git tag -fa "v$VERSION" git push origin master --tags ================================================ FILE: scripts/git_tag.sh ================================================ #!/bin/bash # get the directory of this script DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" # get the current version VERSION=$($DIR/get_version.sh) # Announce the tag echo "Creating tag v$VERSION" cd $DIR/.. git tag v$VERSION -m "v$VERSION" git push --tags ================================================ FILE: scripts/pre-commit.sh ================================================ #!/bin/bash set -e function make_pre_commit() { # setup pre-commit hook DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" echo -e "#!/bin/bash\n\n./scripts/pre-commit.sh 'placeholder_argument'" > "$DIR/../.git/hooks/pre-commit" chmod a+x "$DIR/../.git/hooks/pre-commit" echo "pre-commit hook successfully configured" } # if no arguments are passed, create the pre-commit hook if [ "$#" -eq 0 ]; then read -p "Do you want to setup the git pre-commit hook? [Y/n] " -n 1 -r echo if [[ $REPLY =~ ^[Yy]$ ]]; then make_pre_commit else echo "pre-commit hook not configured" fi exit 0 fi # if the argument passed is "MAKE_PRECOMMIT_HOOK", then make the pre-commit hook if [[ $1 == "MAKE_PRECOMMIT_HOOK" ]]; then make_pre_commit exit 0 fi # THE PRE-COMMIT HOOK # get the directory of this script DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" ( cd "$DIR/.." no_unstaged_changes=true echo -e "\n\033[1m1. Checking for unstaged changes...\033[0m" for staged_file in $(git diff --name-only --cached); do git diff --name-only | grep -q "${staged_file}" && echo "You have unstaged changes in '${staged_file}'" && no_unstaged_changes=false || true done # modified .py files pyfiles=$(git diff --cached --name-only -- '*.py') # flake8 flake8_pass=true if [ "$pyfiles" != "" ]; then echo -e "\n\033[1m2. Running flake8...\033[0m" venv/bin/flake8 $pyfiles || flake8_pass=false else echo -e "\n\033[1m2. Skipping flake8.\033[0m" fi # pylint pylint_pass=true if [ "$pyfiles" != "" ]; then echo -e "\n\033[1m3. Running pylint...\033[0m" venv/bin/pylint $pyfiles || pylint_pass=false else echo -e "\n\033[1m3. Skipping pylint.\033[0m\n" fi if [ "$flake8_pass" != "true" ] || [ "$pylint_pass" != "true" ] || [ "$no_unstaged_changes" != "true" ]; then echo -e "\033[1m\033[31mSome checks failed.\033[0m\n\n NOT RECOMMENDED: If you want to skip the pre-commit hook, use the --no-verify flag.\n" exit 1 fi echo -e "\033[1m\033[32mAll checks passed.\033[0m\n" ) ================================================ FILE: scripts/rename_package.sh ================================================ #!/bin/bash set -e # Make sure there is only 1 argument passed if [ "$#" -ne 1 ]; then echo "You must provide a new package name" exit 1 fi # Make sure the new package name is not empty if [ -z "$1" ]; then echo "You must provide a non-empty package name" exit 1 fi # get the directory of this script DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" # get the current package name PACKAGE=$($DIR/get_package_name.sh) # Announce the renaming echo "Renaming from '$PACKAGE' to '$1'" # .gitignore sed -i "s|$PACKAGE|$1|g" $DIR/../.gitignore # Doxyfile sed -i "s|$PACKAGE|$1|g" $DIR/../Doxyfile # Makefile sed -i "s|$PACKAGE|$1|g" $DIR/../Makefile # setup.py sed -i "s|$PACKAGE|$1|g" $DIR/../setup.py # docs/Makefile sed -i "s|$PACKAGE|$1|g" $DIR/../docs/Makefile # docs/make.bat sed -i "s|$PACKAGE|$1|g" $DIR/../docs/make.bat # docs/source/conf.py sed -i "s|$PACKAGE|$1|g" $DIR/../docs/source/conf.py ================================================ FILE: setup.py ================================================ """setup.py file for the adb_shell package.""" from setuptools import setup with open("README.rst") as f: readme = f.read() setup( name="adb_shell", version="0.4.4", description="A Python implementation of ADB with shell and FileSync functionality.", long_description=readme, keywords=["adb", "android"], url="https://github.com/JeffLIrion/adb_shell", author="Jeff Irion", author_email="jefflirion@users.noreply.github.com", packages=["adb_shell", "adb_shell.auth", "adb_shell.transport"], install_requires=["cryptography", "pyasn1", "rsa"], tests_require=["pycryptodome", "libusb1>=1.0.16"], extras_require={"usb": ["libusb1>=1.0.16"], "async": ["aiofiles>=0.4.0", "async_timeout>=3.0.0"]}, classifiers=[ "Operating System :: OS Independent", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 2", ], test_suite="tests", ) ================================================ FILE: tests/__init__.py ================================================ ================================================ FILE: tests/async_patchers.py ================================================ try: from contextlib import asynccontextmanager except ImportError: asynccontextmanager = lambda func: func from unittest.mock import patch from adb_shell import constants from adb_shell.adb_message import AdbMessage, unpack from adb_shell.transport.tcp_transport_async import TcpTransportAsync 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) def async_mock_open(read_data=""): class AsyncMockFile: def __init__(self, read_data): self.read_data = read_data _async_mock_open.written = read_data[:0] async def read(self, size=-1): if size == -1: ret = self.read_data self.read_data = self.read_data[:0] return ret n = min(size, len(self.read_data)) ret = self.read_data[:n] self.read_data = self.read_data[n:] return ret async def write(self, b): if _async_mock_open.written: _async_mock_open.written += b else: _async_mock_open.written = b def fileno(self): return 123 @asynccontextmanager async def _async_mock_open(*args, **kwargs): try: yield AsyncMockFile(read_data) finally: pass return _async_mock_open 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 FakeTcpTransportAsync(TcpTransportAsync): def __init__(self, *args, **kwargs): TcpTransportAsync.__init__(self, *args, **kwargs) self.bulk_read_data = b'' self.bulk_write_data = b'' async def close(self): self._reader = None self._writer = None async def connect(self, transport_timeout_s=None): self._reader = True self._writer = True async def bulk_read(self, numbytes, transport_timeout_s=None): num = min(numbytes, constants.MAX_ADB_DATA) ret = self.bulk_read_data[:num] self.bulk_read_data = self.bulk_read_data[num:] return ret async def bulk_write(self, data, transport_timeout_s=None): self.bulk_write_data += data return len(data) # `TcpTransport` patches PATCH_TCP_TRANSPORT_ASYNC = patch('adb_shell.adb_device_async.TcpTransportAsync', FakeTcpTransportAsync) def async_patch(*args, **kwargs): return patch(*args, new_callable=AsyncMock, **kwargs) ================================================ FILE: tests/async_wrapper.py ================================================ import asyncio import warnings def _await(coro): """Create a new event loop, run the coroutine, then close the event loop.""" loop = asyncio.new_event_loop() with warnings.catch_warnings(record=True) as warns: ret = loop.run_until_complete(coro) loop.close() if warns: raise RuntimeError return ret def awaiter(func): def sync_func(*args, **kwargs): return _await(func(*args, **kwargs)) return sync_func ================================================ FILE: tests/filesync_helpers.py ================================================ import struct from adb_shell import constants class FileSyncMessage(object): # pylint: disable=too-few-public-methods """A helper class for packing FileSync messages. Parameters ---------- command : bytes TODO arg0 : int TODO data : bytes The data that will be sent Attributes ---------- arg0 : int TODO command : int The input parameter ``command`` converted to an integer via :const:`adb_shell.constants.FILESYNC_ID_TO_WIRE` data : bytes The data that will be sent """ def __init__(self, command, arg0=None, data=b''): self.command = constants.FILESYNC_ID_TO_WIRE[command] self.arg0 = arg0 or len(data) self.data = data def pack(self): """Returns this message in an over-the-wire format. Returns ------- bytes The message packed into the format required by ADB """ return struct.pack(b'<2I', self.command, self.arg0) class FileSyncListMessage(object): # pylint: disable=too-few-public-methods """A helper class for packing FileSync messages for the "list" service". Parameters ---------- command : bytes TODO arg0 : int TODO arg1 : TODO TODO arg2 : TODO TODO data : bytes The data that will be sent Attributes ---------- arg0 : int TODO arg1 : TODO TODO arg2 : TODO TODO arg3 : int The size of the data command : int The input parameter ``command`` converted to an integer via :const:`adb_shell.constants.FILESYNC_ID_TO_WIRE` data : bytes TODO """ def __init__(self, command, arg0, arg1, arg2, data=b''): self.command = constants.FILESYNC_ID_TO_WIRE[command] self.arg0 = arg0 self.arg1 = arg1 self.arg2 = arg2 self.arg3 = len(data) self.data = data def pack(self): """Returns this message in an over-the-wire format. Returns ------- bytes The message packed into the format required by ADB """ return struct.pack(b'<5I', self.command, self.arg0, self.arg1, self.arg2, self.arg3) class FileSyncStatMessage(object): # pylint: disable=too-few-public-methods """A helper class for packing FileSync messages for the "stat" service". Parameters ---------- command : bytes TODO arg0 : int TODO arg1 : TODO TODO arg2 : TODO TODO Attributes ---------- arg0 : int TODO arg1 : TODO TODO arg2 : TODO TODO command : int The input parameter ``command`` converted to an integer via :const:`adb_shell.constants.FILESYNC_ID_TO_WIRE` data : bytes The data that will be sent (always empty) """ def __init__(self, command, arg0, arg1, arg2): self.command = constants.FILESYNC_ID_TO_WIRE[command] self.arg0 = arg0 self.arg1 = arg1 self.arg2 = arg2 self.data = b'' def pack(self): """Returns this message in an over-the-wire format. Returns ------- bytes The message packed into the format required by ADB """ return struct.pack(b'<4I', self.command, self.arg0, self.arg1, self.arg2) ================================================ FILE: tests/keygen_stub.py ================================================ from contextlib import contextmanager try: from unittest.mock import patch except ImportError: from mock import patch class FileReadWrite(object): """Mock an opened file that can be read and written to.""" def __init__(self): self._content = b'' self._mode = 'r' def read(self): if self._mode == 'r': if not isinstance(self._content, str): return self._content.decode() return self._content if isinstance(self._content, str): return self._content.encode('utf-8') return self._content def write(self, content): self._content = content PRIVATE_KEY = FileReadWrite() PUBLIC_KEY = FileReadWrite() @contextmanager def open_priv_pub(infile, mode='r'): try: if infile.endswith('.pub'): PUBLIC_KEY._mode = mode yield PUBLIC_KEY else: PRIVATE_KEY._mode = mode yield PRIVATE_KEY finally: pass ================================================ FILE: tests/patchers.py ================================================ from collections import namedtuple from contextlib import contextmanager import sys import unittest try: from unittest.mock import patch except ImportError: from mock import patch from adb_shell import constants from adb_shell.adb_message import AdbMessage from adb_shell.transport.tcp_transport import TcpTransport ASYNC_SKIPPER=unittest.skipIf(sys.version_info.major < 3 or sys.version_info.minor < 7, "Async functionality requires Python 3.7+") MSG_CONNECT = AdbMessage(command=constants.CNXN, arg0=constants.PROTOCOL, arg1=constants.MAX_LEGACY_ADB_DATA, 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') MSG_CONNECT_WITH_AUTH2 = AdbMessage(command=constants.CNXN, arg0=constants.PROTOCOL, arg1=2*constants.MAX_LEGACY_ADB_DATA, data=b'host::unknown\0') MSG_CONNECT_WITH_AUTH_NEW_KEY2 = AdbMessage(command=constants.AUTH, arg0=0, arg1=0, data=b'host::unknown\0') MSG_CONNECT_WITH_AUTH_NEW_KEY3 = AdbMessage(command=constants.CNXN, arg0=constants.PROTOCOL, arg1=3*constants.MAX_LEGACY_ADB_DATA, data=b'host::unknown\0') BULK_READ_LIST = [MSG_CONNECT.pack(), MSG_CONNECT.data] BULK_READ_LIST_WITH_AUTH_INVALID = [MSG_CONNECT_WITH_AUTH_INVALID.pack(), MSG_CONNECT_WITH_AUTH_INVALID.data] BULK_READ_LIST_WITH_AUTH = [MSG_CONNECT_WITH_AUTH1.pack(), MSG_CONNECT_WITH_AUTH1.data, MSG_CONNECT_WITH_AUTH2.pack(), MSG_CONNECT_WITH_AUTH2.data] BULK_READ_LIST_WITH_AUTH_NEW_KEY = [MSG_CONNECT_WITH_AUTH1.pack(), MSG_CONNECT_WITH_AUTH1.data, MSG_CONNECT_WITH_AUTH_NEW_KEY2.pack(), MSG_CONNECT_WITH_AUTH_NEW_KEY2.data, MSG_CONNECT_WITH_AUTH_NEW_KEY3.pack(), MSG_CONNECT_WITH_AUTH_NEW_KEY3.data] StSize = namedtuple("StSize", ["st_size"]) def mock_open(read_data=""): class MockFile: def __init__(self, read_data): self.read_data = read_data _mock_open.written = read_data[:0] def read(self, size=-1): if size == -1: ret = self.read_data self.read_data = self.read_data[:0] return ret n = min(size, len(self.read_data)) ret = self.read_data[:n] self.read_data = self.read_data[n:] return ret def write(self, b): if _mock_open.written: _mock_open.written += b else: _mock_open.written = b def fileno(self): return 123 @contextmanager def _mock_open(*args, **kwargs): try: yield MockFile(read_data) finally: pass return _mock_open class FakeSocket(object): def __init__(self): self._recv = b'' def close(self): pass def recv(self, bufsize): ret = self._recv[:bufsize] self._recv = self._recv[bufsize:] return ret def send(self, data): pass def setblocking(self, *args, **kwargs): pass def shutdown(self, how): pass class FakeTcpTransport(TcpTransport): def __init__(self, *args, **kwargs): TcpTransport.__init__(self, *args, **kwargs) self.bulk_read_data = b'' self.bulk_write_data = b'' def close(self): self._connection = None def connect(self, transport_timeout_s=None): self._connection = True def bulk_read(self, numbytes, transport_timeout_s=None): num = min(numbytes, constants.MAX_ADB_DATA) ret = self.bulk_read_data[:num] self.bulk_read_data = self.bulk_read_data[num:] return ret def bulk_write(self, data, transport_timeout_s=None): self.bulk_write_data += data return len(data) # `socket` patches PATCH_CREATE_CONNECTION = patch('socket.create_connection', return_value=FakeSocket()) # `select` patches PATCH_SELECT_SUCCESS = patch('select.select', return_value=(True, True, True)) PATCH_SELECT_FAIL = patch('select.select', return_value=(False, False, False)) # `TcpTransport` patches PATCH_TCP_TRANSPORT = patch('adb_shell.adb_device.TcpTransport', FakeTcpTransport) ================================================ FILE: tests/test_adb_device.py ================================================ import inspect import logging from io import BytesIO import struct import sys import time import unittest try: from unittest.mock import patch except ImportError: from mock import patch from adb_shell import adb_device, constants, exceptions from adb_shell.adb_device import AdbDevice, AdbDeviceTcp, 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 .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') _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 class TestAdbDevice(unittest.TestCase): def setUp(self): self.transport = patchers.FakeTcpTransport('host', 5555) self.device = AdbDevice(transport=self.transport) self.transport.bulk_read_data = b''.join(patchers.BULK_READ_LIST) self.progress_callback_count = 0 def _progress_callback(device_path, current, total_bytes): print("device_path = {}, current = {}, total_bytes = {}".format(device_path, current, total_bytes)) self.progress_callback_count += 1 self.progress_callback = _progress_callback def tearDown(self): self.assertFalse(self.transport.bulk_read_data) self.assertEqual(len(self.device._io_manager._packet_store._dict), 0) @staticmethod def fake_stat(*args, **kwargs): return 1, 2, 3 def test_no_async_references(self): """Make sure there are no references to async code.""" adb_device_source = inspect.getsource(adb_device) self.assertTrue("base_transport_async" not in adb_device_source) self.assertTrue("BaseTransportAsync" not in adb_device_source) self.assertTrue("adb_device_async" not in adb_device_source) self.assertTrue("AdbDeviceAsync" not in adb_device_source) self.assertTrue("async" not in adb_device_source) self.assertTrue("Async" not in adb_device_source) self.transport.bulk_read_data = b'' def test_adb_connection_error(self): with self.assertRaises(exceptions.AdbConnectionError): self.device.exec_out('FAIL') with self.assertRaises(exceptions.AdbConnectionError): self.device.root() with self.assertRaises(exceptions.AdbConnectionError): self.device.shell('FAIL') with self.assertRaises(exceptions.AdbConnectionError): ''.join(self.device.streaming_shell('FAIL')) with self.assertRaises(exceptions.AdbConnectionError): self.device.reboot() with self.assertRaises(exceptions.AdbConnectionError): self.device.root() with self.assertRaises(exceptions.AdbConnectionError): self.device.list('FAIL') with self.assertRaises(exceptions.AdbConnectionError): self.device.push('FAIL', 'FAIL') with self.assertRaises(exceptions.AdbConnectionError): self.device.pull('FAIL', 'FAIL') with self.assertRaises(exceptions.AdbConnectionError): self.device.stat('FAIL') self.transport.bulk_read_data = b'' def test_init_tcp(self): with patchers.PATCH_TCP_TRANSPORT: tcp_device = AdbDeviceTcp('host') tcp_device._io_manager._transport.bulk_read_data = self.transport.bulk_read_data # Make sure that the `connect()` method works self.assertTrue(tcp_device.connect()) self.assertTrue(tcp_device.available) # Clear the `_bulk_read` buffer so that `self.tearDown()` passes self.transport.bulk_read_data = b'' def test_init_banner(self): device_with_banner = AdbDevice(transport=patchers.FakeTcpTransport('host', 5555), banner='banner') self.assertEqual(device_with_banner._banner, b'banner') device_with_banner2 = AdbDevice(transport=patchers.FakeTcpTransport('host', 5555), banner=bytearray('banner2', 'utf-8')) self.assertEqual(device_with_banner2._banner, b'banner2') device_with_banner3 = AdbDevice(transport=patchers.FakeTcpTransport('host', 5555), banner=u'banner3') self.assertEqual(device_with_banner3._banner, b'banner3') with patch('socket.gethostname', side_effect=Exception): device_banner_unknown = AdbDevice(transport=self.transport) self.assertTrue(device_banner_unknown.connect()) self.assertEqual(device_banner_unknown._banner, b'unknown') def test_init_invalid_transport(self): with self.assertRaises(exceptions.InvalidTransportError): device = AdbDevice(transport=123) # Clear the `_bulk_read` buffer so that `self.tearDown()` passes self.transport.bulk_read_data = b'' def test_available(self): self.assertFalse(self.device.available) # Clear the `_bulk_read` buffer so that `self.tearDown()` passes self.transport.bulk_read_data = b'' def test_close(self): self.assertFalse(self.device.close()) self.assertFalse(self.device.available) # Clear the `_bulk_read` buffer so that `self.tearDown()` passes self.transport.bulk_read_data = b'' # ======================================================================= # # # # `connect` tests # # # # ======================================================================= # def test_connect(self): self.assertTrue(self.device.connect()) self.assertTrue(self.device.available) def test_connect_no_keys(self): self.transport.bulk_read_data = b''.join(patchers.BULK_READ_LIST_WITH_AUTH[:2]) with self.assertRaises(exceptions.DeviceAuthError): self.device.connect() self.assertFalse(self.device.available) 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.transport.bulk_read_data = b''.join(patchers.BULK_READ_LIST_WITH_AUTH_INVALID) with self.assertRaises(exceptions.InvalidResponseError): self.device.connect([signer]) self.assertFalse(self.device.available) 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.transport.bulk_read_data = b''.join(patchers.BULK_READ_LIST_WITH_AUTH) self.assertTrue(self.device.connect([signer])) 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.transport.bulk_read_data = b''.join(patchers.BULK_READ_LIST_WITH_AUTH_NEW_KEY) self.assertTrue(self.device.connect([signer])) 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.transport.bulk_read_data = b''.join(patchers.BULK_READ_LIST_WITH_AUTH_NEW_KEY) self.assertTrue(self.device.connect([signer], auth_callback=auth_callback)) self.assertTrue(self._callback_invoked) def test_connect_timeout(self): self.transport.bulk_read_data = AdbMessage(command=constants.CLSE, arg0=1, arg1=1).pack() with self.assertRaises(exceptions.AdbTimeoutError): # Use a negative timeout to ensure that only one packet gets read self.device.connect([], read_timeout_s=-1) # ======================================================================= # # # # `shell` tests # # # # ======================================================================= # def test_shell_no_return(self): self.assertTrue(self.device.connect()) # Provide the `bulk_read` return values self.transport.bulk_read_data = 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(self.device.shell('TEST'), '') def test_shell_return_pass(self): self.assertTrue(self.device.connect()) # Provide the `bulk_read` return values self.transport.bulk_read_data = 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(self.device.shell('TEST'), 'PASS') def test_shell_local_id_wraparound(self): self.assertTrue(self.device.connect()) # Provide the `bulk_read` return values self.transport.bulk_read_data = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=2**32 - 1, data=b'\x00'), AdbMessage(command=constants.WRTE, arg0=1, arg1=2**32 - 1, data=b'PASS1'), AdbMessage(command=constants.CLSE, arg0=1, arg1=2**32 - 1, data=b''), AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=b'PASS2'), AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) self.device._local_id = 2**32 - 2 self.assertEqual(self.device.shell('TEST'), 'PASS1') self.assertEqual(self.device.shell('TEST'), 'PASS2') def test_shell_return_pass_with_unexpected_packet(self): self.assertTrue(self.device.connect()) # Provide the `bulk_read` return values self.transport.bulk_read_data = 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.AUTH, arg0=1, arg1=1, data=b'UNEXPECTED'), AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=b'SS'), AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) self.assertEqual(self.device.shell('TEST'), 'PASS') def test_shell_dont_decode(self): self.assertTrue(self.device.connect()) # Provide the `bulk_read` return values self.transport.bulk_read_data = 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(self.device.shell('TEST', decode=False), b'PASS') def test_shell_avoid_decode_error(self): self.assertTrue(self.device.connect()) # Provide the `bulk_read` return values self.transport.bulk_read_data = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=b'\x80abc'), AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) expected = '\\x80abc' if sys.version_info[0] > 2 else u'\ufffdabc' self.assertEqual(self.device.shell('TEST'), expected) def test_shell_data_length_exceeds_max(self): self.assertTrue(self.device.connect()) # Provide the `bulk_read` return values self.transport.bulk_read_data = 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'')) self.device.shell('TEST') self.assertTrue(True) def test_shell_multibytes_sequence_exceeds_max(self): self.assertTrue(self.device.connect()) # Provide the `bulk_read` return values self.transport.bulk_read_data = 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(u'0'*(constants.MAX_ADB_DATA-1) + u'\u3042', self.device.shell('TEST')) def test_shell_with_multibytes_sequence_over_two_messages(self): self.assertTrue(self.device.connect()) # Provide the `bulk_read` return values self.transport.bulk_read_data = 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(u'\u3042', self.device.shell('TEST')) def test_shell_multiple_clse(self): # https://github.com/JeffLIrion/adb_shell/issues/15#issuecomment-536795938 self.assertTrue(self.device.connect()) # Provide the `bulk_read` return values msg1 = AdbMessage(command=constants.OKAY, arg0=2, arg1=2, data=b'') msg2 = AdbMessage(command=constants.WRTE, arg0=2, arg1=2, data=b'PASS') msg3 = AdbMessage(command=constants.CLSE, arg0=2, arg1=2, data=b'') self.transport.bulk_read_data = 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()]) 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(self.device.shell('TEST'), 'PASS') def test_shell_multiple_streams(self): self.assertTrue(self.device.connect()) # Provide the `bulk_read` return values self.transport.bulk_read_data = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=2, data=b'\x00'), AdbMessage(command=constants.OKAY, arg0=1, arg1=2, data=b'\x00'), AdbMessage(command=constants.OKAY, arg0=1, arg1=2, data=b'\x00'), AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), AdbMessage(command=constants.WRTE, arg0=1, arg1=2, data=b'PASS2'), AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=b'PASS1'), AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b''), AdbMessage(command=constants.CLSE, arg0=1, arg1=2, data=b'')) self.assertEqual(self.device.shell('TEST1'), 'PASS1') self.assertEqual(self.device.shell('TEST2'), 'PASS2') def test_shell_multiple_streams2(self): self.assertTrue(self.device.connect()) def fake_read_packet_from_device(*args, **kwargs): # Mimic the scenario that this stream's packets get read by another stream after the first attempt to read the packet from the device self.device._io_manager._packet_store.put(arg0=1, arg1=1, cmd=constants.WRTE, data=b'\x00') self.device._io_manager._packet_store.put(arg0=1, arg1=1, cmd=constants.OKAY, data=b'\x00') self.device._io_manager._packet_store.put(arg0=2, arg1=2, cmd=constants.OKAY, data=b'\x00') self.device._io_manager._packet_store.put(arg0=1, arg1=1, cmd=constants.OKAY, data=b'\x00') self.device._io_manager._packet_store.put(arg0=2, arg1=2, cmd=constants.WRTE, data=b'PASS2') self.device._io_manager._packet_store.put(arg0=1, arg1=1, cmd=constants.WRTE, data=b"PASS1") self.device._io_manager._packet_store.put(arg0=1, arg1=1, cmd=constants.CLSE, data=b"") self.device._io_manager._packet_store.put(arg0=2, arg1=2, cmd=constants.CLSE, data=b"") return constants.OKAY, 2, 2, b"\x00" with patch.object(self.device._io_manager, "_read_packet_from_device", fake_read_packet_from_device): # The patch function will only be called once, all subsequent packets will be retrieved from the store self.assertEqual(self.device.shell('TEST1'), 'PASS1') self.assertEqual(self.device.shell('TEST2'), 'PASS2') def test_shell_local_id2(self): self.assertTrue(self.device.connect()) # Provide the `bulk_read` return values self.transport.bulk_read_data = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), AdbMessage(command=constants.OKAY, arg0=1, arg1=2, data=b'\x00'), AdbMessage(command=constants.WRTE, arg0=1, arg1=2, data=b'PASS2'), AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=b'PASS1'), AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b''), AdbMessage(command=constants.CLSE, arg0=1, arg1=2, data=b'')) self.assertEqual(self.device.shell('TEST1'), 'PASS1') self.assertEqual(self.device.shell('TEST2'), 'PASS2') def test_shell_remote_id2(self): self.assertTrue(self.device.connect()) # Provide the `bulk_read` return values self.transport.bulk_read_data = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), AdbMessage(command=constants.OKAY, arg0=2, arg1=2, data=b'\x00'), AdbMessage(command=constants.WRTE, arg0=2, arg1=2, data=b'PASS2'), AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=b'PASS1'), AdbMessage(command=constants.CLSE, arg0=2, arg1=2, data=b''), AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) self.assertEqual(self.device.shell('TEST1'), 'PASS1') self.assertEqual(self.device.shell('TEST2'), 'PASS2') # ======================================================================= # # # # `shell` error tests # # # # ======================================================================= # def test_shell_error_local_id_timeout(self): self.assertTrue(self.device.connect()) # Provide the `bulk_read` return values self.transport.bulk_read_data = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=1234, data=b'\x00'), AdbMessage(command=constants.OKAY, arg0=1, arg1=1234, data=b'\x00')) with self.assertRaises(exceptions.AdbTimeoutError): self.device.shell('TEST', read_timeout_s=1) # Close the connection so that the packet store gets cleared self.device.close() def test_shell_error_unknown_command(self): self.assertTrue(self.device.connect()) # Provide the `bulk_read` return values self.transport.bulk_read_data = join_messages(AdbMessageForTesting(command=constants.FAIL, arg0=1, arg1=1, data=b'')) with self.assertRaises(exceptions.InvalidCommandError): self.assertEqual(self.device.shell('TEST'), '') def test_shell_error_transport_timeout(self): self.assertTrue(self.device.connect()) # Provide the `bulk_read` return values self.transport.bulk_read_data = join_messages(AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=b'')) with self.assertRaises(exceptions.AdbTimeoutError): self.device.shell('TEST', read_timeout_s=-1) def test_shell_error_read_timeout_multiple_clse(self): self.assertTrue(self.device.connect()) # Provide the `bulk_read` return values self.transport.bulk_read_data = 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.AdbTimeoutError): self.device.shell('TEST', read_timeout_s=-1) def test_shell_error_timeout(self): self.assertTrue(self.device.connect()) # Provide the `bulk_read` return values self.transport.bulk_read_data = 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'')) def fake_read_until(*args, **kwargs): time.sleep(0.2) return b'WRTE', b'PA' with patch('adb_shell.adb_device.AdbDevice._read_until', fake_read_until): with self.assertRaises(exceptions.AdbTimeoutError): self.device.shell('TEST', timeout_s=0.5) # Clear the `_bulk_read` buffer so that `self.tearDown()` passes self.transport.bulk_read_data = b'' def test_shell_error_checksum(self): self.assertTrue(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.transport.bulk_read_data = b''.join([msg1.pack(), msg1.data, msg2.pack(), msg2.data[:-1] + b'0']) with self.assertRaises(exceptions.InvalidChecksumError): self.device.shell('TEST') 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') okay3 = AdbMessage(command=constants.OKAY, arg0=1, arg1=3, data=b'\x00') clse3 = AdbMessage(command=constants.CLSE, arg0=1, arg1=3, data=b'') okay5 = AdbMessage(command=constants.OKAY, arg0=1, arg1=5, data=b'\x00') clse5 = AdbMessage(command=constants.CLSE, arg0=1, arg1=5, data=b'') okay7 = AdbMessage(command=constants.OKAY, arg0=1, arg1=7, data=b'\x00') clse7 = AdbMessage(command=constants.CLSE, arg0=1, arg1=7, data=b'') self.transport.bulk_read_data = 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\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb0\xb4\xbe\xa6', # Line 305 b'CLSE\xa5\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbc\xb3\xac\xba', # Line 306 okay3.pack(), okay3.data, clse3.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\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb0\xb4\xbe\xa6', # Line 323 b'CLSE\xa7\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbc\xb3\xac\xba', # Line 324 okay5.pack(), okay5.data, clse5.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\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb0\xb4\xbe\xa6', # Line 341 b'CLSE\xa9\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbc\xb3\xac\xba', # Line 342 okay7.pack(), okay7.data, clse7.pack()]) self.assertTrue(self.device.connect([signer])) self.device.shell('Android TV update command') self.assertTrue(self.device.connect([signer])) self.device.shell('Android TV update command') self.device.shell('Android TV update command') self.assertTrue(self.device.connect([signer])) self.device.shell('Android TV update command') self.device.shell('Android TV update command') self.assertTrue(self.device.connect([signer])) self.device.shell('Android TV update command') self.device.shell('Android TV update command') # ======================================================================= # # # # `streaming_shell` tests # # # # ======================================================================= # def test_streaming_shell_decode(self): self.assertTrue(self.device.connect()) # Provide the `bulk_read` return values self.transport.bulk_read_data = 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'), ) generator = self.device.streaming_shell('TEST', decode=True) self.assertEqual('ABC', next(generator)) self.assertEqual('123', next(generator)) def test_streaming_shell_dont_decode(self): self.assertTrue(self.device.connect()) # Provide the `bulk_read` return values self.transport.bulk_read_data = 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'), ) generator = self.device.streaming_shell('TEST', decode=False) self.assertEqual(b'ABC', next(generator)) self.assertEqual(b'123', next(generator)) # ======================================================================= # # # # `reboot` test # # # # ======================================================================= # def test_reboot(self): self.assertTrue(self.device.connect()) with patch('adb_shell.adb_device.AdbDevice._open') as patch_open: self.device.reboot() assert patch_open.call_count == 1 # ======================================================================= # # # # `root` test # # # # ======================================================================= # def test_root(self): self.assertTrue(self.device.connect()) with patch('adb_shell.adb_device.AdbDevice._service') as patch_service: self.device.root() assert patch_service.call_count == 1 # ======================================================================= # # # # `exec_out` test # # # # ======================================================================= # def test_exec_out(self): self.assertTrue(self.device.connect()) # Provide the `bulk_read` return values self.transport.bulk_read_data = b''.join([b'OKAY\x14\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb0\xb4\xbe\xa6', b'WRTE\x14\x00\x00\x00\x01\x00\x00\x00\x05\x00\x00\x00J\x01\x00\x00\xa8\xad\xab\xba', b'TEST\n', b'', b'CLSE\x14\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbc\xb3\xac\xba']) self.assertEqual(self.device.exec_out("echo 'TEST'"), "TEST\n") # ======================================================================= # # # # `filesync` tests # # # # ======================================================================= # def test_list(self): self.assertTrue(self.device.connect()) self.transport.bulk_write_data = b'' # Provide the `bulk_read` return values self.transport.bulk_read_data = 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(expected_result, self.device.list('/dir')) self.assertEqual(expected_bulk_write, self.transport.bulk_write_data) def test_list_empty_path(self): with self.assertRaises(exceptions.DevicePathInvalidError): self.device.list("") with self.assertRaises(exceptions.DevicePathInvalidError): self.device.list(b"") with self.assertRaises(exceptions.DevicePathInvalidError): self.device.list(u"") with self.assertRaises(exceptions.DevicePathInvalidError): self.device.list(None) # Clear the `_bulk_read` buffer so that `self.tearDown()` passes self.transport.bulk_read_data = b'' def test_push_fail(self): self.assertTrue(self.device.connect()) self.transport.bulk_write_data = b'' mtime = 100 filedata = b'Ohayou sekai.\nGood morning world!' # Provide the `bulk_read` return values self.transport.bulk_read_data = 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.adb_device.open', patchers.mock_open(read_data=filedata)): self.device.push('TEST_FILE', '/data', mtime=mtime) def test_push_file(self): self.assertTrue(self.device.connect()) self.transport.bulk_write_data = b'' mtime = 100 filedata = b'Ohayou sekai.\nGood morning world!' # Provide the `bulk_read` return values self.transport.bulk_read_data = 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.adb_device.open', patchers.mock_open(read_data=filedata)): self.assertEqual(self.progress_callback_count, 0) with patch("adb_shell.adb_device.os.fstat", return_value=patchers.StSize(12345)): self.device.push('TEST_FILE', '/data', mtime=mtime, progress_callback=self.progress_callback) self.assertEqual(self.progress_callback_count, 1) self.assertEqual(expected_bulk_write, self.transport.bulk_write_data) def test_push_bytesio(self): self.assertTrue(self.device.connect()) self.transport.bulk_write_data = b'' mtime = 100 filedata = b'Ohayou sekai.\nGood morning world!' # Provide the `bulk_read` return values self.transport.bulk_read_data = 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'')) stream = BytesIO(filedata) self.device.push(stream, '/data', mtime=mtime) self.assertEqual(expected_bulk_write, self.transport.bulk_write_data) def test_push_file_exception(self): self.assertTrue(self.device.connect()) self.transport.bulk_write_data = b'' mtime = 100 filedata = b'Ohayou sekai.\nGood morning world!' # Provide the `bulk_read` return values self.transport.bulk_read_data = 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.adb_device.open', patchers.mock_open(read_data=filedata)): # Set self.progress_callback_count to None so that an exception occurs when self.progress_callback tries to increment it self.progress_callback_count = None with patch("adb_shell.adb_device.os.fstat", return_value=patchers.StSize(12345)): self.device.push('TEST_FILE', '/data', mtime=mtime, progress_callback=self.progress_callback) self.assertIsNone(self.progress_callback_count) self.assertEqual(expected_bulk_write, self.transport.bulk_write_data) def test_push_file_mtime0(self): self.assertTrue(self.device.connect()) self.transport.bulk_write_data = b'' mtime = 0 filedata = b'Ohayou sekai.\nGood morning world!' # Provide the `bulk_read` return values self.transport.bulk_read_data = 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('adb_shell.adb_device.open', patchers.mock_open(read_data=filedata)), patch('time.time', return_value=mtime): self.device.push('TEST_FILE', '/data', mtime=mtime) self.assertEqual(expected_bulk_write, self.transport.bulk_write_data) def test_push_big_file(self): self.assertTrue(self.device.connect()) self.transport.bulk_write_data = b'' mtime = 100 filedata = b'0' * int(3.5 * self.device.max_chunk_size) # Provide the `bulk_read` return values self.transport.bulk_read_data = 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 mcs0, mcs1, mcs2, mcs3 = 0, self.device.max_chunk_size, 2*self.device.max_chunk_size, 3*self.device.max_chunk_size 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[mcs0:mcs1]))), AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=join_messages( FileSyncMessage(command=constants.DATA, data=filedata[mcs1:mcs2]))), AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=join_messages( FileSyncMessage(command=constants.DATA, data=filedata[mcs2:mcs3]), FileSyncMessage(command=constants.DATA, data=filedata[mcs3:]), FileSyncMessage(command=constants.DONE, arg0=mtime))), 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', patchers.mock_open(read_data=filedata)): self.assertEqual(self.progress_callback_count, 0) with patch("adb_shell.adb_device.os.fstat", return_value=patchers.StSize(12345)): self.device.push('TEST_FILE', '/data', mtime=mtime, progress_callback=self.progress_callback) self.assertEqual(self.progress_callback_count, 4) self.assertEqual(expected_bulk_write, self.transport.bulk_write_data) def test_push_dir(self): self.assertTrue(self.device.connect()) mtime = 100 filedata = b'Ohayou sekai.\nGood morning world!' # Provide the `bulk_read` return values self.transport.bulk_read_data = 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=2, arg1=2, data=b'\x00'), AdbMessage(command=constants.OKAY, arg0=2, arg1=2, data=b'\x00'), AdbMessage(command=constants.WRTE, arg0=2, arg1=2, data=join_messages(FileSyncMessage(constants.OKAY))), AdbMessage(command=constants.CLSE, arg0=2, arg1=2, data=b''), AdbMessage(command=constants.OKAY, arg0=3, arg1=3, data=b'\x00'), AdbMessage(command=constants.OKAY, arg0=3, arg1=3, data=b'\x00'), AdbMessage(command=constants.WRTE, arg0=3, arg1=3, data=join_messages(FileSyncMessage(constants.OKAY))), AdbMessage(command=constants.CLSE, arg0=3, arg1=3, data=b'')) # Expected `bulk_write` values #TODO with patch('adb_shell.adb_device.open', patchers.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_push_empty_path(self): with self.assertRaises(exceptions.DevicePathInvalidError): self.device.push("NOTHING", "") with self.assertRaises(exceptions.DevicePathInvalidError): self.device.push("NOTHING", b"") with self.assertRaises(exceptions.DevicePathInvalidError): self.device.push("NOTHING", u"") with self.assertRaises(exceptions.DevicePathInvalidError): self.device.push("NOTHING", None) # Clear the `_bulk_read` buffer so that `self.tearDown()` passes self.transport.bulk_read_data = b'' def test_pull_file(self): self.assertTrue(self.device.connect()) self.transport.bulk_write_data = b'' filedata = b'Ohayou sekai.\nGood morning world!' # Provide the `bulk_read` return values self.transport.bulk_read_data = 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.adb_device.open', patchers.mock_open()) as m: self.assertEqual(self.progress_callback_count, 0) with patch("adb_shell.adb_device.AdbDevice.stat", self.fake_stat): self.device.pull('/data', 'TEST_FILE', progress_callback=self.progress_callback) self.assertEqual(self.progress_callback_count, 1) self.assertEqual(m.written, filedata) self.assertEqual(expected_bulk_write, self.transport.bulk_write_data) def test_pull_bytesio(self): self.assertTrue(self.device.connect()) self.transport.bulk_write_data = b'' filedata = b'Ohayou sekai.\nGood morning world!' # Provide the `bulk_read` return values self.transport.bulk_read_data = 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'')) stream = BytesIO() self.device.pull('/data', stream) self.assertEqual(expected_bulk_write, self.transport.bulk_write_data) self.assertEqual(stream.getvalue(), filedata) def test_pull_file_exception(self): self.assertTrue(self.device.connect()) self.transport.bulk_write_data = b'' filedata = b'Ohayou sekai.\nGood morning world!' # Provide the `bulk_read` return values self.transport.bulk_read_data = 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.adb_device.open', patchers.mock_open()) as m: # Set self.progress_callback_count to None so that an exception occurs when self.progress_callback tries to increment it self.progress_callback_count = None with patch("adb_shell.adb_device.AdbDevice.stat", self.fake_stat): self.device.pull('/data', 'TEST_FILE', progress_callback=self.progress_callback) self.assertIsNone(self.progress_callback_count) self.assertEqual(m.written, filedata) self.assertEqual(expected_bulk_write, self.transport.bulk_write_data) def test_pull_big_file(self): self.assertTrue(self.device.connect()) self.transport.bulk_write_data = b'' filedata = b'0' * int(1.5 * constants.MAX_ADB_DATA) # Provide the `bulk_read` return values self.transport.bulk_read_data = 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.adb_device.open', patchers.mock_open()) as m: self.assertEqual(self.progress_callback_count, 0) with patch("adb_shell.adb_device.AdbDevice.stat", self.fake_stat): self.device.pull('/data', 'TEST_FILE', progress_callback=self.progress_callback) self.assertEqual(self.progress_callback_count, 1) self.assertEqual(m.written, filedata) self.assertEqual(expected_bulk_write, self.transport.bulk_write_data) def test_pull_empty_path(self): with self.assertRaises(exceptions.DevicePathInvalidError): self.device.pull("", "NOWHERE") with self.assertRaises(exceptions.DevicePathInvalidError): self.device.pull(b"", "NOWHERE") with self.assertRaises(exceptions.DevicePathInvalidError): self.device.pull(u"", "NOWHERE") with self.assertRaises(exceptions.DevicePathInvalidError): self.device.pull(None, "NOWHERE") # Clear the `_bulk_read` buffer so that `self.tearDown()` passes self.transport.bulk_read_data = b'' def test_pull_non_existant_path(self): self.assertTrue(self.device.connect()) self.transport.bulk_write_data = b'' # Provide the `bulk_read` return values self.transport.bulk_read_data = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b''), AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b''), AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=b'FAIL&\x00\x00\x00'), AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=b'open failed: No such file or directory'), 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'/does/not/exist'))), AdbMessage(command=constants.OKAY, arg0=1, arg1=1), AdbMessage(command=constants.OKAY, arg0=1, arg1=1), AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) with self.assertRaises(exceptions.AdbCommandFailureException): self.device.pull("/does/not/exist", "NOWHERE") self.assertEqual(expected_bulk_write, self.transport.bulk_write_data) def test_pull_non_existant_path_2(self): self.assertTrue(self.device.connect()) self.transport.bulk_write_data = b'' # Provide the `bulk_read` return values self.transport.bulk_read_data = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b''), AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b''), AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=b'FAIL&\x00\x00\x00open failed: No such file or directory'), 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'/does/not/exist'))), AdbMessage(command=constants.OKAY, arg0=1, arg1=1), AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) with self.assertRaises(exceptions.AdbCommandFailureException): self.device.pull("/does/not/exist", "NOWHERE") self.assertEqual(expected_bulk_write, self.transport.bulk_write_data) def test_stat(self): self.assertTrue(self.device.connect()) self.transport.bulk_write_data = b'' # Provide the `bulk_read` return values self.transport.bulk_read_data = 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((1, 2, 3), self.device.stat('/data')) self.assertEqual(expected_bulk_write, self.transport.bulk_write_data) def test_stat_empty_path(self): with self.assertRaises(exceptions.DevicePathInvalidError): self.device.stat("") with self.assertRaises(exceptions.DevicePathInvalidError): self.device.stat(b"") with self.assertRaises(exceptions.DevicePathInvalidError): self.device.stat(u"") with self.assertRaises(exceptions.DevicePathInvalidError): self.device.stat(None) # Clear the `_bulk_read` buffer so that `self.tearDown()` passes self.transport.bulk_read_data = b'' def test_stat_issue155(self): self.assertTrue(self.device.connect()) # Provide the `bulk_read` return values self.transport.bulk_read_data = b"".join([b'CLSE\n\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbc\xb3\xac\xba', b'OKAY\x0b\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb0\xb4\xbe\xa6', b'OKAY\x0b\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb0\xb4\xbe\xa6', b'WRTE\x0b\x00\x00\x00\x01\x00\x00\x00\x10\x00\x00\x00\x96\x04\x00\x00\xa8\xad\xab\xba', b'STAT\xedA\x00\x00\x00\x10\x00\x00\xf0\x88[I', b'CLSE\x0b\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbc\xb3\xac\xba']) # This is where the expected values come from mode = 16877 size = 4096 mtime = 1230735600 self.assertEqual(FileSyncStatMessage(constants.STAT, mode, size, mtime).pack(), b'STAT\xedA\x00\x00\x00\x10\x00\x00\xf0\x88[I') self.assertEqual((mode, size, mtime), self.device.stat('/')) # ======================================================================= # # # # `filesync` hidden methods tests # # # # ======================================================================= # def test_filesync_read_adb_command_failure_exceptions(self): self.assertTrue(self.device.connect()) self.transport.bulk_write_data = b'' # Provide the `bulk_read` return values self.transport.bulk_read_data = 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): self.device.stat('/data') def test_filesync_read_invalid_response_error(self): self.assertTrue(self.device.connect()) self.transport.bulk_write_data = b'' # Provide the `bulk_read` return values self.transport.bulk_read_data = 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): self.device.stat('/data') ================================================ FILE: tests/test_adb_device_async.py ================================================ import asyncio import inspect import logging from io import BytesIO import sys import unittest from unittest.mock import mock_open, patch from adb_shell import adb_device_async, 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_TRANSPORT_ASYNC, FakeTcpTransportAsync, async_patch, async_mock_open 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.transport = FakeTcpTransportAsync('host', 5555) self.device = AdbDeviceAsync(self.transport) self.transport.bulk_read_data = b''.join(patchers.BULK_READ_LIST) self.progress_callback_count = 0 async def _progress_callback(device_path, current, total_bytes): print("device_path = {}, current = {}, total_bytes = {}".format(device_path, current, total_bytes)) self.progress_callback_count += 1 self.progress_callback = _progress_callback def tearDown(self): self.assertFalse(self.transport.bulk_read_data) self.assertEqual(len(self.device._io_manager._packet_store._dict), 0) @staticmethod async def fake_stat(*args, **kwargs): return 1, 2, 3 def test_no_sync_references(self): """Make sure there are no references to sync code.""" adb_device_async_source = inspect.getsource(adb_device_async) self.assertTrue("base_transport." not in adb_device_async_source) self.assertTrue("BaseTransport." not in adb_device_async_source) self.assertTrue("adb_device." not in adb_device_async_source) self.assertTrue("AdbDevice." not in adb_device_async_source) self.transport.bulk_read_data = b'' @awaiter async def test_adb_connection_error(self): with self.assertRaises(exceptions.AdbConnectionError): await self.device.exec_out('FAIL') with self.assertRaises(exceptions.AdbConnectionError): await self.device.root() 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.reboot() 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.transport.bulk_read_data = b'' @awaiter async def test_init_tcp(self): with PATCH_TCP_TRANSPORT_ASYNC: tcp_device = AdbDeviceTcpAsync('host') tcp_device._io_manager._transport.bulk_read_data = self.transport.bulk_read_data # 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.transport.bulk_read_data = b'' @awaiter async def test_init_banner(self): device_with_banner = AdbDeviceAsync(transport=FakeTcpTransportAsync('host', 5555), banner='banner') self.assertEqual(device_with_banner._banner, b'banner') device_with_banner2 = AdbDeviceAsync(transport=FakeTcpTransportAsync('host', 5555), banner=bytearray('banner2', 'utf-8')) self.assertEqual(device_with_banner2._banner, b'banner2') device_with_banner3 = AdbDeviceAsync(transport=FakeTcpTransportAsync('host', 5555), banner=u'banner3') self.assertEqual(device_with_banner3._banner, b'banner3') with patch('socket.gethostname', side_effect=Exception): device_banner_unknown = AdbDeviceAsync(self.transport) self.assertTrue(await device_banner_unknown.connect()) self.assertEqual(device_banner_unknown._banner, b'unknown') @awaiter async def test_init_invalid_transport(self): with self.assertRaises(exceptions.InvalidTransportError): device = AdbDeviceAsync(transport=123) # Clear the `_bulk_read` buffer so that `self.tearDown()` passes self.transport.bulk_read_data = b'' @awaiter async def test_available(self): self.assertFalse(self.device.available) # Clear the `_bulk_read` buffer so that `self.tearDown()` passes self.transport.bulk_read_data = 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.transport.bulk_read_data = 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.transport.bulk_read_data = 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.transport.bulk_read_data = 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.transport.bulk_read_data = 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.transport.bulk_read_data = 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.transport.bulk_read_data = 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) @awaiter async def test_connect_timeout(self): self.transport.bulk_read_data = AdbMessage(command=constants.CLSE, arg0=1, arg1=1).pack() with self.assertRaises(exceptions.AdbTimeoutError): # Use a negative timeout to ensure that only one packet gets read await self.device.connect([], read_timeout_s=-1) # ======================================================================= # # # # `shell` tests # # # # ======================================================================= # @awaiter async def test_shell_no_return(self): self.assertTrue(await self.device.connect()) # Provide the `bulk_read` return values self.transport.bulk_read_data = 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.transport.bulk_read_data = 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_local_id_wraparound(self): self.assertTrue(await self.device.connect()) # Provide the `bulk_read` return values self.transport.bulk_read_data = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=2**32 - 1, data=b'\x00'), AdbMessage(command=constants.WRTE, arg0=1, arg1=2**32 - 1, data=b'PASS1'), AdbMessage(command=constants.CLSE, arg0=1, arg1=2**32 - 1, data=b''), AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=b'PASS2'), AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) self.device._local_id = 2**32 - 2 self.assertEqual(await self.device.shell('TEST'), 'PASS1') self.assertEqual(await self.device.shell('TEST'), 'PASS2') @awaiter async def test_shell_return_pass_with_unexpected_packet(self): self.assertTrue(await self.device.connect()) # Provide the `bulk_read` return values self.transport.bulk_read_data = 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.AUTH, arg0=1, arg1=1, data=b'UNEXPECTED'), 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.transport.bulk_read_data = 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_avoid_decode_error(self): self.assertTrue(await self.device.connect()) # Provide the `bulk_read` return values self.transport.bulk_read_data = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=b'\x80abc'), AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) self.assertEqual(await self.device.shell('TEST'), '\\x80abc') @awaiter async def test_shell_data_length_exceeds_max(self): self.assertTrue(await self.device.connect()) # Provide the `bulk_read` return values self.transport.bulk_read_data = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=b'0'*(self.device.max_chunk_size+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.transport.bulk_read_data = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=b'0'*(self.device.max_chunk_size-1) + b'\xe3\x81\x82'), AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) self.assertEqual(await self.device.shell('TEST'), u'0'*(self.device.max_chunk_size-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.transport.bulk_read_data = 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=2, arg1=2, data=b'') msg2 = AdbMessage(command=constants.WRTE, arg0=2, arg1=2, data=b'PASS') msg3 = AdbMessage(command=constants.CLSE, arg0=2, arg1=2, data=b'') self.transport.bulk_read_data = 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') @awaiter async def test_shell_multiple_streams(self): self.assertTrue(await self.device.connect()) # Provide the `bulk_read` return values self.transport.bulk_read_data = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=2, data=b'\x00'), AdbMessage(command=constants.OKAY, arg0=1, arg1=2, data=b'\x00'), AdbMessage(command=constants.OKAY, arg0=1, arg1=2, data=b'\x00'), AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), AdbMessage(command=constants.WRTE, arg0=1, arg1=2, data=b'PASS2'), AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=b'PASS1'), AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b''), AdbMessage(command=constants.CLSE, arg0=1, arg1=2, data=b'')) self.assertEqual(await self.device.shell('TEST1'), 'PASS1') self.assertEqual(await self.device.shell('TEST2'), 'PASS2') @awaiter async def test_shell_multiple_streams2(self): self.assertTrue(await self.device.connect()) async def fake_read_packet_from_device(*args, **kwargs): # Mimic the scenario that this stream's packets get read by another stream after the first attempt to read the packet from the device self.device._io_manager._packet_store.put(arg0=1, arg1=1, cmd=constants.WRTE, data=b'\x00') self.device._io_manager._packet_store.put(arg0=1, arg1=1, cmd=constants.OKAY, data=b'\x00') self.device._io_manager._packet_store.put(arg0=2, arg1=2, cmd=constants.OKAY, data=b'\x00') self.device._io_manager._packet_store.put(arg0=1, arg1=1, cmd=constants.OKAY, data=b'\x00') self.device._io_manager._packet_store.put(arg0=2, arg1=2, cmd=constants.WRTE, data=b'PASS2') self.device._io_manager._packet_store.put(arg0=1, arg1=1, cmd=constants.WRTE, data=b"PASS1") self.device._io_manager._packet_store.put(arg0=1, arg1=1, cmd=constants.CLSE, data=b"") self.device._io_manager._packet_store.put(arg0=2, arg1=2, cmd=constants.CLSE, data=b"") return constants.OKAY, 2, 2, b"\x00" with patch.object(self.device._io_manager, "_read_packet_from_device", fake_read_packet_from_device): # The patch function will only be called once, all subsequent packets will be retrieved from the store self.assertEqual(await self.device.shell('TEST1'), 'PASS1') self.assertEqual(await self.device.shell('TEST2'), 'PASS2') @awaiter async def test_shell_local_id2(self): self.assertTrue(await self.device.connect()) # Provide the `bulk_read` return values self.transport.bulk_read_data = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), AdbMessage(command=constants.OKAY, arg0=1, arg1=2, data=b'\x00'), AdbMessage(command=constants.WRTE, arg0=1, arg1=2, data=b'PASS2'), AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=b'PASS1'), AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b''), AdbMessage(command=constants.CLSE, arg0=1, arg1=2, data=b'')) self.assertEqual(await self.device.shell('TEST1'), 'PASS1') self.assertEqual(await self.device.shell('TEST2'), 'PASS2') @awaiter async def test_shell_remote_id2(self): self.assertTrue(await self.device.connect()) # Provide the `bulk_read` return values self.transport.bulk_read_data = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b'\x00'), AdbMessage(command=constants.OKAY, arg0=2, arg1=2, data=b'\x00'), AdbMessage(command=constants.WRTE, arg0=2, arg1=2, data=b'PASS2'), AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=b'PASS1'), AdbMessage(command=constants.CLSE, arg0=2, arg1=2, data=b''), AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) self.assertEqual(await self.device.shell('TEST1'), 'PASS1') self.assertEqual(await self.device.shell('TEST2'), 'PASS2') # ======================================================================= # # # # `shell` error tests # # # # ======================================================================= # @awaiter async def test_shell_error_local_id_timeout(self): self.assertTrue(await self.device.connect()) # Provide the `bulk_read` return values self.transport.bulk_read_data = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=1234, data=b'\x00'), AdbMessage(command=constants.OKAY, arg0=1, arg1=1234, data=b'\x00')) with self.assertRaises(exceptions.AdbTimeoutError): await self.device.shell('TEST', read_timeout_s=1) # Close the connection so that the packet store gets cleared await self.device.close() @awaiter async def test_shell_error_unknown_command(self): self.assertTrue(await self.device.connect()) # Provide the `bulk_read` return values self.transport.bulk_read_data = 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_transport_timeout(self): self.assertTrue(await self.device.connect()) # Provide the `bulk_read` return values self.transport.bulk_read_data = join_messages(AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=b'')) with self.assertRaises(exceptions.AdbTimeoutError): await self.device.shell('TEST', read_timeout_s=-1) @awaiter async def test_shell_error_read_timeout_multiple_clse(self): self.assertTrue(await self.device.connect()) # Provide the `bulk_read` return values self.transport.bulk_read_data = 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.AdbTimeoutError): await self.device.shell('TEST', read_timeout_s=-1) @awaiter async def test_shell_error_timeout(self): self.assertTrue(await self.device.connect()) # Provide the `bulk_read` return values self.transport.bulk_read_data = 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'')) async def fake_read_until(*args, **kwargs): await asyncio.sleep(0.2) return b'WRTE', b'PA' with patch('adb_shell.adb_device_async.AdbDeviceAsync._read_until', fake_read_until): with self.assertRaises(exceptions.AdbTimeoutError): await self.device.shell('TEST', timeout_s=0.5) # Clear the `_bulk_read` buffer so that `self.tearDown()` passes self.transport.bulk_read_data = b'' @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.transport.bulk_read_data = 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_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') okay3 = AdbMessage(command=constants.OKAY, arg0=1, arg1=3, data=b'\x00') clse3 = AdbMessage(command=constants.CLSE, arg0=1, arg1=3, data=b'') okay5 = AdbMessage(command=constants.OKAY, arg0=1, arg1=5, data=b'\x00') clse5 = AdbMessage(command=constants.CLSE, arg0=1, arg1=5, data=b'') okay7 = AdbMessage(command=constants.OKAY, arg0=1, arg1=7, data=b'\x00') clse7 = AdbMessage(command=constants.CLSE, arg0=1, arg1=7, data=b'') self.transport.bulk_read_data = 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\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb0\xb4\xbe\xa6', # Line 305 b'CLSE\xa5\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbc\xb3\xac\xba', # Line 306 okay3.pack(), okay3.data, clse3.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\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb0\xb4\xbe\xa6', # Line 323 b'CLSE\xa7\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbc\xb3\xac\xba', # Line 324 okay5.pack(), okay5.data, clse5.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\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb0\xb4\xbe\xa6', # Line 341 b'CLSE\xa9\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbc\xb3\xac\xba', # Line 342 okay7.pack(), okay7.data, clse7.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.transport.bulk_read_data = 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.transport.bulk_read_data = 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') # ======================================================================= # # # # `reboot` test # # # # ======================================================================= # @awaiter async def test_reboot(self): self.assertTrue(await self.device.connect()) with async_patch('adb_shell.adb_device_async.AdbDeviceAsync._open') as patch_open: await self.device.reboot() patch_open.assert_called_once() # ======================================================================= # # # # `root` test # # # # ======================================================================= # @awaiter async def test_root(self): self.assertTrue(await self.device.connect()) with async_patch('adb_shell.adb_device_async.AdbDeviceAsync._service') as patch_service: await self.device.root() patch_service.assert_called_once() # ======================================================================= # # # # `exec_out` test # # # # ======================================================================= # @awaiter async def test_exec_out(self): self.assertTrue(await self.device.connect()) # Provide the `bulk_read` return values self.transport.bulk_read_data = b''.join([b'OKAY\x14\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb0\xb4\xbe\xa6', b'WRTE\x14\x00\x00\x00\x01\x00\x00\x00\x05\x00\x00\x00J\x01\x00\x00\xa8\xad\xab\xba', b'TEST\n', b'', b'CLSE\x14\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbc\xb3\xac\xba']) self.assertEqual(await self.device.exec_out("echo 'TEST'"), "TEST\n") # ======================================================================= # # # # `filesync` tests # # # # ======================================================================= # @awaiter async def test_list(self): self.assertTrue(await self.device.connect()) self.transport.bulk_write_data = b'' # Provide the `bulk_read` return values self.transport.bulk_read_data = 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.transport.bulk_write_data, expected_bulk_write) @awaiter async def test_list_empty_path(self): with self.assertRaises(exceptions.DevicePathInvalidError): await self.device.list("") with self.assertRaises(exceptions.DevicePathInvalidError): await self.device.list(b"") with self.assertRaises(exceptions.DevicePathInvalidError): await self.device.list(u"") with self.assertRaises(exceptions.DevicePathInvalidError): await self.device.list(None) # Clear the `_bulk_read` buffer so that `self.tearDown()` passes self.transport.bulk_read_data = b'' @patchers.ASYNC_SKIPPER @awaiter async def test_push_fail(self): self.assertTrue(await self.device.connect()) self.transport.bulk_write_data = b'' mtime = 100 filedata = b'Ohayou sekai.\nGood morning world!' # Provide the `bulk_read` return values self.transport.bulk_read_data = 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('aiofiles.open', async_mock_open(read_data=filedata)): await self.device.push('TEST_FILE', '/data', mtime=mtime) @patchers.ASYNC_SKIPPER @awaiter async def test_push_file(self): self.assertTrue(await self.device.connect()) self.transport.bulk_write_data = b'' mtime = 100 filedata = b'Ohayou sekai.\nGood morning world!' # Provide the `bulk_read` return values self.transport.bulk_read_data = 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('aiofiles.open', async_mock_open(read_data=filedata)): self.assertEqual(self.progress_callback_count, 0) with patch("adb_shell.adb_device_async.os.fstat", return_value=patchers.StSize(12345)): await self.device.push('TEST_FILE', '/data', mtime=mtime, progress_callback=self.progress_callback) self.assertEqual(self.progress_callback_count, 1) self.assertEqual(self.transport.bulk_write_data, expected_bulk_write) @patchers.ASYNC_SKIPPER @awaiter async def test_push_bytesio(self): self.assertTrue(await self.device.connect()) self.transport.bulk_write_data = b'' mtime = 100 filedata = b'Ohayou sekai.\nGood morning world!' # Provide the `bulk_read` return values self.transport.bulk_read_data = 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'')) stream = BytesIO(filedata) await self.device.push(stream, '/data', mtime=mtime) self.assertEqual(self.transport.bulk_write_data, expected_bulk_write) @patchers.ASYNC_SKIPPER @awaiter async def test_push_file_exception(self): self.assertTrue(await self.device.connect()) self.transport.bulk_write_data = b'' mtime = 100 filedata = b'Ohayou sekai.\nGood morning world!' # Provide the `bulk_read` return values self.transport.bulk_read_data = 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('aiofiles.open', async_mock_open(read_data=filedata)): # Set self.progress_callback_count to None so that an exception occurs when self.progress_callback tries to increment it self.progress_callback_count = None with patch("adb_shell.adb_device_async.os.fstat", return_value=patchers.StSize(12345)): await self.device.push('TEST_FILE', '/data', mtime=mtime, progress_callback=self.progress_callback) self.assertIsNone(self.progress_callback_count) self.assertEqual(self.transport.bulk_write_data, expected_bulk_write) @patchers.ASYNC_SKIPPER @awaiter async def test_push_file_mtime0(self): self.assertTrue(await self.device.connect()) self.transport.bulk_write_data = b'' mtime = 0 filedata = b'Ohayou sekai.\nGood morning world!' # Provide the `bulk_read` return values self.transport.bulk_read_data = 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('aiofiles.open', async_mock_open(read_data=filedata)), patch('time.time', return_value=mtime): await self.device.push('TEST_FILE', '/data', mtime=mtime) self.assertEqual(self.transport.bulk_write_data, expected_bulk_write) @patchers.ASYNC_SKIPPER @awaiter async def test_push_big_file(self): self.assertTrue(await self.device.connect()) self.transport.bulk_write_data = b'' mtime = 100 filedata = b'0' * int(3.5 * self.device.max_chunk_size) # Provide the `bulk_read` return values self.transport.bulk_read_data = 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 mcs0, mcs1, mcs2, mcs3 = 0, self.device.max_chunk_size, 2*self.device.max_chunk_size, 3*self.device.max_chunk_size 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[mcs0:mcs1]))), AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=join_messages( FileSyncMessage(command=constants.DATA, data=filedata[mcs1:mcs2]))), AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=join_messages( FileSyncMessage(command=constants.DATA, data=filedata[mcs2:mcs3]), FileSyncMessage(command=constants.DATA, data=filedata[mcs3:]), FileSyncMessage(command=constants.DONE, arg0=mtime))), AdbMessage(command=constants.OKAY, arg0=1, arg1=1), AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) with patch('aiofiles.open', async_mock_open(read_data=filedata)): self.assertEqual(self.progress_callback_count, 0) with patch("adb_shell.adb_device_async.os.fstat", return_value=patchers.StSize(12345)): await self.device.push('TEST_FILE', '/data', mtime=mtime, progress_callback=self.progress_callback) self.assertEqual(self.progress_callback_count, 4) self.assertEqual(self.transport.bulk_write_data, expected_bulk_write) @patchers.ASYNC_SKIPPER @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.transport.bulk_read_data = 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=2, arg1=2, data=b'\x00'), AdbMessage(command=constants.OKAY, arg0=2, arg1=2, data=b'\x00'), AdbMessage(command=constants.WRTE, arg0=2, arg1=2, data=join_messages(FileSyncMessage(constants.OKAY))), AdbMessage(command=constants.CLSE, arg0=2, arg1=2, data=b''), AdbMessage(command=constants.OKAY, arg0=3, arg1=3, data=b'\x00'), AdbMessage(command=constants.OKAY, arg0=3, arg1=3, data=b'\x00'), AdbMessage(command=constants.WRTE, arg0=3, arg1=3, data=join_messages(FileSyncMessage(constants.OKAY))), AdbMessage(command=constants.CLSE, arg0=3, arg1=3, data=b'')) # Expected `bulk_write` values #TODO with patch('aiofiles.open', async_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_push_empty_path(self): with self.assertRaises(exceptions.DevicePathInvalidError): await self.device.push("NOTHING", "") with self.assertRaises(exceptions.DevicePathInvalidError): await self.device.push("NOTHING", b"") with self.assertRaises(exceptions.DevicePathInvalidError): await self.device.push("NOTHING", u"") with self.assertRaises(exceptions.DevicePathInvalidError): await self.device.push("NOTHING", None) # Clear the `_bulk_read` buffer so that `self.tearDown()` passes self.transport.bulk_read_data = b'' @patchers.ASYNC_SKIPPER @awaiter async def test_pull_file(self): self.assertTrue(await self.device.connect()) self.transport.bulk_write_data = b'' filedata = b'Ohayou sekai.\nGood morning world!' # Provide the `bulk_read` return values self.transport.bulk_read_data = 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('aiofiles.open', async_mock_open()) as m: self.assertEqual(self.progress_callback_count, 0) with patch('adb_shell.adb_device_async.AdbDeviceAsync.stat', self.fake_stat): await self.device.pull('/data', 'TEST_FILE', progress_callback=self.progress_callback) self.assertEqual(self.progress_callback_count, 1) self.assertEqual(m.written, filedata) self.assertEqual(self.transport.bulk_write_data, expected_bulk_write) @patchers.ASYNC_SKIPPER @awaiter async def test_pull_bytesio(self): self.assertTrue(await self.device.connect()) self.transport.bulk_write_data = b'' filedata = b'Ohayou sekai.\nGood morning world!' # Provide the `bulk_read` return values self.transport.bulk_read_data = 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'')) stream = BytesIO() await self.device.pull('/data', stream) self.assertEqual(self.transport.bulk_write_data, expected_bulk_write) self.assertEqual(stream.getvalue(), filedata) @patchers.ASYNC_SKIPPER @awaiter async def test_pull_file_exception(self): self.assertTrue(await self.device.connect()) self.transport.bulk_write_data = b'' filedata = b'Ohayou sekai.\nGood morning world!' # Provide the `bulk_read` return values self.transport.bulk_read_data = 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('aiofiles.open', async_mock_open()) as m: # Set self.progress_callback_count to None so that an exception occurs when self.progress_callback tries to increment it self.progress_callback_count = None with patch('adb_shell.adb_device_async.AdbDeviceAsync.stat', self.fake_stat): await self.device.pull('/data', 'TEST_FILE', progress_callback=self.progress_callback) self.assertIsNone(self.progress_callback_count) self.assertEqual(m.written, filedata) self.assertEqual(self.transport.bulk_write_data, expected_bulk_write) @patchers.ASYNC_SKIPPER @awaiter async def test_pull_big_file(self): self.assertTrue(await self.device.connect()) self.transport.bulk_write_data = b'' filedata = b'0' * int(1.5 * constants.MAX_ADB_DATA) # Provide the `bulk_read` return values self.transport.bulk_read_data = 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('aiofiles.open', async_mock_open()) as m: self.assertEqual(self.progress_callback_count, 0) with patch('adb_shell.adb_device_async.AdbDeviceAsync.stat', self.fake_stat): await self.device.pull('/data', 'TEST_FILE', progress_callback=self.progress_callback) self.assertEqual(self.progress_callback_count, 1) self.assertEqual(m.written, filedata) self.assertEqual(self.transport.bulk_write_data, expected_bulk_write) @awaiter async def test_pull_empty_path(self): with self.assertRaises(exceptions.DevicePathInvalidError): await self.device.pull("", "NOWHERE") with self.assertRaises(exceptions.DevicePathInvalidError): await self.device.pull(b"", "NOWHERE") with self.assertRaises(exceptions.DevicePathInvalidError): await self.device.pull(u"", "NOWHERE") with self.assertRaises(exceptions.DevicePathInvalidError): await self.device.pull(None, "NOWHERE") # Clear the `_bulk_read` buffer so that `self.tearDown()` passes self.transport.bulk_read_data = b'' @awaiter async def test_pull_non_existant_path(self): self.assertTrue(await self.device.connect()) self.transport.bulk_write_data = b'' # Provide the `bulk_read` return values self.transport.bulk_read_data = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b''), AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b''), AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=b'FAIL&\x00\x00\x00'), AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=b'open failed: No such file or directory'), 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'/does/not/exist'))), AdbMessage(command=constants.OKAY, arg0=1, arg1=1), AdbMessage(command=constants.OKAY, arg0=1, arg1=1), AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) with self.assertRaises(exceptions.AdbCommandFailureException): await self.device.pull("/does/not/exist", "NOWHERE") self.assertEqual(expected_bulk_write, self.transport.bulk_write_data) @awaiter async def test_pull_non_existant_path_2(self): self.assertTrue(await self.device.connect()) self.transport.bulk_write_data = b'' # Provide the `bulk_read` return values self.transport.bulk_read_data = join_messages(AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b''), AdbMessage(command=constants.OKAY, arg0=1, arg1=1, data=b''), AdbMessage(command=constants.WRTE, arg0=1, arg1=1, data=b'FAIL&\x00\x00\x00open failed: No such file or directory'), 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'/does/not/exist'))), AdbMessage(command=constants.OKAY, arg0=1, arg1=1), AdbMessage(command=constants.CLSE, arg0=1, arg1=1, data=b'')) with self.assertRaises(exceptions.AdbCommandFailureException): await self.device.pull("/does/not/exist", "NOWHERE") self.assertEqual(expected_bulk_write, self.transport.bulk_write_data) @awaiter async def test_stat(self): self.assertTrue(await self.device.connect()) self.transport.bulk_write_data = b'' # Provide the `bulk_read` return values self.transport.bulk_read_data = 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.transport.bulk_write_data, expected_bulk_write) @awaiter async def test_stat_empty_path(self): with self.assertRaises(exceptions.DevicePathInvalidError): await self.device.stat("") with self.assertRaises(exceptions.DevicePathInvalidError): await self.device.stat(b"") with self.assertRaises(exceptions.DevicePathInvalidError): await self.device.stat(u"") with self.assertRaises(exceptions.DevicePathInvalidError): await self.device.stat(None) # Clear the `_bulk_read` buffer so that `self.tearDown()` passes self.transport.bulk_read_data = b'' @awaiter async def test_stat_issue155(self): self.assertTrue(await self.device.connect()) # Provide the `bulk_read` return values self.transport.bulk_read_data = b"".join([b'CLSE\n\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbc\xb3\xac\xba', b'OKAY\x0b\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb0\xb4\xbe\xa6', b'OKAY\x0b\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb0\xb4\xbe\xa6', b'WRTE\x0b\x00\x00\x00\x01\x00\x00\x00\x10\x00\x00\x00\x96\x04\x00\x00\xa8\xad\xab\xba', b'STAT\xedA\x00\x00\x00\x10\x00\x00\xf0\x88[I', b'CLSE\x0b\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbc\xb3\xac\xba']) # This is where the expected values come from mode = 16877 size = 4096 mtime = 1230735600 self.assertEqual(FileSyncStatMessage(constants.STAT, mode, size, mtime).pack(), b'STAT\xedA\x00\x00\x00\x10\x00\x00\xf0\x88[I') self.assertEqual((mode, size, mtime), await self.device.stat('/')) # ======================================================================= # # # # `filesync` hidden methods tests # # # # ======================================================================= # @awaiter async def test_filesync_read_adb_command_failure_exceptions(self): self.assertTrue(await self.device.connect()) self.transport.bulk_write_data = b'' # Provide the `bulk_read` return values self.transport.bulk_read_data = 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.transport.bulk_write_data = b'' # Provide the `bulk_read` return values self.transport.bulk_read_data = 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') ================================================ FILE: tests/test_adb_message.py ================================================ import os import unittest try: from unittest.mock import patch except ImportError: from mock import patch from adb_shell import constants from adb_shell.adb_device import AdbDevice from adb_shell.adb_message import AdbMessage, checksum, int_to_cmd, unpack class TestAdbMessage(unittest.TestCase): def test_checksum_bytearray(self): cs = checksum(bytearray('TEST', 'utf-8')) self.assertEqual(cs, 320) def test_checksum_bytes(self): cs = checksum(b'TEST') self.assertEqual(cs, 320) def test_checksum_unicode(self): cs = checksum(u'TEST') self.assertEqual(cs, 320) def test_unpack_error(self): with self.assertRaises(ValueError): unpack(b'TEST') def test_constants(self): for key, val in constants.ID_TO_WIRE.items(): self.assertEqual(key, int_to_cmd(val)) ================================================ FILE: tests/test_exceptions.py ================================================ import functools import inspect import pickle import re import unittest try: from unittest import mock except ImportError: import mock import adb_shell.exceptions try: getargspec = inspect.getfullargspec except AttributeError: getargspec = inspect.getargspec try: _assertRegex = unittest.TestCase.assertRegex except AttributeError: _assertRegex = unittest.TestCase.assertRegexpMatches class TestExceptionSerialization(unittest.TestCase): def __test_serialize_one_exc_cls(exc_cls): # Work out how many args we need to instantiate this object try: exc_required_arity = len(getargspec(exc_cls.__init__).args) except TypeError: # In Python 2.7 this could be a slot wrapper which means `__init__` # wasn't overridden by the exception subclass - use 0 arity. exc_required_arity = 0 # Don't try to provide `self` - we assume strings will be fine here fake_args = ("foo", ) * (exc_required_arity - 1) # Instantiate the exception object and then attempt a serializion cycle # using `pickle` - we mainly care about whether this blows up or not exc_obj = exc_cls(*fake_args) pickled_exc_data = pickle.dumps(exc_obj) depickled_exc_obj = pickle.loads(pickled_exc_data) for __obj in adb_shell.exceptions.__dict__.values(): if isinstance(__obj, type) and issubclass(__obj, BaseException): __test_method = functools.partial( __test_serialize_one_exc_cls, __obj ) __test_name = "test_serialize_{}".format(__obj.__name__) locals()[__test_name] = __test_method # We want to confirm what the stringification and representation of # `UsbReadFailedError` look like since it's a non-trivial subclass of # `Exception` def test_usbreadfailederror_as_str(self): exc_args = (mock.sentinel.error_msg, mock.sentinel.usb1_exc_obj) exc_obj = adb_shell.exceptions.UsbReadFailedError(*exc_args) expected_str = "{}: {}".format(*exc_args) _assertRegex(self, str(exc_obj), re.escape(expected_str)) def test_usbreadfailederror_as_repr(self): exc_args = (mock.sentinel.error_msg, mock.sentinel.usb1_exc_obj) exc_obj = adb_shell.exceptions.UsbReadFailedError(*exc_args) expected_repr = "{}{!r}".format( adb_shell.exceptions.UsbReadFailedError.__name__, exc_args ) _assertRegex(self, repr(exc_obj), re.escape(expected_repr)) ================================================ FILE: tests/test_hidden_helpers.py ================================================ import unittest from adb_shell import constants from adb_shell.hidden_helpers import _AdbPacketStore, _AdbTransactionInfo class TestAdbPacketStore(unittest.TestCase): def setUp(self): self.packet_store = _AdbPacketStore() def test_init(self): self.assertEqual(len(self.packet_store), 0) self.assertFalse((None, None) in self.packet_store) def test_contains(self): self.assertFalse((None, None) in self.packet_store) self.packet_store.put(arg0=0, arg1=1, cmd=b"cmd1", data=b"data1") self.assertTrue((0, 1) in self.packet_store) self.assertTrue((None, 1) in self.packet_store) self.assertFalse((None, 0) in self.packet_store) self.assertTrue((0, None) in self.packet_store) self.assertFalse((1, None) in self.packet_store) self.assertFalse((1, 1) in self.packet_store) self.assertTrue((None, None) in self.packet_store) def test_put(self): self.packet_store.put(arg0=0, arg1=1, cmd=b"cmd1", data=b"data1") self.assertTrue((0, 1) in self.packet_store) self.assertEqual(len(self.packet_store), 1) self.packet_store.put(arg0=0, arg1=1, cmd=b"cmd2", data=b"data2") self.assertTrue((0, 1) in self.packet_store) self.assertEqual(len(self.packet_store), 1) self.packet_store.put(arg0=1, arg1=0, cmd=b"cmd3", data=b"data3") self.assertTrue((1, 0) in self.packet_store) self.assertEqual(len(self.packet_store), 2) self.packet_store.put(arg0=1, arg1=1, cmd=b"cmd4", data=b"data4") self.assertTrue((1, 1) in self.packet_store) self.assertEqual(len(self.packet_store), 3) self.packet_store.put(arg0=5, arg1=5, cmd=constants.CLSE, data=b"data5") self.assertEqual(len(self.packet_store), 3) self.assertFalse((5, 5) in self.packet_store) self.packet_store.put(arg0=5, arg1=1, cmd=constants.CLSE, data=b"data5") self.assertEqual(len(self.packet_store), 3) self.assertFalse((5, 1) in self.packet_store) def test_get(self): self.packet_store.put(arg0=0, arg1=1, cmd=b"cmd1", data=b"data1") self.packet_store.put(arg0=0, arg1=1, cmd=b"cmd2", data=b"data2") self.packet_store.put(arg0=1, arg1=0, cmd=b"cmd3", data=b"data3") self.packet_store.put(arg0=1, arg1=1, cmd=b"cmd4", data=b"data4") self.packet_store.put(arg0=2, arg1=3, cmd=b"cmd5", data=b"data5") self.packet_store.put(arg0=4, arg1=5, cmd=b"cmd6", data=b"data6") self.assertTrue((0, 1) in self.packet_store) cmd1, arg0, arg1, data1 = self.packet_store.get(arg0=0, arg1=1) self.assertEqual(arg0, 0) self.assertEqual(arg1, 1) self.assertEqual(cmd1, b"cmd1") self.assertEqual(data1, b"data1") self.assertTrue((0, 1) in self.packet_store) self.assertEqual(len(self.packet_store), 5) self.assertTrue((0, 1) in self.packet_store) cmd2, arg0, arg1, data2 = self.packet_store.get(arg0=0, arg1=1) self.assertEqual(arg0, 0) self.assertEqual(arg1, 1) self.assertEqual(cmd2, b"cmd2") self.assertEqual(data2, b"data2") self.assertFalse((0, 1) in self.packet_store) self.assertEqual(len(self.packet_store), 4) self.assertTrue((1, 0) in self.packet_store) cmd3, arg0, arg1, data3 = self.packet_store.get(arg0=1, arg1=0) self.assertEqual(arg0, 1) self.assertEqual(arg1, 0) self.assertEqual(cmd3, b"cmd3") self.assertEqual(data3, b"data3") self.assertEqual(len(self.packet_store), 3) self.assertTrue((1, None) in self.packet_store) cmd4, arg0, arg1, data4 = self.packet_store.get(arg0=1, arg1=None) self.assertEqual(arg0, 1) self.assertEqual(arg1, 1) self.assertEqual(cmd4, b"cmd4") self.assertEqual(data4, b"data4") self.assertEqual(len(self.packet_store), 2) self.assertTrue((None, 3) in self.packet_store) cmd5, arg0, arg1, data5 = self.packet_store.get(arg0=None, arg1=3) self.assertEqual(arg0, 2) self.assertEqual(arg1, 3) self.assertEqual(cmd5, b"cmd5") self.assertEqual(data5, b"data5") self.assertEqual(len(self.packet_store), 1) self.assertTrue((None, None) in self.packet_store) cmd6, arg0, arg1, data6 = self.packet_store.get(arg0=None, arg1=None) self.assertEqual(arg0, 4) self.assertEqual(arg1, 5) self.assertEqual(cmd6, b"cmd6") self.assertEqual(data6, b"data6") self.assertEqual(len(self.packet_store), 0) self.assertEqual(len(self.packet_store._dict), 4) self.assertEqual(len(self.packet_store._dict[1]), 2) self.assertEqual(len(self.packet_store._dict[0]), 1) self.assertEqual(len(self.packet_store._dict[3]), 1) self.assertEqual(len(self.packet_store._dict[5]), 1) def test_get_clse(self): self.packet_store.put(arg0=0, arg1=1, cmd=b"cmd1", data=b"data1") self.packet_store.put(arg0=0, arg1=1, cmd=constants.CLSE, data=b"data2") self.packet_store.put(arg0=0, arg1=1, cmd=b"cmd3", data=b"data3") self.assertTrue((0, 1) in self.packet_store) cmd1, arg0, arg1, data1 = self.packet_store.get(arg0=0, arg1=1) self.assertEqual(arg0, 0) self.assertEqual(arg1, 1) self.assertEqual(cmd1, b"cmd1") self.assertEqual(data1, b"data1") self.assertTrue((0, 1) in self.packet_store) self.assertEqual(len(self.packet_store), 1) self.assertTrue((0, 1) in self.packet_store) cmd2, arg0, arg1, data2 = self.packet_store.get(arg0=0, arg1=1) self.assertEqual(arg0, 0) self.assertEqual(arg1, 1) self.assertEqual(cmd2, constants.CLSE) self.assertEqual(data2, b"data2") self.assertFalse((0, 1) in self.packet_store) self.assertEqual(len(self.packet_store), 0) self.assertEqual(len(self.packet_store._dict), 0) def test_clear(self): self.packet_store.put(arg0=0, arg1=1, cmd=b"cmd1", data=b"data1") self.packet_store.put(arg0=2, arg1=1, cmd=b"cmd2", data=b"data2") self.packet_store.clear(arg0=None, arg1=None) self.assertEqual(len(self.packet_store), 2) self.packet_store.clear(arg0=1, arg1=0) self.assertEqual(len(self.packet_store), 2) self.packet_store.clear(arg0=0, arg1=1) self.assertEqual(len(self.packet_store), 1) self.assertEqual(len(self.packet_store._dict), 1) self.packet_store.clear(arg0=2, arg1=1) self.assertEqual(len(self.packet_store), 0) self.assertEqual(len(self.packet_store._dict), 0) def test_clear_all(self): self.packet_store.put(arg0=0, arg1=1, cmd=b"cmd1", data=b"data1") self.packet_store.clear_all() self.assertFalse((0, 1) in self.packet_store) self.assertEqual(len(self.packet_store), 0) def test_find_allow_zeros(self): self.packet_store.put(arg0=0, arg1=1, cmd=b"cmd", data=b"data") self.assertEqual(self.packet_store.find_allow_zeros(arg0=2, arg1=1), (0, 1)) self.assertIsNone(self.packet_store.find_allow_zeros(arg0=2, arg1=2)) class TestAdbTransactionInfo(unittest.TestCase): def test_args_match(self): adb_info_1_None = _AdbTransactionInfo(1, None, 123, 456, 789) adb_info_1_2 = _AdbTransactionInfo(1, 2, 123, 456, 789) # (1, None) -> exact matches self.assertTrue(adb_info_1_None.args_match(6, 1)) self.assertTrue(adb_info_1_None.args_match(7, 1)) self.assertTrue(adb_info_1_None.args_match(6, 1, allow_zeros=True)) self.assertTrue(adb_info_1_None.args_match(7, 1, allow_zeros=True)) # (1, None) -> no match self.assertFalse(adb_info_1_None.args_match(0, 0)) self.assertFalse(adb_info_1_None.args_match(1, 0)) self.assertFalse(adb_info_1_None.args_match(2, 0)) self.assertFalse(adb_info_1_None.args_match(3, 0)) self.assertFalse(adb_info_1_None.args_match(4, 5, allow_zeros=True)) # (1, None) -> zero matches self.assertTrue(adb_info_1_None.args_match(0, 0, allow_zeros=True)) self.assertTrue(adb_info_1_None.args_match(1, 0, allow_zeros=True)) self.assertTrue(adb_info_1_None.args_match(2, 0, allow_zeros=True)) self.assertTrue(adb_info_1_None.args_match(3, 0, allow_zeros=True)) # (1, 2) -> exact matches self.assertTrue(adb_info_1_2.args_match(2, 1)) self.assertTrue(adb_info_1_2.args_match(2, 1, allow_zeros=True)) # (1, 2) -> no match self.assertFalse(adb_info_1_2.args_match(0, 0)) self.assertFalse(adb_info_1_2.args_match(2, 0)) self.assertFalse(adb_info_1_2.args_match(0, 1)) self.assertFalse(adb_info_1_2.args_match(1, 2)) self.assertFalse(adb_info_1_2.args_match(1, 2, allow_zeros=True)) self.assertFalse(adb_info_1_2.args_match(3, 0)) self.assertFalse(adb_info_1_2.args_match(0, 4)) self.assertFalse(adb_info_1_2.args_match(3, 4)) self.assertFalse(adb_info_1_2.args_match(3, 0, allow_zeros=True)) self.assertFalse(adb_info_1_2.args_match(0, 4, allow_zeros=True)) self.assertFalse(adb_info_1_2.args_match(3, 4, allow_zeros=True)) self.assertFalse(adb_info_1_2.args_match(2, 6)) self.assertFalse(adb_info_1_2.args_match(2, 6, allow_zeros=True)) self.assertFalse(adb_info_1_2.args_match(7, 1)) self.assertFalse(adb_info_1_2.args_match(7, 1, allow_zeros=True)) # (1, 2) -> zero matches self.assertTrue(adb_info_1_2.args_match(0, 0, allow_zeros=True)) self.assertTrue(adb_info_1_2.args_match(2, 0, allow_zeros=True)) self.assertTrue(adb_info_1_2.args_match(0, 1, allow_zeros=True)) ================================================ FILE: tests/test_keygen.py ================================================ import unittest try: from unittest.mock import patch except ImportError: from mock import patch from adb_shell.auth.keygen import get_user_info class TestKeygen(unittest.TestCase): def test_get_user_info(self): with patch('adb_shell.auth.keygen.os.getlogin', side_effect=OSError), patch('adb_shell.auth.keygen.socket.gethostname', return_value=''): user_host = get_user_info() self.assertEqual(user_host, ' unknown@unknown') with patch('adb_shell.auth.keygen.os.getlogin', return_value=''), patch('adb_shell.auth.keygen.socket.gethostname', return_value=''): user_host = get_user_info() self.assertEqual(user_host, ' unknown@unknown') ================================================ FILE: tests/test_sign_cryptography.py ================================================ import os import unittest try: from unittest.mock import patch except ImportError: from mock import patch from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from adb_shell.auth.keygen import keygen from adb_shell.auth.sign_cryptography import CryptographySigner from .keygen_stub import open_priv_pub class TestCryptographySigner(unittest.TestCase): def setUp(self): with patch('adb_shell.auth.sign_cryptography.open', open_priv_pub), patch('adb_shell.auth.keygen.open', open_priv_pub): keygen('tests/adbkey') self.signer = CryptographySigner('tests/adbkey') def test_sign(self): """Test that the ``Sign`` method does not raise an exception.""" # https://www.programcreek.com/python/example/107988/cryptography.hazmat.primitives.hashes.Hash hash_ctx = hashes.Hash(hashes.SHA1(), default_backend()) hash_ctx.update(b'notadb') data = hash_ctx.finalize() # For reference: # data = b'(\x8b\x9e\x88|JY\xb5\x18\x13b_\xe0\xc4\xfb\xa5\x83\xbdx\xfc' self.signer.Sign(data) self.assertTrue(True) def test_get_public_key(self): """Test that the ``GetPublicKey`` method works correctly.""" with patch('{}.open'.format(__name__), open_priv_pub): with open('tests/adbkey.pub', 'rb') as f: pub = f.read() self.assertEqual(pub, self.signer.GetPublicKey()) ================================================ FILE: tests/test_sign_pycryptodome.py ================================================ import os import unittest try: from unittest.mock import patch except ImportError: from mock import patch from adb_shell.auth.keygen import keygen from adb_shell.auth.sign_pycryptodome import PycryptodomeAuthSigner from .keygen_stub import open_priv_pub class TestPycryptodomeAuthSigner(unittest.TestCase): def setUp(self): with patch('adb_shell.auth.sign_pycryptodome.open', open_priv_pub), patch('adb_shell.auth.keygen.open', open_priv_pub): keygen('tests/adbkey') self.signer = PycryptodomeAuthSigner('tests/adbkey') def test_sign(self): """Test that the ``Sign`` method does not raise an exception.""" self.signer.Sign(b'notadb') self.assertTrue(True) def test_get_public_key(self): """Test that the ``GetPublicKey`` method works correctly.""" with patch('{}.open'.format(__name__), open_priv_pub): with open('tests/adbkey.pub', 'rb') as f: pub = f.read() self.assertEqual(pub, self.signer.GetPublicKey()) ================================================ FILE: tests/test_sign_pythonrsa.py ================================================ import os import unittest try: from unittest.mock import patch except ImportError: from mock import patch from adb_shell.auth.keygen import keygen from adb_shell.auth.sign_pythonrsa import PythonRSASigner from .keygen_stub import open_priv_pub class TestPythonRSASigner(unittest.TestCase): def setUp(self): with patch('adb_shell.auth.sign_pythonrsa.open', open_priv_pub), patch('adb_shell.auth.keygen.open', open_priv_pub): keygen('tests/adbkey') self.signer = PythonRSASigner.FromRSAKeyPath('tests/adbkey') def test_sign(self): """Test that the ``Sign`` method does not raise an exception.""" self.signer.Sign(b'notadb') self.assertTrue(True) def test_get_public_key(self): """Test that the ``GetPublicKey`` method works correctly.""" with patch('{}.open'.format(__name__), open_priv_pub): with open('tests/adbkey.pub') as f: pub = f.read() self.assertEqual(pub, self.signer.GetPublicKey()) class TestPythonRSASignerExceptions(unittest.TestCase): def test_value_error(self): with patch('adb_shell.auth.sign_pythonrsa.open', open_priv_pub), patch('adb_shell.auth.keygen.open', open_priv_pub): with patch('adb_shell.auth.sign_pythonrsa.decoder.decode', return_value=([None, [None]], None)): with self.assertRaises(ValueError): keygen('tests/adbkey') self.signer = PythonRSASigner.FromRSAKeyPath('tests/adbkey') def test_index_error(self): with patch('adb_shell.auth.sign_pythonrsa.open', open_priv_pub), patch('adb_shell.auth.keygen.open', open_priv_pub): with patch('adb_shell.auth.sign_pythonrsa.decoder.decode', side_effect=IndexError): with self.assertRaises(ValueError): keygen('tests/adbkey') self.signer = PythonRSASigner.FromRSAKeyPath('tests/adbkey') ================================================ FILE: tests/test_tcp_transport.py ================================================ import unittest try: from unittest.mock import patch except ImportError: from mock import patch from adb_shell.exceptions import TcpTimeoutException from adb_shell.transport.tcp_transport import TcpTransport from . import patchers class TestTcpTransport(unittest.TestCase): def setUp(self): """Create a ``TcpTransport`` and connect to a TCP service. """ self.transport = TcpTransport('host', 5555) with patchers.PATCH_CREATE_CONNECTION: self.transport.connect(transport_timeout_s=1) def tearDown(self): """Close the socket connection.""" self.transport.close() def test_connect_with_timeout(self): """TODO """ self.transport.close() with patchers.PATCH_CREATE_CONNECTION: self.transport.connect(transport_timeout_s=1) self.assertTrue(True) def test_bulk_read(self): """TODO """ # Provide the `recv` return values self.transport._connection._recv = b'TEST1TEST2' with patchers.PATCH_SELECT_SUCCESS: self.assertEqual(self.transport.bulk_read(5, transport_timeout_s=1), b'TEST1') self.assertEqual(self.transport.bulk_read(5, transport_timeout_s=1), b'TEST2') with patchers.PATCH_SELECT_FAIL: with self.assertRaises(TcpTimeoutException): self.transport.bulk_read(4, transport_timeout_s=1) def test_close_oserror(self): """Test that an `OSError` exception is handled when closing the socket. """ with patch('{}.patchers.FakeSocket.shutdown'.format(__name__), side_effect=OSError): self.transport.close() def test_bulk_write(self): """TODO """ with patchers.PATCH_SELECT_SUCCESS: self.transport.bulk_write(b'TEST', transport_timeout_s=1) with patchers.PATCH_SELECT_FAIL: with self.assertRaises(TcpTimeoutException): self.transport.bulk_write(b'FAIL', transport_timeout_s=1) ================================================ FILE: tests/test_tcp_transport_async.py ================================================ import asyncio import unittest from unittest.mock import patch from adb_shell.exceptions import TcpTimeoutException from adb_shell.transport.tcp_transport_async import TcpTransportAsync from .async_patchers import FakeStreamReader, FakeStreamWriter, async_patch from .async_wrapper import awaiter from . import patchers @patchers.ASYNC_SKIPPER class TestTcpTransportAsync(unittest.TestCase): def setUp(self): """Create a ``TcpTransportAsync`` and connect to a TCP service. """ self.transport = TcpTransportAsync('host', 5555) @awaiter async def test_close(self): await self.transport.close() @awaiter async def test_close2(self): await self.transport.close() @awaiter async def test_connect(self): with async_patch('asyncio.open_connection', return_value=(True, True)): await self.transport.connect(transport_timeout_s=1) @awaiter async def test_connect_close(self): with async_patch('asyncio.open_connection', return_value=(FakeStreamReader(), FakeStreamWriter())): await self.transport.connect(transport_timeout_s=1) self.assertIsNotNone(self.transport._writer) await self.transport.close() self.assertIsNone(self.transport._reader) self.assertIsNone(self.transport._writer) @awaiter async def test_connect_close_catch_oserror(self): with async_patch('asyncio.open_connection', return_value=(FakeStreamReader(), FakeStreamWriter())): await self.transport.connect(transport_timeout_s=1) self.assertIsNotNone(self.transport._writer) with patch('{}.FakeStreamWriter.close'.format(__name__), side_effect=OSError): await self.transport.close() self.assertIsNone(self.transport._reader) self.assertIsNone(self.transport._writer) @awaiter async def test_connect_with_timeout(self): with self.assertRaises(TcpTimeoutException): with async_patch('asyncio.open_connection', side_effect=asyncio.TimeoutError): await self.transport.connect(transport_timeout_s=1) @awaiter async def test_bulk_read(self): with async_patch('asyncio.open_connection', return_value=(FakeStreamReader(), FakeStreamWriter())): await self.transport.connect(transport_timeout_s=1) self.assertEqual(await self.transport.bulk_read(4, transport_timeout_s=1), b'TEST') with self.assertRaises(TcpTimeoutException): with patch('{}.FakeStreamReader.read'.format(__name__), side_effect=asyncio.TimeoutError): await self.transport.bulk_read(4, transport_timeout_s=1) @awaiter async def test_bulk_write(self): with async_patch('asyncio.open_connection', return_value=(FakeStreamReader(), FakeStreamWriter())): await self.transport.connect(transport_timeout_s=1) self.assertEqual(await self.transport.bulk_write(b'TEST', transport_timeout_s=1), 4) with self.assertRaises(TcpTimeoutException): with patch('{}.FakeStreamWriter.write'.format(__name__), side_effect=asyncio.TimeoutError): await self.transport.bulk_write(b'TEST', transport_timeout_s=1) ================================================ FILE: tests/test_usb_importerror.py ================================================ import unittest try: from unittest.mock import patch except ImportError: from mock import patch try: from adb_shell.transport.usb_transport import UsbTransport except (ImportError, OSError): UsbTransport = None class TestUsbImportError(unittest.TestCase): def test_import_error(self): """Test that the package still works when ``libusb1`` is not installed.""" from adb_shell import adb_device from adb_shell.exceptions import InvalidTransportError # TODO: I can't manage to trigger an `ImportError` in adb_device.py # self.assertIsNone(adb_device.UsbTransport) # In lieu of a real `ImportError`, I'll just set this to None with patch("adb_shell.adb_device.UsbTransport", None): with self.assertRaises(InvalidTransportError): adb_device.AdbDeviceUsb('serial') def test_import_successful(self): from adb_shell import adb_device if UsbTransport is not None: # Make sure `UsbTransport` was imported with patch("adb_shell.adb_device.UsbTransport", UsbTransport): with patch("adb_shell.adb_device.UsbTransport.find_adb", return_value=UsbTransport("TODO", "TODO")): adb_device.AdbDeviceUsb("serial") ================================================ FILE: tests/test_usb_transport.py ================================================ """Tests for the `UsbTransport` class.""" import unittest from adb_shell.exceptions import TcpTimeoutException try: from adb_shell.transport.usb_transport import UsbTransport except (ImportError, OSError): UsbTransport = None from . import patchers # pylint: disable=missing-class-docstring, missing-function-docstring @unittest.skipIf(UsbTransport is None, "UsbTransport could not be imported") class TestUsbTransport(unittest.TestCase): def setUp(self): """Create a ``UsbTransport`` and do something... """ self.transport = UsbTransport('TODO', 'TODO') if True: return with patchers.PATCH_CREATE_CONNECTION: self.transport.connect() def tearDown(self): """Close the USB connection.""" self.transport.close() def test_connect_with_timeout(self): """TODO """ if True: return self.transport.close() with patchers.PATCH_CREATE_CONNECTION: self.transport.connect(transport_timeout_s=1) self.assertTrue(True) def test_bulk_read(self): """TODO """ if True: return # Provide the `recv` return values self.transport._connection._recv = b'TEST1TEST2' with patchers.PATCH_SELECT_SUCCESS: self.assertEqual(self.transport.bulk_read(5), b'TEST1') self.assertEqual(self.transport.bulk_read(5), b'TEST2') with patchers.PATCH_SELECT_FAIL: with self.assertRaises(TcpTimeoutException): self.transport.bulk_read(4) def test_bulk_write(self): """TODO """ if True: return with patchers.PATCH_SELECT_SUCCESS: self.transport.bulk_write(b'TEST') with patchers.PATCH_SELECT_FAIL: with self.assertRaises(TcpTimeoutException): self.transport.bulk_write(b'FAIL') ================================================ FILE: venv_requirements.txt ================================================ # Standard requirements black coverage coveralls flake8 pylint pytest setuptools sphinx sphinx-rtd-theme twine # Specific requirements for this project aiofiles async_timeout cryptography libusb1 pyasn1 pycryptodome rsa