--- /dev/null
+# Pycharm
+/.idea
+
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+pip-wheel-metadata/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+.python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
--- /dev/null
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<https://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<https://www.gnu.org/licenses/why-not-lgpl.html>.
--- /dev/null
+# Waydroid
+
+Waydroid is a container-based approach to boot a full Android system on a
+regular GNU/Linux system like Ubuntu.
+
+## Overview
+
+Waydroid uses Linux namespaces (user, pid, uts, net, mount, ipc) to run a
+full Android system in a container and provide Android applications on
+any GNU/Linux-based platform.
+
+The Android inside the container has direct access to needed hardwares.
+
+The Android runtime environment ships with a minimal customized Android system
+image based on the [LineageOS](https://lineageos.org/).
+The used image is currently based on Android 10
+
+## Install and Run Android Applications
+
+You can install Android applications from the command line.
+
+```sh
+waydroid app install xyz.apk
+```
+
+The apk files you will sometimes find on the internet tend to only have arm
+support, and will therefore not work on x86\_64.
+
+You may want to install [F-Droid](https://f-droid.org/) to get applications
+graphically. Note that the Google Play Store will not work as is, because it
+relies on the proprietary Google Play Services, which are not installed.
+
+## Reporting bugs
+
+If you have found an issue with Waydroid, please [file a bug](https://github.com/Waydroid/waydroid/issues/new).
+
+## Get in Touch
+
+If you want to get in contact with the developers please feel free to join the
+*WayDroid* groups in [Matrix](https://matrix.to/#/#waydroid:connolly.tech) or [Telegram](https://t.me/WayDroid).
--- /dev/null
+# Waydroid LXC Config
+
+lxc.rootfs.path = /home/.waydroid/rootfs
+lxc.utsname = waydroid
+lxc.arch = LXCARCH
+lxc.autodev = 0
+# lxc.autodev.tmpfs.size = 25000000
+lxc.aa_profile = unconfined
+
+lxc.init_cmd = /init
+
+lxc.mount.auto = cgroup:ro sys:ro proc
+
+lxc.network.type = veth
+lxc.network.flags = up
+lxc.network.link = waydroid0
+lxc.network.name = eth0
+lxc.network.hwaddr = 00:16:3e:f9:d3:03
+lxc.network.mtu = 1500
+
+lxc.console.path = none
+
+lxc.include = /home/.waydroid/lxc/waydroid/config_nodes
+
+lxc.hook.post-stop = /dev/null
--- /dev/null
+# Waydroid LXC Config
+
+lxc.rootfs.path = /home/.waydroid/rootfs
+lxc.uts.name = waydroid
+lxc.arch = LXCARCH
+lxc.autodev = 0
+# lxc.autodev.tmpfs.size = 25000000
+lxc.apparmor.profile = unconfined
+
+lxc.init.cmd = /init
+
+lxc.mount.auto = cgroup:ro sys:ro proc
+
+lxc.net.0.type = veth
+lxc.net.0.flags = up
+lxc.net.0.link = waydroid0
+lxc.net.0.name = eth0
+lxc.net.0.hwaddr = 00:16:3e:f9:d3:03
+lxc.net.0.mtu = 1500
+
+lxc.console.path = none
+
+lxc.include = /home/.waydroid/lxc/waydroid/config_nodes
+
+lxc.hook.post-stop = /dev/null
--- /dev/null
+#!/bin/sh -
+
+varrun="/run/waydroid-lxc"
+varlib="/var/lib"
+
+USE_LXC_BRIDGE="true"
+LXC_BRIDGE="waydroid0"
+LXC_BRIDGE_MAC="00:16:3e:00:00:01"
+LXC_ADDR="192.168.250.1"
+LXC_NETMASK="255.255.255.0"
+LXC_NETWORK="192.168.250.0/24"
+LXC_DHCP_RANGE="192.168.250.2,192.168.250.254"
+LXC_DHCP_MAX="253"
+LXC_DHCP_CONFILE=""
+LXC_DHCP_PING="true"
+LXC_DOMAIN=""
+LXC_USE_NFT="true"
+
+LXC_IPV6_ADDR=""
+LXC_IPV6_MASK=""
+LXC_IPV6_NETWORK=""
+LXC_IPV6_NAT="false"
+
+IPTABLES_BIN="$(which iptables-legacy)"
+if [ ! -n "$IPTABLES_BIN" ]; then
+ IPTABLES_BIN="$(which iptables)"
+fi
+IP6TABLES_BIN="$(which ip6tables-legacy)"
+if [ ! -n "$IP6TABLES_BIN" ]; then
+ IP6TABLES_BIN="$(which ip6tables)"
+fi
+
+use_nft() {
+ [ -n "$NFT" ] && nft list ruleset > /dev/null 2>&1 && [ "$LXC_USE_NFT" = "true" ]
+}
+
+NFT="$(which nft)"
+if ! use_nft; then
+ use_iptables_lock="-w"
+ $IPTABLES_BIN -w -L -n > /dev/null 2>&1 || use_iptables_lock=""
+fi
+
+_netmask2cidr ()
+{
+ # Assumes there's no "255." after a non-255 byte in the mask
+ local x=${1##*255.}
+ set -- 0^^^128^192^224^240^248^252^254^ $(( (${#1} - ${#x})*2 )) ${x%%.*}
+ x=${1%%$3*}
+ echo $(( $2 + (${#x}/4) ))
+}
+
+_ifdown() {
+ ip addr flush dev ${LXC_BRIDGE}
+ ip link set dev ${LXC_BRIDGE} down
+}
+
+_ifup() {
+ MASK=`_netmask2cidr ${LXC_NETMASK}`
+ CIDR_ADDR="${LXC_ADDR}/${MASK}"
+ ip addr add ${CIDR_ADDR} broadcast + dev ${LXC_BRIDGE}
+ ip link set dev ${LXC_BRIDGE} address $LXC_BRIDGE_MAC
+ ip link set dev ${LXC_BRIDGE} up
+}
+
+start_ipv6() {
+ LXC_IPV6_ARG=""
+ if [ -n "$LXC_IPV6_ADDR" ] && [ -n "$LXC_IPV6_MASK" ] && [ -n "$LXC_IPV6_NETWORK" ]; then
+ echo 1 > /proc/sys/net/ipv6/conf/all/forwarding
+ echo 0 > /proc/sys/net/ipv6/conf/${LXC_BRIDGE}/autoconf
+ ip -6 addr add dev ${LXC_BRIDGE} ${LXC_IPV6_ADDR}/${LXC_IPV6_MASK}
+ LXC_IPV6_ARG="--dhcp-range=${LXC_IPV6_ADDR},ra-only --listen-address ${LXC_IPV6_ADDR}"
+ fi
+}
+
+start_iptables() {
+ start_ipv6
+ if [ -n "$LXC_IPV6_ARG" ] && [ "$LXC_IPV6_NAT" = "true" ]; then
+ $IP6TABLES_BIN $use_iptables_lock -t nat -A POSTROUTING -s ${LXC_IPV6_NETWORK} ! -d ${LXC_IPV6_NETWORK} -j MASQUERADE
+ fi
+ $IPTABLES_BIN $use_iptables_lock -I INPUT -i ${LXC_BRIDGE} -p udp --dport 67 -j ACCEPT
+ $IPTABLES_BIN $use_iptables_lock -I INPUT -i ${LXC_BRIDGE} -p tcp --dport 67 -j ACCEPT
+ $IPTABLES_BIN $use_iptables_lock -I INPUT -i ${LXC_BRIDGE} -p udp --dport 53 -j ACCEPT
+ $IPTABLES_BIN $use_iptables_lock -I INPUT -i ${LXC_BRIDGE} -p tcp --dport 53 -j ACCEPT
+ $IPTABLES_BIN $use_iptables_lock -I FORWARD -i ${LXC_BRIDGE} -j ACCEPT
+ $IPTABLES_BIN $use_iptables_lock -I FORWARD -o ${LXC_BRIDGE} -j ACCEPT
+ $IPTABLES_BIN $use_iptables_lock -t nat -A POSTROUTING -s ${LXC_NETWORK} ! -d ${LXC_NETWORK} -j MASQUERADE
+ $IPTABLES_BIN $use_iptables_lock -t mangle -A POSTROUTING -o ${LXC_BRIDGE} -p udp -m udp --dport 68 -j CHECKSUM --checksum-fill
+}
+
+start_nftables() {
+ start_ipv6
+ NFT_RULESET=""
+ if [ -n "$LXC_IPV6_ARG" ] && [ "$LXC_IPV6_NAT" = "true" ]; then
+ NFT_RULESET="${NFT_RULESET}
+add table ip6 lxc;
+flush table ip6 lxc;
+add chain ip6 lxc postrouting { type nat hook postrouting priority 100; };
+add rule ip6 lxc postrouting ip saddr ${LXC_IPV6_NETWORK} ip daddr != ${LXC_IPV6_NETWORK} counter masquerade;
+"
+ fi
+ NFT_RULESET="${NFT_RULESET};
+add table inet lxc;
+flush table inet lxc;
+add chain inet lxc input { type filter hook input priority 0; };
+add rule inet lxc input iifname ${LXC_BRIDGE} udp dport { 53, 67 } accept;
+add rule inet lxc input iifname ${LXC_BRIDGE} tcp dport { 53, 67 } accept;
+add chain inet lxc forward { type filter hook forward priority 0; };
+add rule inet lxc forward iifname ${LXC_BRIDGE} accept;
+add rule inet lxc forward oifname ${LXC_BRIDGE} accept;
+add table ip lxc;
+flush table ip lxc;
+add chain ip lxc postrouting { type nat hook postrouting priority 100; };
+add rule ip lxc postrouting ip saddr ${LXC_NETWORK} ip daddr != ${LXC_NETWORK} counter masquerade"
+ nft "${NFT_RULESET}"
+}
+
+start() {
+ [ "x$USE_LXC_BRIDGE" = "xtrue" ] || { exit 0; }
+
+ [ ! -f "${varrun}/network_up" ] || { echo "waydroid-net is already running"; exit 1; }
+
+ if [ -d /sys/class/net/${LXC_BRIDGE} ]; then
+ stop force || true
+ fi
+
+ FAILED=1
+
+ cleanup() {
+ set +e
+ if [ "$FAILED" = "1" ]; then
+ echo "Failed to setup waydroid-net." >&2
+ stop force
+ exit 1
+ fi
+ }
+
+ trap cleanup EXIT HUP INT TERM
+ set -e
+
+ # set up the lxc network
+ [ ! -d /sys/class/net/${LXC_BRIDGE} ] && ip link add dev ${LXC_BRIDGE} type bridge
+ echo 1 > /proc/sys/net/ipv4/ip_forward
+ echo 0 > /proc/sys/net/ipv6/conf/${LXC_BRIDGE}/accept_dad || true
+
+ # if we are run from systemd on a system with selinux enabled,
+ # the mkdir will create /run/lxc as init_var_run_t which dnsmasq
+ # can't write its pid into, so we restorecon it (to var_run_t)
+ if [ ! -d "${varrun}" ]; then
+ mkdir -p "${varrun}"
+ if which restorecon >/dev/null 2>&1; then
+ restorecon "${varrun}"
+ fi
+ fi
+
+ _ifup
+
+ if use_nft; then
+ start_nftables
+ else
+ start_iptables
+ fi
+
+ LXC_DOMAIN_ARG=""
+ if [ -n "$LXC_DOMAIN" ]; then
+ LXC_DOMAIN_ARG="-s $LXC_DOMAIN -S /$LXC_DOMAIN/"
+ fi
+
+ # lxc's dnsmasq should be hermetic and not read `/etc/dnsmasq.conf` (which
+ # it does by default if `--conf-file` is not present
+ LXC_DHCP_CONFILE_ARG="--conf-file=${LXC_DHCP_CONFILE:-/dev/null}"
+
+ # https://lists.linuxcontainers.org/pipermail/lxc-devel/2014-October/010561.html
+ for DNSMASQ_USER in lxc-dnsmasq dnsmasq nobody
+ do
+ if getent passwd ${DNSMASQ_USER} >/dev/null; then
+ break
+ fi
+ done
+
+ LXC_DHCP_PING_ARG=""
+ if [ "x$LXC_DHCP_PING" = "xfalse" ]; then
+ LXC_DHCP_PING_ARG="--no-ping"
+ fi
+
+ dnsmasq $LXC_DHCP_CONFILE_ARG $LXC_DOMAIN_ARG $LXC_DHCP_PING_ARG -u ${DNSMASQ_USER} \
+ --strict-order --bind-interfaces --pid-file="${varrun}"/dnsmasq.pid \
+ --listen-address ${LXC_ADDR} --dhcp-range ${LXC_DHCP_RANGE} \
+ --dhcp-lease-max=${LXC_DHCP_MAX} --dhcp-no-override \
+ --except-interface=lo --interface=${LXC_BRIDGE} \
+ --dhcp-leasefile="${varlib}"/misc/dnsmasq.${LXC_BRIDGE}.leases \
+ --dhcp-authoritative $LXC_IPV6_ARG || cleanup
+
+ touch "${varrun}"/network_up
+ FAILED=0
+}
+
+stop_iptables() {
+ $IPTABLES_BIN $use_iptables_lock -D INPUT -i ${LXC_BRIDGE} -p udp --dport 67 -j ACCEPT
+ $IPTABLES_BIN $use_iptables_lock -D INPUT -i ${LXC_BRIDGE} -p tcp --dport 67 -j ACCEPT
+ $IPTABLES_BIN $use_iptables_lock -D INPUT -i ${LXC_BRIDGE} -p udp --dport 53 -j ACCEPT
+ $IPTABLES_BIN $use_iptables_lock -D INPUT -i ${LXC_BRIDGE} -p tcp --dport 53 -j ACCEPT
+ $IPTABLES_BIN $use_iptables_lock -D FORWARD -i ${LXC_BRIDGE} -j ACCEPT
+ $IPTABLES_BIN $use_iptables_lock -D FORWARD -o ${LXC_BRIDGE} -j ACCEPT
+ $IPTABLES_BIN $use_iptables_lock -t nat -D POSTROUTING -s ${LXC_NETWORK} ! -d ${LXC_NETWORK} -j MASQUERADE
+ $IPTABLES_BIN $use_iptables_lock -t mangle -D POSTROUTING -o ${LXC_BRIDGE} -p udp -m udp --dport 68 -j CHECKSUM --checksum-fill
+ if [ "$LXC_IPV6_NAT" = "true" ]; then
+ $IP6TABLES_BIN $use_iptables_lock -t nat -D POSTROUTING -s ${LXC_IPV6_NETWORK} ! -d ${LXC_IPV6_NETWORK} -j MASQUERADE
+ fi
+}
+
+stop_nftables() {
+ # Adding table before removing them is just to avoid
+ # delete error for non-existent table
+ NFT_RULESET="add table inet lxc;
+delete table inet lxc;
+add table ip lxc;
+delete table ip lxc;
+"
+ if [ "$LXC_IPV6_NAT" = "true" ]; then
+ NFT_RULESET="${NFT_RULESET};
+add table ip6 lxc;
+delete table ip6 lxc;"
+ fi
+ nft "${NFT_RULESET}"
+}
+
+stop() {
+ [ "x$USE_LXC_BRIDGE" = "xtrue" ] || { exit 0; }
+
+ [ -f "${varrun}/network_up" ] || [ "$1" = "force" ] || { echo "waydroid-net isn't running"; exit 1; }
+
+ if [ -d /sys/class/net/${LXC_BRIDGE} ]; then
+ _ifdown
+ if use_nft; then
+ stop_nftables
+ else
+ stop_iptables
+ fi
+
+ pid=`cat "${varrun}"/dnsmasq.pid 2>/dev/null` && kill -9 $pid
+ rm -f "${varrun}"/dnsmasq.pid
+ # if $LXC_BRIDGE has attached interfaces, don't destroy the bridge
+ ls /sys/class/net/${LXC_BRIDGE}/brif/* > /dev/null 2>&1 || ip link delete ${LXC_BRIDGE}
+ fi
+
+ rm -f "${varrun}"/network_up
+}
+
+# See how we were called.
+case "$1" in
+ start)
+ start
+ ;;
+
+ stop)
+ stop
+ ;;
+
+ restart|reload|force-reload)
+ $0 stop
+ $0 start
+ ;;
+
+ *)
+ echo "Usage: $0 {start|stop|restart|reload|force-reload}"
+ exit 2
+esac
+
+exit $?
--- /dev/null
+# Copyright 2021 Oliver Smith
+# SPDX-License-Identifier: GPL-3.0-or-later
+# PYTHON_ARGCOMPLETE_OK
+import sys
+import logging
+import os
+import traceback
+
+from . import actions
+from . import config
+from . import helpers
+from .helpers import logging as tools_logging
+
+
+def main():
+ def actionNeedRoot(action):
+ if os.geteuid() != 0:
+ raise RuntimeError(
+ "Action \"{}\" needs root access".format(action))
+
+ # Wrap everything to display nice error messages
+ args = None
+ try:
+ os.umask(0o000)
+ # Parse arguments, set up logging
+ args = helpers.arguments()
+ args.cache = {}
+ args.work = config.defaults["work"]
+ args.config = args.work + "/waydroid.cfg"
+ args.log = args.work + "/tools.log"
+ args.sudo_timer = True
+ args.timeout = 1800
+
+ if not os.path.isfile(args.config):
+ if args.action and args.action != "init":
+ print('ERROR: WayDroid is not initialized, run "waydroid init"')
+ return 0
+ elif os.geteuid() == 0 and args.action == "init":
+ os.mkdir(args.work)
+ else:
+ args.log = "/tmp/tools.log"
+
+ tools_logging.init(args)
+
+ # Initialize or require config
+ if args.action == "init":
+ actionNeedRoot(args.action)
+ actions.init(args)
+ elif args.action == "upgrade":
+ actionNeedRoot(args.action)
+ actions.upgrade(args)
+ elif args.action == "session":
+ if args.subaction == "start":
+ actions.session_manager.start(args)
+ elif args.subaction == "stop":
+ actions.session_manager.stop(args)
+ else:
+ logging.info(
+ "Run waydroid {} -h for usage information.".format(args.action))
+ elif args.action == "container":
+ actionNeedRoot(args.action)
+ if args.subaction == "start":
+ actions.container_manager.start(args)
+ elif args.subaction == "stop":
+ actions.container_manager.stop(args)
+ elif args.subaction == "freeze":
+ actions.container_manager.freeze(args)
+ elif args.subaction == "unfreeze":
+ actions.container_manager.unfreeze(args)
+ else:
+ logging.info(
+ "Run waydroid {} -h for usage information.".format(args.action))
+ elif args.action == "app":
+ if args.subaction == "install":
+ actions.app_manager.install(args)
+ elif args.subaction == "remove":
+ actions.app_manager.remove(args)
+ elif args.subaction == "launch":
+ actions.app_manager.launch(args)
+ elif args.subaction == "list":
+ actions.app_manager.list(args)
+ else:
+ logging.info(
+ "Run waydroid {} -h for usage information.".format(args.action))
+ elif args.action == "prop":
+ if args.subaction == "get":
+ ret = helpers.props.get(args, args.key)
+ if ret:
+ print(ret)
+ elif args.subaction == "set":
+ helpers.props.set(args, args.key, args.value)
+ else:
+ logging.info(
+ "Run waydroid {} -h for usage information.".format(args.action))
+ elif args.action == "shell":
+ actionNeedRoot(args.action)
+ helpers.lxc.shell(args)
+ elif args.action == "logcat":
+ actionNeedRoot(args.action)
+ helpers.lxc.logcat(args)
+ elif args.action == "show-full-ui":
+ actions.app_manager.showFullUI(args)
+ elif args.action == "status":
+ actions.status.print_status(args)
+ elif args.action == "log":
+ if args.clear_log:
+ helpers.run.user(args, ["truncate", "-s", "0", args.log])
+ helpers.run.user(
+ args, ["tail", "-n", args.lines, "-F", args.log], output="tui")
+ else:
+ logging.info("Run waydroid -h for usage information.")
+
+ #logging.info("Done")
+
+ except Exception as e:
+ # Dump log to stdout when args (and therefore logging) init failed
+ if not args:
+ logging.getLogger().setLevel(logging.DEBUG)
+
+ logging.info("ERROR: " + str(e))
+ logging.info("See also: <https://github.com/waydroid>")
+ logging.debug(traceback.format_exc())
+
+ # Hints about the log file (print to stdout only)
+ log_hint = "Run 'waydroid log' for details."
+ if not args or not os.path.exists(args.log):
+ log_hint += (" Alternatively you can use '--details-to-stdout' to"
+ " get more output, e.g. 'waydroid"
+ " --details-to-stdout init'.")
+ print(log_hint)
+ return 1
+
+
+if __name__ == "__main__":
+ sys.exit(main())
--- /dev/null
+# Copyright 2021 Erfan Abdi
+# SPDX-License-Identifier: GPL-3.0-or-later
+from tools.actions.initializer import init
+from tools.actions.upgrader import upgrade
+from tools.actions.session_manager import start, stop
+from tools.actions.container_manager import start, stop, freeze, unfreeze
+from tools.actions.app_manager import install, remove, launch, list
+from tools.actions.status import print_status
--- /dev/null
+# Copyright 2021 Erfan Abdi
+# SPDX-License-Identifier: GPL-3.0-or-later
+import logging
+import os
+import shutil
+import time
+import tools.config
+import tools.helpers.props
+from tools.interfaces import IPlatform
+from tools.interfaces import IStatusBarService
+
+def install(args):
+ if os.path.exists(tools.config.session_defaults["config_path"]):
+ session_cfg = tools.config.load_session()
+ if session_cfg["session"]["state"] == "RUNNING":
+ tmp_dir = session_cfg["session"]["waydroid_data"] + "/waydroid_tmp"
+ if not os.path.exists(tmp_dir):
+ os.makedirs(tmp_dir)
+
+ shutil.copyfile(args.PACKAGE, tmp_dir + "/base.apk")
+ platformService = IPlatform.get_service(args)
+ if platformService:
+ platformService.installApp("/data/waydroid_tmp/base.apk")
+ shutil.rmtree(tmp_dir)
+ else:
+ logging.error("WayDroid container is {}".format(
+ session_cfg["session"]["state"]))
+ else:
+ logging.error("WayDroid session is stopped")
+
+def remove(args):
+ if os.path.exists(tools.config.session_defaults["config_path"]):
+ session_cfg = tools.config.load_session()
+ if session_cfg["session"]["state"] == "RUNNING":
+ platformService = IPlatform.get_service(args)
+ if platformService:
+ ret = platformService.removeApp(args.PACKAGE)
+ if ret != 0:
+ logging.error("Failed to uninstall package: {}".format(args.PACKAGE))
+ else:
+ logging.error("Failed to access IPlatform service")
+ else:
+ logging.error("WayDroid container is {}".format(
+ session_cfg["session"]["state"]))
+ else:
+ logging.error("WayDroid session is stopped")
+
+def launch(args):
+ def justLaunch():
+ platformService = IPlatform.get_service(args)
+ if platformService:
+ platformService.setprop("waydroid.active_apps", args.PACKAGE)
+ ret = platformService.launchApp(args.PACKAGE)
+ multiwin = platformService.getprop(
+ "persist.waydroid.multi_windows", "false")
+ if multiwin == "false":
+ platformService.settingsPutString(
+ 2, "policy_control", "immersive.status=*")
+ else:
+ platformService.settingsPutString(
+ 2, "policy_control", "immersive.full=*")
+ else:
+ logging.error("Failed to access IPlatform service")
+
+ if os.path.exists(tools.config.session_defaults["config_path"]):
+ session_cfg = tools.config.load_session()
+
+ if session_cfg["session"]["state"] == "RUNNING":
+ justLaunch()
+ elif session_cfg["session"]["state"] == "FROZEN" or session_cfg["session"]["state"] == "UNFREEZE":
+ session_cfg["session"]["state"] = "UNFREEZE"
+ tools.config.save_session(session_cfg)
+ while session_cfg["session"]["state"] != "RUNNING":
+ session_cfg = tools.config.load_session()
+ justLaunch()
+ else:
+ logging.error("WayDroid container is {}".format(
+ session_cfg["session"]["state"]))
+ else:
+ logging.error("WayDroid session is stopped")
+
+def list(args):
+ if os.path.exists(tools.config.session_defaults["config_path"]):
+ session_cfg = tools.config.load_session()
+ if session_cfg["session"]["state"] == "RUNNING":
+ platformService = IPlatform.get_service(args)
+ if platformService:
+ appsList = platformService.getAppsInfo()
+ for app in appsList:
+ print("Name: " + app["name"])
+ print("packageName: " + app["packageName"])
+ print("categories:")
+ for cat in app["categories"]:
+ print("\t" + cat)
+ else:
+ logging.error("Failed to access IPlatform service")
+ else:
+ logging.error("WayDroid container is {}".format(
+ session_cfg["session"]["state"]))
+ else:
+ logging.error("WayDroid session is stopped")
+
+def showFullUI(args):
+ platformService = IPlatform.get_service(args)
+ if platformService:
+ platformService.setprop("waydroid.active_apps", "Waydroid")
+ platformService.settingsPutString(2, "policy_control", "null*")
+ #HACK: Refresh display contents
+ statusBarService = IStatusBarService.get_service(args)
+ if statusBarService:
+ statusBarService.expand()
+ time.sleep(0.5)
+ statusBarService.collapse()
--- /dev/null
+# Copyright 2021 Erfan Abdi
+# SPDX-License-Identifier: GPL-3.0-or-later
+from shutil import which
+import logging
+import os
+import time
+import glob
+import signal
+import sys
+import tools.config
+from tools import helpers
+from tools import services
+
+
+def start(args):
+ def make_prop(full_props_path):
+ def add_prop(key, cfg_key):
+ value = session_cfg["session"][cfg_key]
+ if value != "None":
+ props.append(key + "=" + session_cfg["session"][cfg_key])
+
+ if not os.path.isfile(args.work + "/waydroid_base.prop"):
+ raise RuntimeError("waydroid_base.prop Not found")
+ with open(args.work + "/waydroid_base.prop") as f:
+ props = f.read().splitlines()
+ if not props:
+ raise RuntimeError("waydroid_base.prop is broken!!?")
+
+ add_prop("waydroid.host.user", "user_name")
+ add_prop("waydroid.host.uid", "user_id")
+ add_prop("waydroid.host.gid", "group_id")
+ add_prop("waydroid.xdg_runtime_dir", "xdg_runtime_dir")
+ add_prop("waydroid.pulse_runtime_path", "pulse_runtime_path")
+ add_prop("waydroid.wayland_display", "wayland_display")
+ if which("waydroid-sensord") is None:
+ props.append("waydroid.stub_sensors_hal=1")
+ dpi = session_cfg["session"]["lcd_density"]
+ if dpi != "0":
+ props.append("ro.sf.lcd_density=" + dpi)
+
+ final_props = open(full_props_path, "w")
+ for prop in props:
+ final_props.write(prop + "\n")
+ final_props.close()
+ os.chmod(full_props_path, 0o644)
+
+ def set_permissions(perm_list=None, mode="777"):
+ def chmod(path, mode):
+ if os.path.exists(path):
+ command = ["chmod", mode, "-R", path]
+ tools.helpers.run.root(args, command, check=False)
+
+ # Nodes list
+ if not perm_list:
+ perm_list = [
+ "/dev/ashmem",
+
+ # sw_sync for HWC
+ "/dev/sw_sync",
+ "/sys/kernel/debug/sync/sw_sync",
+
+ # Media
+ "/dev/Vcodec",
+ "/dev/MTK_SMI",
+ "/dev/mdp_sync",
+ "/dev/mtk_cmdq",
+ "/dev/video32",
+ "/dev/video33",
+
+ # Graphics
+ "/dev/dri",
+ "/dev/graphics",
+
+ # Wayland and pulse socket permissions
+ session_cfg["session"]["pulse_runtime_path"],
+ session_cfg["session"]["xdg_runtime_dir"]
+ ]
+
+ # Framebuffers
+ perm_list.extend(glob.glob("/dev/fb*"))
+
+ for path in perm_list:
+ chmod(path, mode)
+
+ def signal_handler(sig, frame):
+ services.hardware_manager.stop(args)
+ stop(args)
+ sys.exit(0)
+
+ status = helpers.lxc.status(args)
+ if status == "STOPPED":
+ # Load binder and ashmem drivers
+ cfg = tools.config.load(args)
+ if cfg["waydroid"]["vendor_type"] == "MAINLINE":
+ if helpers.drivers.probeBinderDriver(args) != 0:
+ logging.error("Failed to load Binder driver")
+ if helpers.drivers.probeAshmemDriver(args) != 0:
+ logging.error("Failed to load Ashmem driver")
+ helpers.drivers.loadBinderNodes(args)
+ set_permissions([
+ "/dev/" + args.BINDER_DRIVER,
+ "/dev/" + args.VNDBINDER_DRIVER,
+ "/dev/" + args.HWBINDER_DRIVER
+ ], "666")
+
+ if os.path.exists(tools.config.session_defaults["config_path"]):
+ session_cfg = tools.config.load_session()
+ if session_cfg["session"]["state"] != "STOPPED":
+ logging.warning("Found session config on state: {}, restart session".format(
+ session_cfg["session"]["state"]))
+ os.remove(tools.config.session_defaults["config_path"])
+ logging.debug("Container manager is waiting for session to load")
+ while not os.path.exists(tools.config.session_defaults["config_path"]):
+ time.sleep(1)
+
+ # Load session configs
+ session_cfg = tools.config.load_session()
+
+ # Generate props
+ make_prop(args.work + "/waydroid.prop")
+
+ # Networking
+ command = [tools.config.tools_src +
+ "/data/scripts/waydroid-net.sh", "start"]
+ tools.helpers.run.root(args, command, check=False)
+
+ # Sensors
+ tools.helpers.run.root(
+ args, ["waydroid-sensord", args.HWBINDER_DRIVER], output="background")
+
+ # Mount rootfs
+ helpers.images.mount_rootfs(args, cfg["waydroid"]["images_path"])
+
+ # Mount data
+ helpers.mount.bind(args, session_cfg["session"]["waydroid_data"],
+ tools.config.defaults["data"])
+
+ # Cgroup hacks
+ if which("start"):
+ command = ["start", "cgroup-lite"]
+ tools.helpers.run.root(args, command, check=False)
+ helpers.mount.umount_all(args, "/sys/fs/cgroup/schedtune")
+
+ #TODO: remove NFC hacks
+ if which("stop"):
+ command = ["stop", "nfcd"]
+ tools.helpers.run.root(args, command, check=False)
+
+ # Set permissions
+ set_permissions()
+
+ helpers.lxc.start(args)
+ session_cfg["session"]["state"] = helpers.lxc.status(args)
+ tools.config.save_session(session_cfg)
+
+ if not hasattr(args, 'hardwareLoop'):
+ services.hardware_manager.start(args)
+
+ signal.signal(signal.SIGINT, signal_handler)
+ while os.path.exists(tools.config.session_defaults["config_path"]):
+ session_cfg = tools.config.load_session()
+ if session_cfg["session"]["state"] == "STOPPED":
+ services.hardware_manager.stop(args)
+ sys.exit(0)
+ elif session_cfg["session"]["state"] == "UNFREEZE":
+ session_cfg["session"]["state"] = helpers.lxc.status(args)
+ tools.config.save_session(session_cfg)
+ unfreeze(args)
+ time.sleep(1)
+
+ logging.warning("session manager stopped, stopping container and waiting...")
+ stop(args)
+ start(args)
+ else:
+ logging.error("WayDroid container is {}".format(status))
+
+def stop(args):
+ status = helpers.lxc.status(args)
+ if status != "STOPPED":
+ helpers.lxc.stop(args)
+ if os.path.exists(tools.config.session_defaults["config_path"]):
+ session_cfg = tools.config.load_session()
+ session_cfg["session"]["state"] = helpers.lxc.status(args)
+ tools.config.save_session(session_cfg)
+
+ # Networking
+ command = [tools.config.tools_src +
+ "/data/scripts/waydroid-net.sh", "stop"]
+ tools.helpers.run.root(args, command, check=False)
+
+ #TODO: remove NFC hacks
+ if which("start"):
+ command = ["start", "nfcd"]
+ tools.helpers.run.root(args, command, check=False)
+
+ # Sensors
+ if which("waydroid-sensord"):
+ command = ["pidof", "waydroid-sensord"]
+ pid = tools.helpers.run.root(args, command, check=False, output_return=True)
+ if pid:
+ command = ["killall", pid]
+ tools.helpers.run.root(args, command, check=False)
+
+ else:
+ logging.error("WayDroid container is {}".format(status))
+
+def freeze(args):
+ status = helpers.lxc.status(args)
+ if status == "RUNNING":
+ helpers.lxc.freeze(args)
+ if os.path.exists(tools.config.session_defaults["config_path"]):
+ session_cfg = tools.config.load_session()
+ session_cfg["session"]["state"] = helpers.lxc.status(args)
+ tools.config.save_session(session_cfg)
+ else:
+ logging.error("WayDroid container is {}".format(status))
+
+def unfreeze(args):
+ status = helpers.lxc.status(args)
+ if status == "FROZEN":
+ helpers.lxc.unfreeze(args)
+ if os.path.exists(tools.config.session_defaults["config_path"]):
+ session_cfg = tools.config.load_session()
+ session_cfg["session"]["state"] = helpers.lxc.status(args)
+ tools.config.save_session(session_cfg)
+ else:
+ logging.error("WayDroid container is {}".format(status))
--- /dev/null
+# Copyright 2021 Erfan Abdi
+# SPDX-License-Identifier: GPL-3.0-or-later
+import logging
+import os
+import requests
+from tools import helpers
+import tools.config
+
+
+def get_vendor_type(args):
+ vndk_str = helpers.props.host_get(args, "ro.vndk.version")
+ ret = "MAINLINE"
+ if vndk_str != "":
+ vndk = int(vndk_str)
+ if vndk > 19:
+ ret = "HALIUM_" + str(vndk - 19)
+
+ return ret
+
+def setup_config(args):
+ cfg = tools.config.load(args)
+ args.arch = helpers.arch.host()
+ cfg["waydroid"]["arch"] = args.arch
+ cfg["waydroid"]["images_path"] = args.images_path
+
+ channels_cfg = tools.config.load_channels()
+ if not args.system_channel:
+ args.system_channel = channels_cfg["channels"]["system_channel"]
+ if not args.vendor_channel:
+ args.vendor_channel = channels_cfg["channels"]["vendor_channel"]
+ if not args.rom_type:
+ args.rom_type = channels_cfg["channels"]["rom_type"]
+ if not args.system_type:
+ args.system_type = channels_cfg["channels"]["system_type"]
+
+ args.system_ota = args.system_channel + "/" + args.rom_type + \
+ "/waydroid_" + args.arch + "/" + args.system_type + ".json"
+ system_request = requests.get(args.system_ota)
+ if system_request.status_code != 200:
+ raise ValueError(
+ "Failed to get system OTA channel: {}".format(args.system_ota))
+
+ device_codename = helpers.props.host_get(args, "ro.product.device")
+ args.vendor_type = None
+ for vendor in [device_codename, get_vendor_type(args)]:
+ vendor_ota = args.vendor_channel + "/waydroid_" + \
+ args.arch + "/" + vendor + ".json"
+ vendor_request = requests.get(vendor_ota)
+ if vendor_request.status_code == 200:
+ args.vendor_type = vendor
+ args.vendor_ota = vendor_ota
+ break
+
+ if not args.vendor_type:
+ raise ValueError(
+ "Failed to get vendor OTA channel: {}".format(vendor_ota))
+
+ cfg["waydroid"]["vendor_type"] = args.vendor_type
+ cfg["waydroid"]["system_ota"] = args.system_ota
+ cfg["waydroid"]["vendor_ota"] = args.vendor_ota
+ helpers.drivers.setupBinderNodes(args)
+ cfg["waydroid"]["binder"] = args.BINDER_DRIVER
+ cfg["waydroid"]["vndbinder"] = args.VNDBINDER_DRIVER
+ cfg["waydroid"]["hwbinder"] = args.HWBINDER_DRIVER
+ tools.config.save(args, cfg)
+
+def init(args):
+ if not os.path.isfile(args.config) or args.force:
+ setup_config(args)
+ status = "STOPPED"
+ if os.path.exists(tools.config.defaults["lxc"] + "/waydroid"):
+ status = helpers.lxc.status(args)
+ if status != "STOPPED":
+ logging.info("Stopping container")
+ helpers.lxc.stop(args)
+ helpers.images.umount_rootfs(args)
+ helpers.images.get(args)
+ if not os.path.isdir(tools.config.defaults["rootfs"]):
+ os.mkdir(tools.config.defaults["rootfs"])
+ helpers.lxc.setup_host_perms(args)
+ helpers.lxc.set_lxc_config(args)
+ helpers.lxc.make_base_props(args)
+ if status != "STOPPED":
+ logging.info("Starting container")
+ helpers.images.mount_rootfs(args, args.images_path)
+ helpers.lxc.start(args)
+ else:
+ logging.info("Already initialized")
--- /dev/null
+# Copyright 2021 Erfan Abdi
+# SPDX-License-Identifier: GPL-3.0-or-later
+import logging
+import os
+import time
+import signal
+import sys
+import tools.config
+from tools import services
+
+
+def start(args):
+ def signal_handler(sig, frame):
+ stop(args)
+ sys.exit(0)
+
+ xdg_session = os.getenv("XDG_SESSION_TYPE")
+ if xdg_session != "wayland":
+ logging.warning('XDG Session is not "wayland"')
+
+ cfg = tools.config.load_session()
+ waydroid_data = cfg["session"]["waydroid_data"]
+ if not os.path.isdir(waydroid_data):
+ os.makedirs(waydroid_data)
+ dpi = tools.helpers.props.host_get(args, "ro.sf.lcd_density")
+ if dpi == "":
+ dpi = os.getenv("GRID_UNIT_PX")
+ if dpi is not None:
+ dpi = str(int(dpi) * 20)
+ else:
+ dpi = "0"
+ cfg["session"]["lcd_density"] = dpi
+ tools.config.save_session(cfg)
+
+ services.user_manager.start(args)
+ services.clipboard_manager.start(args)
+
+ signal.signal(signal.SIGINT, signal_handler)
+ while os.path.exists(tools.config.session_defaults["config_path"]):
+ time.sleep(1)
+ services.user_manager.stop(args)
+ services.clipboard_manager.stop(args)
+
+def stop(args):
+ config_path = tools.config.session_defaults["config_path"]
+ if os.path.isfile(config_path):
+ services.user_manager.stop(args)
+ services.clipboard_manager.stop(args)
+ os.remove(config_path)
+ else:
+ logging.error("WayDroid session is not started")
--- /dev/null
+# Copyright 2021 Erfan Abdi
+# SPDX-License-Identifier: GPL-3.0-or-later
+import os
+import tools.config
+
+def print_status(args):
+ cfg = tools.config.load(args)
+ if os.path.exists(tools.config.session_defaults["config_path"]):
+ session_cfg = tools.config.load_session()
+ print("Session:\tRUNNING")
+ print("Container:\t" + session_cfg["session"]["state"])
+ print("Vendor type:\t" + cfg["waydroid"]["vendor_type"])
+ print("Session user:\t{}({})".format(
+ session_cfg["session"]["user_name"], session_cfg["session"]["user_id"]))
+ print("Wayland display:\t" +
+ session_cfg["session"]["wayland_display"])
+ else:
+ print("Session:\tSTOPPED")
+ print("Vendor type:\t" + cfg["waydroid"]["vendor_type"])
--- /dev/null
+# Copyright 2021 Erfan Abdi
+# SPDX-License-Identifier: GPL-3.0-or-later
+import logging
+import os
+from tools import helpers
+import tools.config
+
+
+def get_config(args):
+ cfg = tools.config.load(args)
+ args.arch = cfg["waydroid"]["arch"]
+ args.images_path = cfg["waydroid"]["images_path"]
+ args.vendor_type = cfg["waydroid"]["vendor_type"]
+ args.system_ota = cfg["waydroid"]["system_ota"]
+ args.vendor_ota = cfg["waydroid"]["vendor_ota"]
+
+def upgrade(args):
+ get_config(args)
+ status = "STOPPED"
+ if os.path.exists(tools.config.defaults["lxc"] + "/waydroid"):
+ status = helpers.lxc.status(args)
+ if status != "STOPPED":
+ logging.info("Stopping container")
+ helpers.lxc.stop(args)
+ helpers.images.umount_rootfs(args)
+ helpers.drivers.loadBinderNodes(args)
+ if not args.offline:
+ helpers.images.get(args)
+ helpers.lxc.setup_host_perms(args)
+ helpers.lxc.set_lxc_config(args)
+ helpers.lxc.make_base_props(args)
+ if status != "STOPPED":
+ logging.info("Starting container")
+ helpers.images.mount_rootfs(args, args.images_path)
+ helpers.lxc.start(args)
--- /dev/null
+# Copyright 2021 Oliver Smith
+# SPDX-License-Identifier: GPL-3.0-or-later
+import os
+import pwd
+
+#
+# Exported functions
+#
+from tools.config.load import load, load_session, load_channels
+from tools.config.save import save, save_session
+
+#
+# Exported variables (internal configuration)
+#
+version = "1.0.0"
+tools_src = os.path.normpath(os.path.realpath(__file__) + "/../../..")
+
+# Keys saved in the config file (mostly what we ask in 'waydroid init')
+config_keys = ["arch",
+ "images_path",
+ "vendor_type",
+ "system_datetime",
+ "vendor_datetime"]
+
+session_config_keys = ["user_name",
+ "user_id",
+ "group_id",
+ "host_user",
+ "waydroid_data",
+ "xdg_runtime_dir",
+ "wayland_display",
+ "pulse_runtime_path",
+ "state",
+ "lcd_density"]
+
+# Config file/commandline default values
+# $WORK gets replaced with the actual value for args.work (which may be
+# overridden on the commandline)
+defaults = {
+ "arch": "arm64",
+ "work": "/home/.waydroid",
+ "vendor_type": "MAINLINE",
+ "system_datetime": "0",
+ "vendor_datetime": "0"
+}
+defaults["images_path"] = defaults["work"] + "/images"
+defaults["rootfs"] = defaults["work"] + "/rootfs"
+defaults["data"] = defaults["work"] + "/data"
+defaults["lxc"] = defaults["work"] + "/lxc"
+defaults["host_perms"] = defaults["work"] + "/host-permissions"
+
+session_defaults = {
+ "user_name": pwd.getpwuid(os.getuid()).pw_name,
+ "user_id": str(os.getuid()),
+ "group_id": str(os.getgid()),
+ "host_user": os.path.expanduser("~"),
+ "xdg_runtime_dir": str(os.environ.get('XDG_RUNTIME_DIR')),
+ "wayland_display": str(os.environ.get('WAYLAND_DISPLAY')),
+ "pulse_runtime_path": str(os.environ.get('PULSE_RUNTIME_PATH')),
+ "state": "STOPPED",
+ "lcd_density": "0"
+}
+session_defaults["config_path"] = defaults["work"] + "/session.cfg"
+session_defaults["waydroid_data"] = session_defaults["host_user"] + \
+ "/waydroid/data"
+if session_defaults["pulse_runtime_path"] == "None":
+ session_defaults["pulse_runtime_path"] = session_defaults["xdg_runtime_dir"] + "/pulse"
+
+channels_defaults = {
+ "config_path": "/usr/share/waydroid-extra/channels.cfg",
+ "system_channel": "https://raw.githubusercontent.com/waydroid/OTA/master/systems",
+ "vendor_channel": "https://raw.githubusercontent.com/waydroid/OTA/master/vendor",
+ "rom_type": "lineage",
+ "system_type": "VANILLA"
+}
+channels_config_keys = ["system_channel",
+ "vendor_channel",
+ "rom_type",
+ "system_type"]
--- /dev/null
+# Copyright 2021 Oliver Smith
+# SPDX-License-Identifier: GPL-3.0-or-later
+import logging
+import configparser
+import os
+import tools.config
+
+
+def load(args):
+ cfg = configparser.ConfigParser()
+ if os.path.isfile(args.config):
+ cfg.read(args.config)
+
+ if "waydroid" not in cfg:
+ cfg["waydroid"] = {}
+
+ for key in tools.config.defaults:
+ if key in tools.config.config_keys and key not in cfg["waydroid"]:
+ cfg["waydroid"][key] = str(tools.config.defaults[key])
+
+ # We used to save default values in the config, which can *not* be
+ # configured in "waydroid init". That doesn't make sense, we always
+ # want to use the defaults from tools/config/__init__.py in that case,
+ if key not in tools.config.config_keys and key in cfg["waydroid"]:
+ logging.debug("Ignored unconfigurable and possibly outdated"
+ " default value from config: {}".format(cfg['waydroid'][key]))
+ del cfg["waydroid"][key]
+
+ return cfg
+
+def load_session():
+ config_path = tools.config.session_defaults["config_path"]
+ cfg = configparser.ConfigParser()
+ if os.path.isfile(config_path):
+ cfg.read(config_path)
+
+ if "session" not in cfg:
+ cfg["session"] = {}
+
+ for key in tools.config.session_defaults:
+ if key in tools.config.session_config_keys and key not in cfg["session"]:
+ cfg["session"][key] = str(tools.config.session_defaults[key])
+
+ if key not in tools.config.session_config_keys and key in cfg["session"]:
+ logging.debug("Ignored unconfigurable and possibly outdated"
+ " default value from config: {}".format(cfg['session'][key]))
+ del cfg["session"][key]
+
+ return cfg
+
+def load_channels():
+ config_path = tools.config.channels_defaults["config_path"]
+ cfg = configparser.ConfigParser()
+ if os.path.isfile(config_path):
+ cfg.read(config_path)
+
+ if "channels" not in cfg:
+ cfg["channels"] = {}
+
+ for key in tools.config.channels_defaults:
+ if key in tools.config.channels_config_keys and key not in cfg["channels"]:
+ cfg["channels"][key] = str(tools.config.channels_defaults[key])
+
+ if key not in tools.config.channels_config_keys and key in cfg["channels"]:
+ logging.debug("Ignored unconfigurable and possibly outdated"
+ " default value from config: {}".format(cfg['channels'][key]))
+ del cfg["channels"][key]
+
+ return cfg
--- /dev/null
+# Copyright 2021 Oliver Smith
+# SPDX-License-Identifier: GPL-3.0-or-later
+import os
+import logging
+import tools.config
+
+
+def save(args, cfg):
+ logging.debug("Save config: " + args.config)
+ os.makedirs(os.path.dirname(args.config), 0o700, True)
+ with open(args.config, "w") as handle:
+ cfg.write(handle)
+
+def save_session(cfg):
+ config_path = tools.config.session_defaults["config_path"]
+ logging.debug("Save session config: " + config_path)
+ os.makedirs(os.path.dirname(config_path), 0o700, True)
+ with open(config_path, "w") as handle:
+ cfg.write(handle)
--- /dev/null
+# Copyright 2021 Oliver Smith
+# SPDX-License-Identifier: GPL-3.0-or-later
+from tools.helpers.arguments import arguments
+import tools.helpers.arch
+import tools.helpers.props
+import tools.helpers.lxc
+import tools.helpers.images
+import tools.helpers.drivers
+import tools.helpers.mount
+import tools.helpers.http
--- /dev/null
+# Copyright 2021 Oliver Smith
+# SPDX-License-Identifier: GPL-3.0-or-later
+import platform
+
+def host():
+ machine = platform.machine()
+
+ mapping = {
+ "i686": "x86",
+ "x86_64": "x86_64",
+ "aarch64": "arm64",
+ "armv7l": "arm"
+ }
+ if machine in mapping:
+ return mapping[machine]
+ raise ValueError("platform.machine '" + machine + "'"
+ " architecture is not supported")
--- /dev/null
+# Copyright 2021 Oliver Smith
+# SPDX-License-Identifier: GPL-3.0-or-later
+import argparse
+
+try:
+ import argcomplete
+except ImportError:
+ argcomplete = False
+
+import tools.config
+
+""" This file is about parsing command line arguments passed to waydroid, as
+ well as generating the help pages (waydroid -h). All this is done with
+ Python's argparse. The parsed arguments get extended and finally stored in
+ the "args" variable, which is prominently passed to most functions all
+ over the waydroid code base.
+
+ See tools/helpers/args.py for more information about the args variable. """
+
+def arguments_init(subparser):
+ ret = subparser.add_parser("init", help="set up waydroid specific"
+ " configs and install images")
+ ret.add_argument("-i", "--images_path",
+ default=tools.config.defaults["images_path"],
+ help="custom path to waeiod images (default in"
+ " /home/.waydroid/images)")
+ ret.add_argument("-f", "--force", action="store_true",
+ help="re-initialize configs and images")
+ ret.add_argument("-c", "--system_channel",
+ help="custom system channel (options: OTA channel URL; default is Official OTA server)")
+ ret.add_argument("-v", "--vendor_channel",
+ help="custom vendor channel (options: OTA channel URL; default is Official OTA server)")
+ ret.add_argument("-r", "--rom_type",
+ help="rom type (options: \"lineage\", \"bliss\" or OTA channel URL; default is LineageOS)")
+ ret.add_argument("-s", "--system_type",
+ help="system type (options: VANILLA, FOSS or GAPPS; default is VANILLA)")
+ return ret
+
+def arguments_status(subparser):
+ ret = subparser.add_parser("status",
+ help="quick check for the waydroid")
+ return ret
+
+def arguments_upgrade(subparser):
+ ret = subparser.add_parser("upgrade", help="upgrade images")
+ ret.add_argument("-o", "--offline", action="store_true",
+ help="just for updating configs")
+ return ret
+
+def arguments_log(subparser):
+ ret = subparser.add_parser("log", help="follow the waydroid logfile")
+ ret.add_argument("-n", "--lines", default="60",
+ help="count of initial output lines")
+ ret.add_argument("-c", "--clear", help="clear the log",
+ action="store_true", dest="clear_log")
+ return ret
+
+def arguments_session(subparser):
+ ret = subparser.add_parser("session", help="session controller")
+ sub = ret.add_subparsers(title="subaction", dest="subaction")
+ sub.add_parser("start", help="start session")
+ sub.add_parser("stop", help="start session")
+ return ret
+
+def arguments_container(subparser):
+ ret = subparser.add_parser("container", help="container controller")
+ sub = ret.add_subparsers(title="subaction", dest="subaction")
+ sub.add_parser("start", help="start container")
+ sub.add_parser("stop", help="start container")
+ sub.add_parser("freeze", help="freeze container")
+ sub.add_parser("unfreeze", help="unfreeze container")
+ return ret
+
+def arguments_app(subparser):
+ ret = subparser.add_parser("app", help="applications controller")
+ sub = ret.add_subparsers(title="subaction", dest="subaction")
+ install = sub.add_parser(
+ "install", help="push a single package to the container and install it")
+ install.add_argument('PACKAGE', help="path to apk file")
+ remove = sub.add_parser(
+ "remove", help="remove single app package from the container")
+ remove.add_argument('PACKAGE', help="package name of app to remove")
+ launch = sub.add_parser("launch", help="start single application")
+ launch.add_argument('PACKAGE', help="package name of app to launch")
+ sub.add_parser("list", help="list installed applications")
+ return ret
+
+def arguments_prop(subparser):
+ ret = subparser.add_parser("prop", help="android properties controller")
+ sub = ret.add_subparsers(title="subaction", dest="subaction")
+ get = sub.add_parser(
+ "get", help="get value of property from container")
+ get.add_argument('key', help="key of the property to get")
+ set = sub.add_parser(
+ "set", help="set value to property on container")
+ set.add_argument('key', help="key of the property to set")
+ set.add_argument('value', help="value of the property to set")
+ return ret
+
+def arguments_fullUI(subparser):
+ ret = subparser.add_parser("show-full-ui", help="show android full screen in window")
+ return ret
+
+def arguments_shell(subparser):
+ ret = subparser.add_parser("shell", help="run remote shell command")
+ ret.add_argument('COMMAND', nargs='?', help="command to run")
+ return ret
+
+def arguments_logcat(subparser):
+ ret = subparser.add_parser("logcat", help="show android logcat")
+ return ret
+
+def arguments():
+ parser = argparse.ArgumentParser(prog="waydroid")
+
+ # Other
+ parser.add_argument("-V", "--version", action="version",
+ version=tools.config.version)
+
+ # Logging
+ parser.add_argument("-l", "--log", dest="log", default=None,
+ help="path to log file")
+ parser.add_argument("--details-to-stdout", dest="details_to_stdout",
+ help="print details (e.g. build output) to stdout,"
+ " instead of writing to the log",
+ action="store_true")
+ parser.add_argument("-v", "--verbose", dest="verbose",
+ action="store_true", help="write even more to the"
+ " logfiles (this may reduce performance)")
+ parser.add_argument("-q", "--quiet", dest="quiet", action="store_true",
+ help="do not output any log messages")
+
+ # Actions
+ sub = parser.add_subparsers(title="action", dest="action")
+
+ arguments_status(sub)
+ arguments_log(sub)
+ arguments_init(sub)
+ arguments_upgrade(sub)
+ arguments_session(sub)
+ arguments_container(sub)
+ arguments_app(sub)
+ arguments_prop(sub)
+ arguments_fullUI(sub)
+ arguments_shell(sub)
+ arguments_logcat(sub)
+
+ if argcomplete:
+ argcomplete.autocomplete(parser, always_complete_options="long")
+
+ # Parse and extend arguments (also backup unmodified result from argparse)
+ args = parser.parse_args()
+ return args
--- /dev/null
+# Copyright 2021 Erfan Abdi
+# SPDX-License-Identifier: GPL-3.0-or-later
+import logging
+import os
+import glob
+import tools.config
+import tools.helpers.run
+
+
+BINDER_DRIVERS = [
+ "anbox-binder",
+ "puddlejumper",
+ "binder"
+]
+VNDBINDER_DRIVERS = [
+ "anbox-vndbinder",
+ "vndpuddlejumper",
+ "vndbinder"
+]
+HWBINDER_DRIVERS = [
+ "anbox-hwbinder",
+ "hwpuddlejumper",
+ "hwbinder"
+]
+
+def probeBinderDriver(args):
+ binder_dev_nodes = []
+ has_binder = False
+ has_vndbinder = False
+ has_hwbinder = False
+ for node in BINDER_DRIVERS:
+ if os.path.exists("/dev/" + node):
+ has_binder = True
+ if not has_binder:
+ binder_dev_nodes.append(BINDER_DRIVERS[0])
+ for node in VNDBINDER_DRIVERS:
+ if os.path.exists("/dev/" + node):
+ has_vndbinder = True
+ if not has_vndbinder:
+ binder_dev_nodes.append(VNDBINDER_DRIVERS[0])
+ for node in HWBINDER_DRIVERS:
+ if os.path.exists("/dev/" + node):
+ has_hwbinder = True
+ if not has_hwbinder:
+ binder_dev_nodes.append(HWBINDER_DRIVERS[0])
+
+ if len(binder_dev_nodes) > 0:
+ devices = ','.join(binder_dev_nodes)
+ command = ["modprobe", "binder_linux", "devices=\"{}\"".format(devices)]
+ output = tools.helpers.run.root(args, command, check=False, output_return=True)
+ if output:
+ logging.error("Failed to load binder driver for devices: {}".format(devices))
+ logging.error(output.strip())
+ else:
+ command = ["mkdir", "-p", "/dev/binderfs"]
+ tools.helpers.run.root(args, command, check=False)
+ command = ["mount", "-t", "binder", "binder", "/dev/binderfs"]
+ tools.helpers.run.root(args, command, check=False)
+ command = ["ln", "-s"]
+ command.extend(glob.glob("/dev/binderfs/*"))
+ command.append("/dev/")
+ tools.helpers.run.root(args, command, check=False)
+
+ for node in binder_dev_nodes:
+ if not os.path.exists("/dev/" + node):
+ return -1
+
+ return 0
+
+def probeAshmemDriver(args):
+ if not os.path.exists("/dev/ashmem"):
+ command = ["modprobe", "ashmem_linux"]
+ output = tools.helpers.run.root(args, command, check=False, output_return=True)
+ if output:
+ logging.error("Failed to load ashmem driver")
+ logging.error(output.strip())
+
+ if not os.path.exists("/dev/ashmem"):
+ return -1
+
+ return 0
+
+def setupBinderNodes(args):
+ has_binder = False
+ has_vndbinder = False
+ has_hwbinder = False
+ if args.vendor_type == "MAINLINE":
+ probeBinderDriver(args)
+ for node in BINDER_DRIVERS:
+ if os.path.exists("/dev/" + node):
+ has_binder = True
+ args.BINDER_DRIVER = node
+ if not has_binder:
+ raise OSError('Binder node "binder" for waydroid not found')
+
+ for node in VNDBINDER_DRIVERS:
+ if os.path.exists("/dev/" + node):
+ has_vndbinder = True
+ args.VNDBINDER_DRIVER = node
+ if not has_vndbinder:
+ raise OSError('Binder node "vndbinder" for waydroid not found')
+
+ for node in HWBINDER_DRIVERS:
+ if os.path.exists("/dev/" + node):
+ has_hwbinder = True
+ args.HWBINDER_DRIVER = node
+ if not has_hwbinder:
+ raise OSError('Binder node "hwbinder" for waydroid not found')
+ else:
+ for node in BINDER_DRIVERS[:-1]:
+ if os.path.exists("/dev/" + node):
+ has_binder = True
+ args.BINDER_DRIVER = node
+ if not has_binder:
+ raise OSError('Binder node "binder" for waydroid not found')
+
+ for node in VNDBINDER_DRIVERS[:-1]:
+ if os.path.exists("/dev/" + node):
+ has_vndbinder = True
+ args.VNDBINDER_DRIVER = node
+ if not has_vndbinder:
+ raise OSError('Binder node "vndbinder" for waydroid not found')
+
+ for node in HWBINDER_DRIVERS[:-1]:
+ if os.path.exists("/dev/" + node):
+ has_hwbinder = True
+ args.HWBINDER_DRIVER = node
+ if not has_hwbinder:
+ raise OSError('Binder node "hwbinder" for waydroid not found')
+
+def loadBinderNodes(args):
+ cfg = tools.config.load(args)
+ args.BINDER_DRIVER = cfg["waydroid"]["binder"]
+ args.VNDBINDER_DRIVER = cfg["waydroid"]["vndbinder"]
+ args.HWBINDER_DRIVER = cfg["waydroid"]["hwbinder"]
--- /dev/null
+# Copyright 2021 Oliver Smith
+# SPDX-License-Identifier: GPL-3.0-or-later
+import hashlib
+import json
+import logging
+import os
+import shutil
+import urllib.request
+
+import tools.helpers.run
+
+
+def download(args, url, prefix, cache=True, loglevel=logging.INFO,
+ allow_404=False):
+ """ Download a file to disk.
+
+ :param url: the http(s) address of to the file to download
+ :param prefix: for the cache, to make it easier to find (cache files
+ get a hash of the URL after the prefix)
+ :param cache: if True, and url is cached, do not download it again
+ :param loglevel: change to logging.DEBUG to only display the download
+ message in 'waydroid log', not in stdout. We use
+ this when downloading many APKINDEX files at once, no
+ point in showing a dozen messages.
+ :param allow_404: do not raise an exception when the server responds
+ with a 404 Not Found error. Only display a warning on
+ stdout (no matter if loglevel is changed).
+ :returns: path to the downloaded file in the cache or None on 404 """
+ # Create cache folder
+ if not os.path.exists(args.work + "/cache_http"):
+ tools.helpers.run.user(args, ["mkdir", "-p", args.work + "/cache_http"])
+
+ # Check if file exists in cache
+ prefix = prefix.replace("/", "_")
+ path = (args.work + "/cache_http/" + prefix + "_" +
+ hashlib.sha256(url.encode("utf-8")).hexdigest())
+ if os.path.exists(path):
+ if cache:
+ return path
+ tools.helpers.run.user(args, ["rm", path])
+
+ # Download the file
+ logging.log(loglevel, "Download " + url)
+ try:
+ with urllib.request.urlopen(url) as response:
+ with open(path, "wb") as handle:
+ shutil.copyfileobj(response, handle)
+ # Handle 404
+ except urllib.error.HTTPError as e:
+ if e.code == 404 and allow_404:
+ logging.warning("WARNING: file not found: " + url)
+ return None
+ raise
+
+ # Return path in cache
+ return path
+
+
+def retrieve(url, headers=None, allow_404=False):
+ """ Fetch the content of a URL and returns it as string.
+
+ :param url: the http(s) address of to the resource to fetch
+ :param headers: dict of HTTP headers to use
+ :param allow_404: do not raise an exception when the server responds
+ with a 404 Not Found error. Only display a warning
+ :returns: str with the content of the response
+ """
+ # Download the file
+ logging.verbose("Retrieving " + url)
+
+ if headers is None:
+ headers = {}
+
+ req = urllib.request.Request(url, headers=headers)
+ try:
+ with urllib.request.urlopen(req) as response:
+ return response.read()
+ # Handle 404
+ except urllib.error.HTTPError as e:
+ if e.code == 404 and allow_404:
+ logging.warning("WARNING: failed to retrieve content from: " + url)
+ return None
+ raise
+
+
+def retrieve_json(*args, **kwargs):
+ """ Fetch the contents of a URL, parse it as JSON and return it. See
+ retrieve() for the list of all parameters. """
+ return json.loads(retrieve(*args, **kwargs))
--- /dev/null
+# Copyright 2021 Erfan Abdi
+# SPDX-License-Identifier: GPL-3.0-or-later
+import logging
+import zipfile
+import requests
+import hashlib
+import os
+import tools.config
+from tools import helpers
+
+
+def sha256sum(filename):
+ h = hashlib.sha256()
+ b = bytearray(128*1024)
+ mv = memoryview(b)
+ with open(filename, 'rb', buffering=0) as f:
+ for n in iter(lambda: f.readinto(mv), 0):
+ h.update(mv[:n])
+ return h.hexdigest()
+
+
+def get(args):
+ cfg = tools.config.load(args)
+ system_ota = cfg["waydroid"]["system_ota"]
+ system_request = requests.get(system_ota)
+ if system_request.status_code != 200:
+ raise ValueError(
+ "Failed to get system OTA channel: {}".format(system_ota))
+ system_responses = system_request.json()["response"]
+ if len(system_responses) < 1:
+ raise ValueError("No images found on system channel")
+
+ for system_response in system_responses:
+ if system_response['datetime'] > int(cfg["waydroid"]["system_datetime"]):
+ images_zip = helpers.http.download(
+ args, system_response['url'], system_response['filename'], cache=False)
+ logging.info("Validating system image")
+ if sha256sum(images_zip) != system_response['id']:
+ raise ValueError("Downloaded system image hash doesn't match, expected: {}".format(
+ system_response['id']))
+ logging.info("Extracting to " + args.images_path)
+ with zipfile.ZipFile(images_zip, 'r') as zip_ref:
+ zip_ref.extractall(args.images_path)
+ cfg["waydroid"]["system_datetime"] = str(system_response['datetime'])
+ tools.config.save(args, cfg)
+ os.remove(images_zip)
+ break
+
+ vendor_ota = cfg["waydroid"]["vendor_ota"]
+ vendor_request = requests.get(vendor_ota)
+ if vendor_request.status_code != 200:
+ raise ValueError(
+ "Failed to get vendor OTA channel: {}".format(vendor_ota))
+ vendor_responses = vendor_request.json()["response"]
+ if len(vendor_responses) < 1:
+ raise ValueError("No images found on vendor channel")
+
+ for vendor_response in vendor_responses:
+ if vendor_response['datetime'] > int(cfg["waydroid"]["vendor_datetime"]):
+ images_zip = helpers.http.download(
+ args, vendor_response['url'], vendor_response['filename'], cache=False)
+ logging.info("Validating vendor image")
+ if sha256sum(images_zip) != vendor_response['id']:
+ raise ValueError("Downloaded vendor image hash doesn't match, expected: {}".format(
+ vendor_response['id']))
+ logging.info("Extracting to " + args.images_path)
+ with zipfile.ZipFile(images_zip, 'r') as zip_ref:
+ zip_ref.extractall(args.images_path)
+ cfg["waydroid"]["vendor_datetime"] = str(vendor_response['datetime'])
+ tools.config.save(args, cfg)
+ os.remove(images_zip)
+ break
+
+def replace(args, system_zip, system_time, vendor_zip, vendor_time):
+ cfg = tools.config.load(args)
+ args.images_path = cfg["waydroid"]["images_path"]
+ if os.path.exists(system_zip):
+ with zipfile.ZipFile(system_zip, 'r') as zip_ref:
+ zip_ref.extractall(args.images_path)
+ cfg["waydroid"]["system_datetime"] = str(system_time)
+ tools.config.save(args, cfg)
+ if os.path.exists(vendor_zip):
+ with zipfile.ZipFile(vendor_zip, 'r') as zip_ref:
+ zip_ref.extractall(args.images_path)
+ cfg["waydroid"]["vendor_datetime"] = str(vendor_time)
+ tools.config.save(args, cfg)
+
+
+def mount_rootfs(args, images_dir):
+ helpers.mount.mount(args, images_dir + "/system.img",
+ tools.config.defaults["rootfs"], umount=True)
+ helpers.mount.mount(args, images_dir + "/vendor.img",
+ tools.config.defaults["rootfs"] + "/vendor")
+ for egl_path in ["/vendor/lib/egl", "/vendor/lib64/egl"]:
+ if os.path.isdir(egl_path):
+ helpers.mount.bind(
+ args, egl_path, tools.config.defaults["rootfs"] + egl_path)
+ if helpers.mount.ismount("/odm"):
+ helpers.mount.bind(
+ args, "/odm", tools.config.defaults["rootfs"] + "/odm_extra")
+ else:
+ if os.path.isdir("/vendor/odm"):
+ helpers.mount.bind(
+ args, "/vendor/odm", tools.config.defaults["rootfs"] + "/odm_extra")
+ helpers.mount.bind_file(args, args.work + "/waydroid.prop",
+ tools.config.defaults["rootfs"] + "/vendor/waydroid.prop")
+
+def umount_rootfs(args):
+ helpers.mount.umount_all(args, tools.config.defaults["rootfs"])
--- /dev/null
+# Copyright 2021 Oliver Smith
+# SPDX-License-Identifier: GPL-3.0-or-later
+import logging
+import os
+import sys
+
+
+class log_handler(logging.StreamHandler):
+ """
+ Write to stdout and to the already opened log file.
+ """
+ _args = None
+
+ def emit(self, record):
+ try:
+ msg = self.format(record)
+
+ # INFO or higher: Write to stdout
+ if (not self._args.details_to_stdout and
+ not self._args.quiet and
+ record.levelno >= logging.INFO):
+ stream = self.stream
+ stream.write(msg)
+ stream.write(self.terminator)
+ self.flush()
+
+ # Everything: Write to logfd
+ msg = "(" + str(os.getpid()).zfill(6) + ") " + msg
+ self._args.logfd.write(msg + "\n")
+ self._args.logfd.flush()
+
+ except (KeyboardInterrupt, SystemExit):
+ raise
+ except BaseException:
+ self.handleError(record)
+
+
+def add_verbose_log_level():
+ """
+ Add a new log level "verbose", which is below "debug". Also monkeypatch
+ logging, so it can be used with logging.verbose().
+
+ This function is based on work by Voitek Zylinski and sleepycal:
+ https://stackoverflow.com/a/20602183
+ All stackoverflow user contributions are licensed as CC-BY-SA:
+ https://creativecommons.org/licenses/by-sa/3.0/
+ """
+ logging.VERBOSE = 5
+ logging.addLevelName(logging.VERBOSE, "VERBOSE")
+ logging.Logger.verbose = lambda inst, msg, * \
+ args, **kwargs: inst.log(logging.VERBOSE, msg, *args, **kwargs)
+ logging.verbose = lambda msg, *args, **kwargs: logging.log(logging.VERBOSE,
+ msg, *args,
+ **kwargs)
+
+
+def init(args):
+ """
+ Set log format and add the log file descriptor to args.logfd, add the
+ verbose log level.
+ """
+ # Set log file descriptor (logfd)
+ if args.details_to_stdout:
+ setattr(args, "logfd", sys.stdout)
+ else:
+ # Require containing directory to exist (so we don't create the work
+ # folder and break the folder migration logic, which needs to set the
+ # version upon creation)
+ dir = os.path.dirname(args.log)
+ if os.path.exists(dir):
+ setattr(args, "logfd", open(args.log, "a+"))
+ else:
+ setattr(args, "logfd", open(os.devnull, "a+"))
+ if args.action != "init":
+ print("WARNING: Can't create log file in '" + dir + "', path"
+ " does not exist!")
+
+ # Set log format
+ root_logger = logging.getLogger()
+ root_logger.handlers = []
+ formatter = logging.Formatter("[%(asctime)s] %(message)s",
+ datefmt="%H:%M:%S")
+
+ # Set log level
+ add_verbose_log_level()
+ root_logger.setLevel(logging.DEBUG)
+ if args.verbose:
+ root_logger.setLevel(logging.VERBOSE)
+
+ # Add a custom log handler
+ handler = log_handler()
+ log_handler._args = args
+ handler.setFormatter(formatter)
+ root_logger.addHandler(handler)
+
+
+def disable():
+ logger = logging.getLogger()
+ logger.disabled = True
--- /dev/null
+# Copyright 2021 Erfan Abdi
+# SPDX-License-Identifier: GPL-3.0-or-later
+import subprocess
+import os
+import re
+import logging
+import glob
+import shutil
+import platform
+import tools.config
+import tools.helpers.run
+
+
+def get_lxc_version(args):
+ if shutil.which("lxc-info") is not None:
+ command = ["lxc-info", "--version"]
+ version_str = tools.helpers.run.user(args, command, output_return=True)
+ return int(version_str[0])
+ else:
+ return 0
+
+
+def generate_nodes_lxc_config(args):
+ def make_entry(src, dist=None, mnt_type="none", options="bind,create=file,optional 0 0", check=True):
+ if check and not os.path.exists(src):
+ return False
+ entry = "lxc.mount.entry = "
+ entry += src + " "
+ if dist is None:
+ dist = src[1:]
+ entry += dist + " "
+ entry += mnt_type + " "
+ entry += options
+ nodes.append(entry)
+ return True
+
+ nodes = []
+ # Necessary dev nodes
+ make_entry("tmpfs", "dev", "tmpfs", "nosuid 0 0", False)
+ make_entry("/dev/zero")
+ make_entry("/dev/full")
+ make_entry("/dev/ashmem", check=False)
+ make_entry("/dev/fuse")
+ make_entry("/dev/ion")
+ make_entry("/dev/char", options="bind,create=dir,optional 0 0")
+
+ # Graphic dev nodes
+ make_entry("/dev/kgsl-3d0")
+ make_entry("/dev/mali0")
+ make_entry("/dev/pvr_sync")
+ make_entry("/dev/pmsg0")
+ make_entry("/dev/fb0")
+ make_entry("/dev/graphics/fb0")
+ make_entry("/dev/fb1")
+ make_entry("/dev/graphics/fb1")
+ make_entry("/dev/fb2")
+ make_entry("/dev/graphics/fb2")
+ make_entry("/dev/dri", options="bind,create=dir,optional 0 0")
+
+ # Binder dev nodes
+ make_entry("/dev/" + args.BINDER_DRIVER, "dev/binder")
+ make_entry("/dev/" + args.VNDBINDER_DRIVER, "dev/vndbinder")
+ make_entry("/dev/" + args.HWBINDER_DRIVER, "dev/hwbinder")
+
+ if args.vendor_type != "MAINLINE":
+ if not make_entry("/dev/hwbinder", "dev/host_hwbinder"):
+ raise OSError('Binder node "hwbinder" of host not found')
+ make_entry("/vendor", "vendor_extra", options="bind,optional 0 0")
+
+ # Necessary device nodes for adb
+ make_entry("none", "dev/pts", "devpts", "defaults,mode=644,ptmxmode=666,create=dir 0 0", False)
+ make_entry("/dev/uhid")
+
+ # Low memory killer sys node
+ make_entry("/sys/module/lowmemorykiller", options="bind,create=dir,optional 0 0")
+
+ # Mount /data
+ make_entry("tmpfs", "mnt", "tmpfs", "mode=0755,uid=0,gid=1000", False)
+ make_entry(tools.config.defaults["data"], "data", options="bind 0 0", check=False)
+
+ # Mount host permissions
+ make_entry(tools.config.defaults["host_perms"],
+ "vendor/etc/host-permissions", options="bind,optional 0 0")
+
+ # Recursive mount /run to provide necessary host sockets
+ make_entry("/run", options="rbind,create=dir 0 0")
+
+ # Necessary sw_sync node for HWC
+ make_entry("/dev/sw_sync")
+ make_entry("/sys/kernel/debug", options="rbind,create=dir,optional 0 0")
+
+ # Media dev nodes (for Mediatek)
+ make_entry("/dev/Vcodec")
+ make_entry("/dev/MTK_SMI")
+ make_entry("/dev/mdp_sync")
+ make_entry("/dev/mtk_cmdq")
+
+ # Media dev nodes (for Qcom)
+ make_entry("/dev/video32")
+ make_entry("/dev/video33")
+
+ return nodes
+
+
+def set_lxc_config(args):
+ lxc_path = tools.config.defaults["lxc"] + "/waydroid"
+ config_file = "config_2"
+ lxc_ver = get_lxc_version(args)
+ if lxc_ver == 0:
+ raise OSError("LXC is not installed")
+ elif lxc_ver <= 2:
+ config_file = "config_1"
+ config_path = tools.config.tools_src + "/data/configs/" + config_file
+
+ command = ["mkdir", "-p", lxc_path]
+ tools.helpers.run.root(args, command)
+ command = ["cp", "-fpr", config_path, lxc_path + "/config"]
+ tools.helpers.run.root(args, command)
+ command = ["sed", "-i", "s/LXCARCH/{}/".format(platform.machine()), lxc_path + "/config"]
+ tools.helpers.run.root(args, command)
+
+ nodes = generate_nodes_lxc_config(args)
+ config_nodes_tmp_path = args.work + "/config_nodes"
+ config_nodes = open(config_nodes_tmp_path, "w")
+ for node in nodes:
+ config_nodes.write(node + "\n")
+ config_nodes.close()
+ command = ["mv", config_nodes_tmp_path, lxc_path]
+ tools.helpers.run.root(args, command)
+
+
+def make_base_props(args):
+ def find_hal(hardware):
+ hardware_props = [
+ "ro.hardware." + hardware,
+ "ro.hardware",
+ "ro.product.board",
+ "ro.arch",
+ "ro.board.platform"]
+ for p in hardware_props:
+ prop = tools.helpers.props.host_get(args, p)
+ hal_prop = ""
+ if prop != "":
+ for lib in ["lib", "lib64"]:
+ hal_file = "/vendor/" + lib + "/hw/" + hardware + "." + prop + ".so"
+ command = ["readlink", "-f", hal_file]
+ hal_file_path = tools.helpers.run.root(args, command, output_return=True).strip()
+ if os.path.isfile(hal_file_path):
+ hal_prop = re.sub(".*" + hardware + ".", "", hal_file_path)
+ hal_prop = re.sub(".so", "", hal_prop)
+ if hal_prop != "":
+ return hal_prop
+ if hal_prop != "":
+ return hal_prop
+ return ""
+
+ props = []
+ gralloc = find_hal("gralloc")
+ if gralloc == "":
+ gralloc = "gbm"
+ props.append("ro.hardware.egl=mesa")
+ props.append("debug.stagefright.ccodec=0")
+ props.append("ro.hardware.gralloc=" + gralloc)
+
+ egl = tools.helpers.props.host_get(args, "ro.hardware.egl")
+ if egl != "":
+ props.append("ro.hardware.egl=" + egl)
+
+ media_profiles = tools.helpers.props.host_get(args, "media.settings.xml")
+ if media_profiles != "":
+ media_profiles = media_profiles.replace("vendor/", "vendor_extra/")
+ media_profiles = media_profiles.replace("odm/", "odm_extra/")
+ props.append("media.settings.xml=" + media_profiles)
+
+ ccodec = tools.helpers.props.host_get(args, "debug.stagefright.ccodec")
+ if ccodec != "":
+ props.append("debug.stagefright.ccodec=" + ccodec)
+
+ ext_library = tools.helpers.props.host_get(args, "ro.vendor.extension_library")
+ if ext_library != "":
+ ext_library = ext_library.replace("vendor/", "vendor_extra/")
+ ext_library = ext_library.replace("odm/", "odm_extra/")
+ props.append("ro.vendor.extension_library=" + ext_library)
+
+ vulkan = find_hal("vulkan")
+ if vulkan != "":
+ props.append("ro.hardware.vulkan=" + vulkan)
+
+ opengles = tools.helpers.props.host_get(args, "ro.opengles.version")
+ if opengles == "":
+ opengles = "196608"
+ props.append("ro.opengles.version=" + opengles)
+
+ props.append("waydroid.system_ota=" + args.system_ota)
+ props.append("waydroid.vendor_ota=" + args.vendor_ota)
+ props.append("waydroid.tools_version=" + tools.config.version)
+
+ base_props = open(args.work + "/waydroid_base.prop", "w")
+ for prop in props:
+ base_props.write(prop + "\n")
+ base_props.close()
+
+
+def setup_host_perms(args):
+ sku = tools.helpers.props.host_get(args, "ro.boot.product.hardware.sku")
+ copy_list = []
+ copy_list.extend(
+ glob.glob("/vendor/etc/permissions/android.hardware.nfc.*"))
+ if os.path.exists("/vendor/etc/permissions/android.hardware.consumerir.xml"):
+ copy_list.append("/vendor/etc/permissions/android.hardware.consumerir.xml")
+ copy_list.extend(
+ glob.glob("/odm/etc/permissions/android.hardware.nfc.*"))
+ if os.path.exists("/odm/etc/permissions/android.hardware.consumerir.xml"):
+ copy_list.append("/odm/etc/permissions/android.hardware.consumerir.xml")
+ if sku != "":
+ copy_list.extend(
+ glob.glob("/odm/etc/permissions/sku_{}/android.hardware.nfc.*".format(sku)))
+ if os.path.exists("/odm/etc/permissions/sku_{}/android.hardware.consumerir.xml".format(sku)):
+ copy_list.append(
+ "/odm/etc/permissions/sku_{}/android.hardware.consumerir.xml".format(sku))
+
+ if not os.path.exists(tools.config.defaults["host_perms"]):
+ os.mkdir(tools.config.defaults["host_perms"])
+
+ for filename in copy_list:
+ shutil.copy(filename, tools.config.defaults["host_perms"])
+
+def status(args):
+ command = ["sudo", "lxc-info", "-P", tools.config.defaults["lxc"], "-n", "waydroid", "-sH"]
+ return subprocess.run(command, stdout=subprocess.PIPE).stdout.decode('utf-8').strip()
+
+def start(args):
+ command = ["lxc-start", "-P", tools.config.defaults["lxc"],
+ "-F", "-n", "waydroid", "--", "/init"]
+ tools.helpers.run.root(args, command, output="background")
+
+def stop(args):
+ command = ["lxc-stop", "-P",
+ tools.config.defaults["lxc"], "-n", "waydroid", "-k"]
+ tools.helpers.run.root(args, command)
+
+def freeze(args):
+ command = ["lxc-freeze", "-P", tools.config.defaults["lxc"], "-n", "waydroid"]
+ tools.helpers.run.root(args, command)
+
+def unfreeze(args):
+ command = ["lxc-unfreeze", "-P",
+ tools.config.defaults["lxc"], "-n", "waydroid"]
+ tools.helpers.run.root(args, command)
+
+def shell(args):
+ if status(args) != "RUNNING":
+ logging.error("WayDroid container is {}".format(status(args)))
+ return
+ command = ["lxc-attach", "-P", tools.config.defaults["lxc"],
+ "-n", "waydroid", "--"]
+ if args.COMMAND:
+ command.append(args.COMMAND)
+ else:
+ command.append("/system/bin/sh")
+ subprocess.run(command)
+
+def logcat(args):
+ if status(args) != "RUNNING":
+ logging.error("WayDroid container is {}".format(status(args)))
+ return
+ command = ["lxc-attach", "-P", tools.config.defaults["lxc"],
+ "-n", "waydroid", "--", "/system/bin/logcat"]
+ subprocess.run(command)
--- /dev/null
+# Copyright 2021 Oliver Smith
+# SPDX-License-Identifier: GPL-3.0-or-later
+import os
+import tools.helpers.run
+
+
+def ismount(folder):
+ """
+ Ismount() implementation, that works for mount --bind.
+ Workaround for: https://bugs.python.org/issue29707
+ """
+ folder = os.path.realpath(os.path.realpath(folder))
+ with open("/proc/mounts", "r") as handle:
+ for line in handle:
+ words = line.split()
+ if len(words) >= 2 and words[1] == folder:
+ return True
+ if words[0] == folder:
+ return True
+ return False
+
+
+def bind(args, source, destination, create_folders=True, umount=False):
+ """
+ Mount --bind a folder and create necessary directory structure.
+ :param umount: when destination is already a mount point, umount it first.
+ """
+ # Check/umount destination
+ if ismount(destination):
+ if umount:
+ umount_all(args, destination)
+ else:
+ return
+
+ # Check/create folders
+ for path in [source, destination]:
+ if os.path.exists(path):
+ continue
+ if create_folders:
+ tools.helpers.run.root(args, ["mkdir", "-p", path])
+ else:
+ raise RuntimeError("Mount failed, folder does not exist: " +
+ path)
+
+ # Actually mount the folder
+ tools.helpers.run.root(args, ["mount", "-o", "bind", source, destination])
+
+ # Verify, that it has worked
+ if not ismount(destination):
+ raise RuntimeError("Mount failed: " + source + " -> " + destination)
+
+
+def bind_file(args, source, destination, create_folders=False):
+ """
+ Mount a file with the --bind option, and create the destination file,
+ if necessary.
+ """
+ # Skip existing mountpoint
+ if ismount(destination):
+ return
+
+ # Create empty file
+ if not os.path.exists(destination):
+ if create_folders:
+ dir = os.path.dirname(destination)
+ if not os.path.isdir(dir):
+ tools.helpers.run.root(args, ["mkdir", "-p", dir])
+
+ tools.helpers.run.root(args, ["touch", destination])
+
+ # Mount
+ tools.helpers.run.root(args, ["mount", "-o", "bind", source,
+ destination])
+
+
+def umount_all_list(prefix, source="/proc/mounts"):
+ """
+ Parses `/proc/mounts` for all folders beginning with a prefix.
+ :source: can be changed for testcases
+ :returns: a list of folders, that need to be umounted
+ """
+ ret = []
+ prefix = os.path.realpath(prefix)
+ with open(source, "r") as handle:
+ for line in handle:
+ words = line.split()
+ if len(words) < 2:
+ raise RuntimeError("Failed to parse line in " + source + ": " +
+ line)
+ mountpoint = words[1]
+ if mountpoint.startswith(prefix):
+ # Remove "\040(deleted)" suffix (#545)
+ deleted_str = r"\040(deleted)"
+ if mountpoint.endswith(deleted_str):
+ mountpoint = mountpoint[:-len(deleted_str)]
+ ret.append(mountpoint)
+ ret.sort(reverse=True)
+ return ret
+
+
+def umount_all(args, folder):
+ """
+ Umount all folders, that are mounted inside a given folder.
+ """
+ for mountpoint in umount_all_list(folder):
+ tools.helpers.run.root(args, ["umount", mountpoint])
+ if ismount(mountpoint):
+ raise RuntimeError("Failed to umount: " + mountpoint)
+
+def mount(args, source, destination, create_folders=True, umount=False, readonly=True):
+ """
+ Mount and create necessary directory structure.
+ :param umount: when destination is already a mount point, umount it first.
+ """
+ # Check/umount destination
+ if ismount(destination):
+ if umount:
+ umount_all(args, destination)
+ else:
+ return
+
+ # Check/create folders
+ if not os.path.exists(destination):
+ if create_folders:
+ tools.helpers.run.root(args, ["mkdir", "-p", destination])
+ else:
+ raise RuntimeError("Mount failed, folder does not exist: " +
+ destination)
+
+ # Actually mount the folder
+ tools.helpers.run.root(args, ["mount", source, destination])
+ if readonly:
+ tools.helpers.run.root(args, ["mount", "-o", "remount,ro", source, destination])
+
+ # Verify, that it has worked
+ if not ismount(destination):
+ raise RuntimeError("Mount failed: " + source + " -> " + destination)
--- /dev/null
+# Copyright 2021 Oliver Smith
+# SPDX-License-Identifier: GPL-3.0-or-later
+from shutil import which
+import logging
+import os
+import tools.helpers.run
+from tools.interfaces import IPlatform
+
+
+def host_get(args, prop):
+ if which("getprop") is not None:
+ command = ["getprop", prop]
+ return tools.helpers.run.user(args, command, output_return=True).strip()
+ else:
+ return ""
+
+def host_set(args, prop, value):
+ if which("setprop") is not None:
+ command = ["setprop", prop, value]
+ tools.helpers.run.user(args, command)
+
+def get(args, prop):
+ if os.path.exists(tools.config.session_defaults["config_path"]):
+ session_cfg = tools.config.load_session()
+ if session_cfg["session"]["state"] == "RUNNING":
+ platformService = IPlatform.get_service(args)
+ if platformService:
+ return platformService.getprop(prop, "")
+ else:
+ logging.error("Failed to access IPlatform service")
+ else:
+ logging.error("WayDroid container is {}".format(
+ session_cfg["session"]["state"]))
+ else:
+ logging.error("WayDroid session is stopped")
+
+def set(args, prop, value):
+ if os.path.exists(tools.config.session_defaults["config_path"]):
+ session_cfg = tools.config.load_session()
+ if session_cfg["session"]["state"] == "RUNNING":
+ platformService = IPlatform.get_service(args)
+ if platformService:
+ platformService.setprop(prop, value)
+ else:
+ logging.error("Failed to access IPlatform service")
+ else:
+ logging.error("WayDroid container is {}".format(
+ session_cfg["session"]["state"]))
+ else:
+ logging.error("WayDroid session is stopped")
--- /dev/null
+# Copyright 2021 Oliver Smith
+# SPDX-License-Identifier: GPL-3.0-or-later
+import shlex
+import tools.helpers.run_core
+
+
+def flat_cmd(cmd, working_dir=None, env={}):
+ """
+ Convert a shell command passed as list into a flat shell string with
+ proper escaping.
+
+ :param cmd: command as list, e.g. ["echo", "string with spaces"]
+ :param working_dir: when set, prepend "cd ...;" to execute the command
+ in the given working directory
+ :param env: dict of environment variables to be passed to the command, e.g.
+ {"JOBS": "5"}
+ :returns: the flat string, e.g.
+ echo 'string with spaces'
+ cd /home/pmos;echo 'string with spaces'
+ """
+ # Merge env and cmd into escaped list
+ escaped = []
+ for key, value in env.items():
+ escaped.append(key + "=" + shlex.quote(value))
+ for i in range(len(cmd)):
+ escaped.append(shlex.quote(cmd[i]))
+
+ # Prepend working dir
+ ret = " ".join(escaped)
+ if working_dir:
+ ret = "cd " + shlex.quote(working_dir) + ";" + ret
+
+ return ret
+
+
+def user(args, cmd, working_dir=None, output="log", output_return=False,
+ check=None, env={}, sudo=False):
+ """
+ Run a command on the host system as user.
+
+ :param env: dict of environment variables to be passed to the command, e.g.
+ {"JOBS": "5"}
+
+ See tools.helpers.run_core.core() for a detailed description of all other
+ arguments and the return value.
+ """
+ # Readable log message (without all the escaping)
+ msg = "% "
+ for key, value in env.items():
+ msg += key + "=" + value + " "
+ if working_dir:
+ msg += "cd " + working_dir + "; "
+ msg += " ".join(cmd)
+
+ # Add environment variables and run
+ if env:
+ cmd = ["sh", "-c", flat_cmd(cmd, env=env)]
+ return tools.helpers.run_core.core(args, msg, cmd, working_dir, output,
+ output_return, check, sudo)
+
+
+def root(args, cmd, working_dir=None, output="log", output_return=False,
+ check=None, env={}):
+ """
+ Run a command on the host system as root, with sudo.
+
+ :param env: dict of environment variables to be passed to the command, e.g.
+ {"JOBS": "5"}
+
+ See tools.helpers.run_core.core() for a detailed description of all other
+ arguments and the return value.
+ """
+ if env:
+ cmd = ["sh", "-c", flat_cmd(cmd, env=env)]
+ cmd = ["sudo"] + cmd
+
+ return user(args, cmd, working_dir, output, output_return, check, env,
+ True)
--- /dev/null
+# Copyright 2021 Oliver Smith
+# SPDX-License-Identifier: GPL-3.0-or-later
+import fcntl
+import logging
+import selectors
+import subprocess
+import sys
+import threading
+import time
+import os
+import tools.helpers.run
+
+""" For a detailed description of all output modes, read the description of
+ core() at the bottom. All other functions in this file get (indirectly)
+ called by core(). """
+
+
+def sanity_checks(output="log", output_return=False, check=None):
+ """
+ Raise an exception if the parameters passed to core() don't make sense
+ (all parameters are described in core() below).
+ """
+ vals = ["log", "stdout", "interactive", "tui", "background", "pipe"]
+ if output not in vals:
+ raise RuntimeError("Invalid output value: " + str(output))
+
+ # Prevent setting the check parameter with output="background".
+ # The exit code won't be checked when running in background, so it would
+ # always by check=False. But we prevent it from getting set to check=False
+ # as well, so it does not look like you could change it to check=True.
+ if check is not None and output == "background":
+ raise RuntimeError("Can't use check with output: background")
+
+ if output_return and output in ["tui", "background"]:
+ raise RuntimeError("Can't use output_return with output: " + output)
+
+
+def background(args, cmd, working_dir=None):
+ """ Run a subprocess in background and redirect its output to the log. """
+ ret = subprocess.Popen(cmd, stdout=args.logfd, stderr=args.logfd,
+ cwd=working_dir)
+ logging.debug("New background process: pid={}, output=background".format(ret.pid))
+ return ret
+
+
+def pipe(args, cmd, working_dir=None):
+ """ Run a subprocess in background and redirect its output to a pipe. """
+ ret = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=args.logfd,
+ cwd=working_dir)
+ logging.verbose("New background process: pid={}, output=pipe".format(ret.pid))
+ return ret
+
+
+def pipe_read(args, process, output_to_stdout=False, output_return=False,
+ output_return_buffer=False):
+ """
+ Read all available output from a subprocess and copy it to the log and
+ optionally stdout and a buffer variable. This is only meant to be called by
+ foreground_pipe() below.
+
+ :param process: subprocess.Popen instance
+ :param output_to_stdout: copy all output to waydroid's stdout
+ :param output_return: when set to True, output_return_buffer will be
+ extended
+ :param output_return_buffer: list of bytes that gets extended with the
+ current output in case output_return is True.
+ """
+ while True:
+ # Copy available output
+ out = process.stdout.readline()
+ if len(out):
+ args.logfd.buffer.write(out)
+ if output_to_stdout:
+ sys.stdout.buffer.write(out)
+ if output_return:
+ output_return_buffer.append(out)
+ continue
+
+ # No more output (flush buffers)
+ args.logfd.flush()
+ if output_to_stdout:
+ sys.stdout.flush()
+ return
+
+
+def kill_process_tree(args, pid, ppids, sudo):
+ """
+ Recursively kill a pid and its child processes
+
+ :param pid: process id that will be killed
+ :param ppids: list of process id and parent process id tuples (pid, ppid)
+ :param sudo: use sudo to kill the process
+ """
+ if sudo:
+ tools.helpers.run.root(args, ["kill", "-9", str(pid)],
+ check=False)
+ else:
+ tools.helpers.run.user(args, ["kill", "-9", str(pid)],
+ check=False)
+
+ for (child_pid, child_ppid) in ppids:
+ if child_ppid == str(pid):
+ kill_process_tree(args, child_pid, ppids, sudo)
+
+
+def kill_command(args, pid, sudo):
+ """
+ Kill a command process and recursively kill its child processes
+
+ :param pid: process id that will be killed
+ :param sudo: use sudo to kill the process
+ """
+ cmd = ["ps", "-e", "-o", "pid,ppid"]
+ ret = subprocess.run(cmd, check=True, stdout=subprocess.PIPE)
+ ppids = []
+ proc_entries = ret.stdout.decode("utf-8").rstrip().split('\n')[1:]
+ for row in proc_entries:
+ items = row.split()
+ if len(items) != 2:
+ raise RuntimeError("Unexpected ps output: " + row)
+ ppids.append(items)
+
+ kill_process_tree(args, pid, ppids, sudo)
+
+
+def foreground_pipe(args, cmd, working_dir=None, output_to_stdout=False,
+ output_return=False, output_timeout=True,
+ sudo=False):
+ """
+ Run a subprocess in foreground with redirected output and optionally kill
+ it after being silent for too long.
+
+ :param cmd: command as list, e.g. ["echo", "string with spaces"]
+ :param working_dir: path in host system where the command should run
+ :param output_to_stdout: copy all output to waydroid's stdout
+ :param output_return: return the output of the whole program
+ :param output_timeout: kill the process when it doesn't print any output
+ after a certain time (configured with --timeout)
+ and raise a RuntimeError exception
+ :param sudo: use sudo to kill the process when it hits the timeout
+ :returns: (code, output)
+ * code: return code of the program
+ * output: ""
+ * output: full program output string (output_return is True)
+ """
+ # Start process in background (stdout and stderr combined)
+ process = subprocess.Popen(cmd, stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT, cwd=working_dir)
+
+ # Make process.stdout non-blocking
+ handle = process.stdout.fileno()
+ flags = fcntl.fcntl(handle, fcntl.F_GETFL)
+ fcntl.fcntl(handle, fcntl.F_SETFL, flags | os.O_NONBLOCK)
+
+ # While process exists wait for output (with timeout)
+ output_buffer = []
+ sel = selectors.DefaultSelector()
+ sel.register(process.stdout, selectors.EVENT_READ)
+ timeout = args.timeout if output_timeout else None
+ while process.poll() is None:
+ wait_start = time.perf_counter() if output_timeout else None
+ sel.select(timeout)
+
+ # On timeout raise error (we need to measure time on our own, because
+ # select() may exit early even if there is no data to read and the
+ # timeout was not reached.)
+ if output_timeout:
+ wait_end = time.perf_counter()
+ if wait_end - wait_start >= args.timeout:
+ logging.info("Process did not write any output for " +
+ str(args.timeout) + " seconds. Killing it.")
+ logging.info("NOTE: The timeout can be increased with"
+ " 'waydroid -t'.")
+ kill_command(args, process.pid, sudo)
+ continue
+
+ # Read all currently available output
+ pipe_read(args, process, output_to_stdout, output_return,
+ output_buffer)
+
+ # There may still be output after the process quit
+ pipe_read(args, process, output_to_stdout, output_return, output_buffer)
+
+ # Return the return code and output (the output gets built as list of
+ # output chunks and combined at the end, this is faster than extending the
+ # combined string with each new chunk)
+ return (process.returncode, b"".join(output_buffer).decode("utf-8"))
+
+
+def foreground_tui(cmd, working_dir=None):
+ """
+ Run a subprocess in foreground without redirecting any of its output.
+
+ This is the only way text-based user interfaces (ncurses programs like
+ vim, nano or the kernel's menuconfig) work properly.
+ """
+
+ logging.debug("*** output passed to waydroid stdout, not to this log"
+ " ***")
+ process = subprocess.Popen(cmd, cwd=working_dir)
+ return process.wait()
+
+
+def check_return_code(args, code, log_message):
+ """
+ Check the return code of a command.
+
+ :param code: exit code to check
+ :param log_message: simplified and more readable form of the command, e.g.
+ "(native) % echo test" instead of the full command with
+ entering the chroot and more escaping
+ :raises RuntimeError: when the code indicates that the command failed
+ """
+
+ if code:
+ logging.debug("^" * 70)
+ logging.info("NOTE: The failed command's output is above the ^^^ line"
+ " in the log file: " + args.log)
+ raise RuntimeError("Command failed: " + log_message)
+
+
+def sudo_timer_iterate():
+ """
+ Run sudo -v and schedule a new timer to repeat the same.
+ """
+
+ subprocess.Popen(["sudo", "-v"]).wait()
+
+ timer = threading.Timer(interval=60, function=sudo_timer_iterate)
+ timer.daemon = True
+ timer.start()
+
+
+def sudo_timer_start(args):
+ """
+ Start a timer to call sudo -v periodically, so that the password is only
+ needed once.
+ """
+
+ if "sudo_timer_active" in args.cache:
+ return
+ args.cache["sudo_timer_active"] = True
+
+ sudo_timer_iterate()
+
+
+def core(args, log_message, cmd, working_dir=None, output="log",
+ output_return=False, check=None, sudo=False, disable_timeout=False):
+ """
+ Run a command and create a log entry.
+
+ This is a low level function not meant to be used directly. Use one of the
+ following instead: tools.helpers.run.user(), tools.helpers.run.root(),
+ tools.chroot.user(), tools.chroot.root()
+
+ :param log_message: simplified and more readable form of the command, e.g.
+ "(native) % echo test" instead of the full command with
+ entering the chroot and more escaping
+ :param cmd: command as list, e.g. ["echo", "string with spaces"]
+ :param working_dir: path in host system where the command should run
+ :param output: where to write the output (stdout and stderr) of the
+ process. We almost always write to the log file, which can
+ be read with "waydroid log" (output values: "log",
+ "stdout", "interactive", "background"), so it's easy to
+ trace what waydroid does.
+
+ The exceptions are "tui" (text-based user interface), where
+ it does not make sense to write to the log file (think of
+ ncurses UIs, such as "menuconfig") and "pipe" where the
+ output is written to a pipe for manual asynchronous
+ consumption by the caller.
+
+ When the output is not set to "interactive", "tui",
+ "background" or "pipe", we kill the process if it does not
+ output anything for 5 minutes (time can be set with
+ "waydroid --timeout").
+
+ The table below shows all possible values along with
+ their properties. "wait" indicates that we wait for the
+ process to complete.
+
+ output value | timeout | out to log | out to stdout | wait
+ -----------------------------------------------------------
+ "log" | x | x | | x
+ "stdout" | x | x | x | x
+ "interactive" | | x | x | x
+ "tui" | | | x | x
+ "background" | | x | |
+ "pipe" | | | |
+
+ :param output_return: in addition to writing the program's output to the
+ destinations above in real time, write to a buffer
+ and return it as string when the command has
+ completed. This is not possible when output is
+ "background", "pipe" or "tui".
+ :param check: an exception will be raised when the command's return code
+ is not 0. Set this to False to disable the check. This
+ parameter can not be used when the output is "background" or
+ "pipe".
+ :param sudo: use sudo to kill the process when it hits the timeout.
+ :returns: * program's return code (default)
+ * subprocess.Popen instance (output is "background" or "pipe")
+ * the program's entire output (output_return is True)
+ """
+ sanity_checks(output, output_return, check)
+
+ if args.sudo_timer and sudo:
+ sudo_timer_start(args)
+
+ # Log simplified and full command (waydroid -v)
+ logging.debug(log_message)
+ logging.verbose("run: " + str(cmd))
+
+ # Background
+ if output == "background":
+ return background(args, cmd, working_dir)
+
+ # Pipe
+ if output == "pipe":
+ return pipe(args, cmd, working_dir)
+
+ # Foreground
+ output_after_run = ""
+ if output == "tui":
+ # Foreground TUI
+ code = foreground_tui(cmd, working_dir)
+ else:
+ # Foreground pipe (always redirects to the error log file)
+ output_to_stdout = False
+ if not args.details_to_stdout and output in ["stdout", "interactive"]:
+ output_to_stdout = True
+
+ output_timeout = output in ["log", "stdout"] and not disable_timeout
+
+ (code, output_after_run) = foreground_pipe(args, cmd, working_dir,
+ output_to_stdout,
+ output_return,
+ output_timeout,
+ sudo)
+
+ # Check the return code
+ if check is not False:
+ check_return_code(args, code, log_message)
+
+ # Return (code or output string)
+ return output_after_run if output_return else code
--- /dev/null
+import gbinder
+import logging
+from tools import helpers
+from gi.repository import GLib
+
+
+INTERFACE = "lineageos.waydroid.IClipboard"
+SERVICE_NAME = "waydroidclipboard"
+
+TRANSACTION_sendClipboardData = 1
+TRANSACTION_getClipboardData = 2
+
+def add_service(args, sendClipboardData, getClipboardData):
+ helpers.drivers.loadBinderNodes(args)
+ serviceManager = gbinder.ServiceManager("/dev/" + args.BINDER_DRIVER)
+
+ def response_handler(req, code, flags):
+ logging.debug(
+ "{}: Received transaction: {}".format(SERVICE_NAME, code))
+ reader = req.init_reader()
+ local_response = response.new_reply()
+ if code == TRANSACTION_sendClipboardData:
+ arg1 = reader.read_string16()
+ sendClipboardData(arg1)
+ local_response.append_int32(0)
+ if code == TRANSACTION_getClipboardData:
+ ret = getClipboardData()
+ local_response.append_int32(0)
+ local_response.append_string16(ret)
+
+ return local_response, 0
+
+ def binder_presence():
+ if serviceManager.is_present():
+ status = serviceManager.add_service_sync(SERVICE_NAME, response)
+
+ if status:
+ logging.error("Failed to add service " + SERVICE_NAME)
+ args.clipboardLoop.quit()
+
+ response = serviceManager.new_local_object(INTERFACE, response_handler)
+ args.clipboardLoop = GLib.MainLoop()
+ binder_presence()
+ status = serviceManager.add_presence_handler(binder_presence)
+ if status:
+ args.clipboardLoop.run()
+ else:
+ logging.error("Failed to add presence handler: {}".format(status))
--- /dev/null
+import gbinder
+import logging
+from tools import helpers
+from gi.repository import GLib
+
+
+INTERFACE = "lineageos.waydroid.IHardware"
+SERVICE_NAME = "waydroidhardware"
+
+TRANSACTION_enableNFC = 1
+TRANSACTION_enableBluetooth = 2
+TRANSACTION_suspend = 3
+TRANSACTION_reboot = 4
+TRANSACTION_upgrade = 5
+
+def add_service(args, enableNFC, enableBluetooth, suspend, reboot, upgrade):
+ helpers.drivers.loadBinderNodes(args)
+ serviceManager = gbinder.ServiceManager("/dev/" + args.BINDER_DRIVER)
+
+ def response_handler(req, code, flags):
+ logging.debug(
+ "{}: Received transaction: {}".format(SERVICE_NAME, code))
+ reader = req.init_reader()
+ local_response = response.new_reply()
+ if code == TRANSACTION_enableNFC:
+ status, arg1 = reader.read_int32()
+ ret = enableNFC(arg1 != 0)
+ local_response.append_int32(0)
+ local_response.append_int32(ret)
+ if code == TRANSACTION_enableBluetooth:
+ status, arg1 = reader.read_int32()
+ ret = enableBluetooth(arg1 != 0)
+ local_response.append_int32(0)
+ local_response.append_int32(ret)
+ if code == TRANSACTION_suspend:
+ suspend()
+ local_response.append_int32(0)
+ if code == TRANSACTION_reboot:
+ reboot()
+ local_response.append_int32(0)
+ if code == TRANSACTION_upgrade:
+ arg1 = reader.read_string16()
+ status, arg2 = reader.read_int32()
+ arg3 = reader.read_string16()
+ status, arg4 = reader.read_int32()
+ upgrade(arg1, arg2, arg3, arg4)
+ local_response.append_int32(0)
+
+ return local_response, 0
+
+ def binder_presence():
+ if serviceManager.is_present():
+ status = serviceManager.add_service_sync(SERVICE_NAME, response)
+
+ if status:
+ logging.error("Failed to add service " + SERVICE_NAME)
+ args.hardwareLoop.quit()
+
+ response = serviceManager.new_local_object(INTERFACE, response_handler)
+ args.hardwareLoop = GLib.MainLoop()
+ binder_presence()
+ status = serviceManager.add_presence_handler(binder_presence)
+ if status:
+ args.hardwareLoop.run()
+ else:
+ logging.error("Failed to add presence handler: {}".format(status))
--- /dev/null
+import gbinder
+import logging
+import time
+from tools import helpers
+
+
+INTERFACE = "lineageos.waydroid.IPlatform"
+SERVICE_NAME = "waydroidplatform"
+
+TRANSACTION_getprop = 1
+TRANSACTION_setprop = 2
+TRANSACTION_getAppsInfo = 3
+TRANSACTION_getAppInfo = 4
+TRANSACTION_installApp = 5
+TRANSACTION_removeApp = 6
+TRANSACTION_launchApp = 7
+TRANSACTION_getAppName = 8
+TRANSACTION_settingsPutString = 9
+TRANSACTION_settingsGetString = 10
+TRANSACTION_settingsPutInt = 11
+TRANSACTION_getAppName = 12
+
+class IPlatform:
+ def __init__(self, remote):
+ self.client = gbinder.Client(remote, INTERFACE)
+
+ def getprop(self, arg1, arg2):
+ request = self.client.new_request()
+ request.append_string16(arg1)
+ request.append_string16(arg2)
+ reply, status = self.client.transact_sync_reply(
+ TRANSACTION_getprop, request)
+
+ if status:
+ logging.error("Sending reply failed")
+ else:
+ reader = reply.init_reader()
+ status, exception = reader.read_int32()
+ if exception == 0:
+ rep1 = reader.read_string16()
+ return rep1
+ else:
+ logging.error("Failed with code: {}".format(exception))
+
+ return None
+
+ def setprop(self, arg1, arg2):
+ request = self.client.new_request()
+ request.append_string16(arg1)
+ request.append_string16(arg2)
+ reply, status = self.client.transact_sync_reply(
+ TRANSACTION_setprop, request)
+
+ if status:
+ logging.error("Sending reply failed")
+ else:
+ reader = reply.init_reader()
+ status, exception = reader.read_int32()
+ if exception == 0:
+ return
+ else:
+ logging.error("Failed with code: {}".format(exception))
+
+ return
+
+ def getAppsInfo(self):
+ request = self.client.new_request()
+ reply, status = self.client.transact_sync_reply(
+ TRANSACTION_getAppsInfo, request)
+
+ if status:
+ logging.error("Sending reply failed")
+ else:
+ reader = reply.init_reader()
+ apps_list = []
+ status, exception = reader.read_int32()
+ if exception == 0:
+ status, apps = reader.read_int32()
+ for j in range(apps):
+ status, has_value = reader.read_int32()
+ if has_value == 1:
+ appinfo = {
+ "name": reader.read_string16(),
+ "packageName": reader.read_string16(),
+ "action": reader.read_string16(),
+ "launchIntent": reader.read_string16(),
+ "componentPackageName": reader.read_string16(),
+ "componentClassName": reader.read_string16(),
+ "categories": []
+ }
+ status, categories = reader.read_int32()
+ for i in range(categories):
+ appinfo["categories"].append(reader.read_string16())
+ apps_list.append(appinfo)
+ else:
+ logging.error("Failed with code: {}".format(exception))
+
+ return apps_list
+
+ def getAppInfo(self, arg1):
+ request = self.client.new_request()
+ request.append_string16(arg1)
+ reply, status = self.client.transact_sync_reply(
+ TRANSACTION_getAppInfo, request)
+
+ if status:
+ logging.error("Sending reply failed")
+ else:
+ reader = reply.init_reader()
+ status, exception = reader.read_int32()
+ if exception == 0:
+ status, has_value = reader.read_int32()
+ if has_value == 1:
+ appinfo = {
+ "name": reader.read_string16(),
+ "packageName": reader.read_string16(),
+ "action": reader.read_string16(),
+ "launchIntent": reader.read_string16(),
+ "componentPackageName": reader.read_string16(),
+ "componentClassName": reader.read_string16(),
+ "categories": []
+ }
+ status, categories = reader.read_int32()
+ for i in range(categories):
+ appinfo["categories"].append(reader.read_string16())
+
+ return appinfo
+ else:
+ logging.error("Failed with code: {}".format(exception))
+
+ return None
+
+ def installApp(self, arg1):
+ request = self.client.new_request()
+ request.append_string16(arg1)
+ reply, status = self.client.transact_sync_reply(
+ TRANSACTION_installApp, request)
+
+ if status:
+ logging.error("Sending reply failed")
+ else:
+ reader = reply.init_reader()
+ status, exception = reader.read_int32()
+ if exception == 0:
+ status, ret = reader.read_int32()
+ return ret
+ else:
+ logging.error("Failed with code: {}".format(exception))
+
+ return None
+
+ def removeApp(self, arg1):
+ request = self.client.new_request()
+ request.append_string16(arg1)
+ reply, status = self.client.transact_sync_reply(
+ TRANSACTION_removeApp, request)
+
+ if status:
+ logging.error("Sending reply failed")
+ else:
+ reader = reply.init_reader()
+ status, exception = reader.read_int32()
+ if exception == 0:
+ status, ret = reader.read_int32()
+ return ret
+ else:
+ logging.error("Failed with code: {}".format(exception))
+
+ return None
+
+ def launchApp(self, arg1):
+ request = self.client.new_request()
+ request.append_string16(arg1)
+ reply, status = self.client.transact_sync_reply(
+ TRANSACTION_launchApp, request)
+
+ if status:
+ logging.error("Sending reply failed")
+ else:
+ reader = reply.init_reader()
+ status, exception = reader.read_int32()
+ if exception != 0:
+ logging.error("Failed with code: {}".format(exception))
+
+ def getAppName(self, arg1):
+ request = self.client.new_request()
+ request.append_string16(arg1)
+ reply, status = self.client.transact_sync_reply(
+ TRANSACTION_getAppName, request)
+
+ if status:
+ logging.error("Sending reply failed")
+ else:
+ reader = reply.init_reader()
+ status, exception = reader.read_int32()
+ if exception == 0:
+ rep1 = reader.read_string16()
+ return rep1
+ else:
+ logging.error("Failed with code: {}".format(exception))
+
+ return None
+
+ def settingsPutString(self, arg1, arg2, arg3):
+ request = self.client.new_request()
+ request.append_int32(arg1)
+ request.append_string16(arg2)
+ request.append_string16(arg3)
+ reply, status = self.client.transact_sync_reply(
+ TRANSACTION_settingsPutString, request)
+
+ if status:
+ logging.error("Sending reply failed")
+ else:
+ reader = reply.init_reader()
+ status, exception = reader.read_int32()
+ if exception != 0:
+ logging.error("Failed with code: {}".format(exception))
+
+ def settingsGetString(self, arg1, arg2):
+ request = self.client.new_request()
+ request.append_int32(arg1)
+ request.append_string16(arg2)
+ reply, status = self.client.transact_sync_reply(
+ TRANSACTION_settingsGetString, request)
+
+ if status:
+ logging.error("Sending reply failed")
+ else:
+ reader = reply.init_reader()
+ status, exception = reader.read_int32()
+ if exception == 0:
+ rep1 = reader.read_string16()
+ return rep1
+ else:
+ logging.error("Failed with code: {}".format(exception))
+
+ return None
+
+ def settingsPutInt(self, arg1, arg2, arg3):
+ request = self.client.new_request()
+ request.append_int32(arg1)
+ request.append_string16(arg2)
+ request.append_int32(arg3)
+ reply, status = self.client.transact_sync_reply(
+ TRANSACTION_settingsPutInt, request)
+
+ if status:
+ logging.error("Sending reply failed")
+ else:
+ reader = reply.init_reader()
+ status, exception = reader.read_int32()
+ if exception != 0:
+ logging.error("Failed with code: {}".format(exception))
+
+ def settingsGetInt(self, arg1, arg2):
+ request = self.client.new_request()
+ request.append_int32(arg1)
+ request.append_string16(arg2)
+ reply, status = self.client.transact_sync_reply(
+ TRANSACTION_settingsGetString, request)
+
+ if status:
+ logging.error("Sending reply failed")
+ else:
+ reader = reply.init_reader()
+ status, exception = reader.read_int32()
+ if exception == 0:
+ status, rep1 = reader.read_int32()
+ return rep1
+ else:
+ logging.error("Failed with code: {}".format(exception))
+
+ return None
+
+def get_service(args):
+ helpers.drivers.loadBinderNodes(args)
+ serviceManager = gbinder.ServiceManager("/dev/" + args.BINDER_DRIVER)
+ tries = 1000
+
+ remote, status = serviceManager.get_service_sync(SERVICE_NAME)
+ while(not remote):
+ if tries > 0:
+ logging.warning(
+ "Failed to get service {}, trying again...".format(SERVICE_NAME))
+ time.sleep(1)
+ remote, status = serviceManager.get_service_sync(SERVICE_NAME)
+ tries = tries - 1
+ else:
+ return None
+
+ return IPlatform(remote)
--- /dev/null
+import gbinder
+import logging
+import time
+from tools import helpers
+
+
+INTERFACE = "com.android.internal.statusbar.IStatusBarService"
+SERVICE_NAME = "statusbar"
+
+TRANSACTION_expand = 1
+TRANSACTION_collapse = 2
+
+class IStatusBarService:
+ def __init__(self, remote):
+ self.client = gbinder.Client(remote, INTERFACE)
+
+ def expand(self):
+ request = self.client.new_request()
+ reply, status = self.client.transact_sync_reply(
+ TRANSACTION_expand, request)
+
+ if status:
+ logging.error("Sending reply failed")
+ else:
+ reader = reply.init_reader()
+ status, exception = reader.read_int32()
+ if exception != 0:
+ logging.error("Failed with code: {}".format(exception))
+
+ def collapse(self):
+ request = self.client.new_request()
+ reply, status = self.client.transact_sync_reply(
+ TRANSACTION_collapse, request)
+
+ if status:
+ logging.error("Sending reply failed")
+ else:
+ reader = reply.init_reader()
+ status, exception = reader.read_int32()
+ if exception != 0:
+ logging.error("Failed with code: {}".format(exception))
+
+def get_service(args):
+ helpers.drivers.loadBinderNodes(args)
+ serviceManager = gbinder.ServiceManager("/dev/" + args.BINDER_DRIVER)
+ tries = 1000
+
+ remote, status = serviceManager.get_service_sync(SERVICE_NAME)
+ while(not remote):
+ if tries > 0:
+ logging.warning(
+ "Failed to get service {}, trying again...".format(SERVICE_NAME))
+ time.sleep(1)
+ remote, status = serviceManager.get_service_sync(SERVICE_NAME)
+ tries = tries - 1
+ else:
+ return None
+
+ return IStatusBarService(remote)
--- /dev/null
+import gbinder
+import logging
+from tools import helpers
+from gi.repository import GLib
+
+
+INTERFACE = "lineageos.waydroid.IUserMonitor"
+SERVICE_NAME = "waydroidusermonitor"
+
+TRANSACTION_userUnlocked = 1
+TRANSACTION_packageStateChanged = 2
+
+def add_service(args, userUnlocked, packageStateChanged):
+ helpers.drivers.loadBinderNodes(args)
+ serviceManager = gbinder.ServiceManager("/dev/" + args.BINDER_DRIVER)
+
+ def response_handler(req, code, flags):
+ logging.debug(
+ "{}: Received transaction: {}".format(SERVICE_NAME, code))
+ reader = req.init_reader()
+ local_response = response.new_reply()
+ if code == TRANSACTION_userUnlocked:
+ status, arg1 = reader.read_int32()
+ userUnlocked(arg1)
+ local_response.append_int32(0)
+ if code == TRANSACTION_packageStateChanged:
+ status, arg1 = reader.read_int32()
+ arg2 = reader.read_string16()
+ status, arg3 = reader.read_int32()
+ packageStateChanged(arg1, arg2, arg3)
+ local_response.append_int32(0)
+
+ return local_response, 0
+
+ def binder_presence():
+ if serviceManager.is_present():
+ status = serviceManager.add_service_sync(SERVICE_NAME, response)
+
+ if status:
+ logging.error("Failed to add service " + SERVICE_NAME)
+ args.userMonitorLoop.quit()
+
+ response = serviceManager.new_local_object(INTERFACE, response_handler)
+ args.userMonitorLoop = GLib.MainLoop()
+ binder_presence()
+ status = serviceManager.add_presence_handler(binder_presence)
+ if status:
+ args.userMonitorLoop.run()
+ else:
+ logging.error("Failed to add presence handler: {}".format(status))
+
--- /dev/null
+# Copyright 2021 Erfan Abdi
+# SPDX-License-Identifier: GPL-3.0-or-later
+from tools.services.user_manager import start, stop
+from tools.services.clipboard_manager import start, stop
+from tools.services.hardware_manager import start, stop
--- /dev/null
+# Copyright 2021 Erfan Abdi
+# SPDX-License-Identifier: GPL-3.0-or-later
+import logging
+import threading
+from tools.interfaces import IClipboard
+
+try:
+ import pyclip
+ canClip = True
+except Exception as e:
+ logging.debug(str(e))
+ canClip = False
+
+def start(args):
+ def sendClipboardData(value):
+ try:
+ pyclip.copy(value)
+ except Exception as e:
+ logging.debug(str(e))
+
+ def getClipboardData():
+ try:
+ return pyclip.paste()
+ except Exception as e:
+ logging.debug(str(e))
+
+ def service_thread():
+ IClipboard.add_service(args, sendClipboardData, getClipboardData)
+
+ if canClip:
+ args.clipboard_manager = threading.Thread(target=service_thread)
+ args.clipboard_manager.start()
+ else:
+ logging.warning("Failed to start Clipboard manager service, check logs")
+
+def stop(args):
+ try:
+ if args.clipboardLoop:
+ args.clipboardLoop.quit()
+ except AttributeError:
+ logging.debug("Clipboard service is not even started")
--- /dev/null
+# Copyright 2021 Erfan Abdi
+# SPDX-License-Identifier: GPL-3.0-or-later
+import logging
+import threading
+import tools.actions.container_manager
+from tools import helpers
+from tools.interfaces import IHardware
+
+
+def start(args):
+ def enableNFC(enable):
+ logging.debug("Function enableNFC not implemented")
+
+ def enableBluetooth(enable):
+ logging.debug("Function enableBluetooth not implemented")
+
+ def suspend():
+ tools.actions.container_manager.freeze(args)
+
+ def reboot():
+ helpers.lxc.stop(args)
+ helpers.lxc.start(args)
+
+ def upgrade(system_zip, system_time, vendor_zip, vendor_time):
+ helpers.lxc.stop(args)
+ helpers.images.umount_rootfs(args)
+ helpers.images.replace(args, system_zip, system_time,
+ vendor_zip, vendor_time)
+ helpers.images.mount_rootfs(args, args.images_path)
+ helpers.lxc.start(args)
+
+ def service_thread():
+ IHardware.add_service(
+ args, enableNFC, enableBluetooth, suspend, reboot, upgrade)
+
+ args.hardware_manager = threading.Thread(target=service_thread)
+ args.hardware_manager.start()
+
+def stop(args):
+ try:
+ if args.hardwareLoop:
+ args.hardwareLoop.quit()
+ except AttributeError:
+ logging.debug("Hardware service is not even started")
--- /dev/null
+# Copyright 2021 Erfan Abdi
+# SPDX-License-Identifier: GPL-3.0-or-later
+import logging
+import os
+import threading
+import tools.config
+from tools.interfaces import IUserMonitor
+from tools.interfaces import IPlatform
+
+
+def start(args):
+ def makeDesktopFile(appInfo):
+ showApp = False
+ for cat in appInfo["categories"]:
+ if cat.strip() == "android.intent.category.LAUNCHER":
+ showApp = True
+ if not showApp:
+ return -1
+
+ packageName = appInfo["packageName"]
+
+ desktop_file_path = args.host_user + \
+ "/.local/share/applications/" + packageName + ".desktop"
+ if not os.path.exists(desktop_file_path):
+ lines = ["[Desktop Entry]", "Type=Application"]
+ lines.append("Name=" + appInfo["name"])
+ lines.append("Exec=waydroid app launch " + packageName)
+ lines.append("Icon=" + args.waydroid_data + "/icons/" + packageName + ".png")
+ desktop_file = open(desktop_file_path, "w")
+ for line in lines:
+ desktop_file.write(line + "\n")
+ desktop_file.close()
+ os.chmod(desktop_file_path, 0o755)
+ return 0
+
+ def makeWaydroidDesktopFile():
+ desktop_file_path = args.host_user + \
+ "/.local/share/applications/Waydroid.desktop"
+ if not os.path.exists(desktop_file_path):
+ lines = ["[Desktop Entry]", "Type=Application"]
+ lines.append("Name=Waydroid")
+ lines.append("Exec=waydroid show-full-ui")
+ lines.append("Icon=" + tools.config.tools_src + "/data/AppIcon.png")
+ desktop_file = open(desktop_file_path, "w")
+ for line in lines:
+ desktop_file.write(line + "\n")
+ desktop_file.close()
+ os.chmod(desktop_file_path, 0o755)
+
+ def userUnlocked(uid):
+ logging.info("Android with user {} is ready".format(uid))
+ session_cfg = tools.config.load_session()
+ args.waydroid_data = session_cfg["session"]["waydroid_data"]
+ args.host_user = session_cfg["session"]["host_user"]
+
+ platformService = IPlatform.get_service(args)
+ if platformService:
+ appsList = platformService.getAppsInfo()
+ for app in appsList:
+ makeDesktopFile(app)
+ multiwin = platformService.getprop("persist.waydroid.multi_windows", "false")
+ if multiwin == "false":
+ makeWaydroidDesktopFile()
+ else:
+ desktop_file_path = args.host_user + \
+ "/.local/share/applications/Waydroid.desktop"
+ if os.path.isfile(desktop_file_path):
+ os.remove(desktop_file_path)
+
+ def packageStateChanged(mode, packageName, uid):
+ platformService = IPlatform.get_service(args)
+ if platformService:
+ appInfo = platformService.getAppInfo(packageName)
+ desktop_file_path = args.host_user + \
+ "/.local/share/applications/" + packageName + ".desktop"
+ if mode == 0:
+ # Package added
+ makeDesktopFile(appInfo)
+ elif mode == 1:
+ if os.path.isfile(desktop_file_path):
+ os.remove(desktop_file_path)
+ else:
+ if os.path.isfile(desktop_file_path):
+ if makeDesktopFile(appInfo) == -1:
+ os.remove(desktop_file_path)
+
+ def service_thread():
+ IUserMonitor.add_service(args, userUnlocked, packageStateChanged)
+
+ args.user_manager = threading.Thread(target=service_thread)
+ args.user_manager.start()
+
+def stop(args):
+ try:
+ if args.userMonitorLoop:
+ args.userMonitorLoop.quit()
+ except AttributeError:
+ logging.debug("UserMonitor service is not even started")
--- /dev/null
+#!/usr/bin/env python3
+# Copyright 2021 Oliver Smith
+# SPDX-License-Identifier: GPL-3.0-or-later
+# PYTHON_ARGCOMPLETE_OK
+import sys
+import tools
+
+if __name__ == "__main__":
+ sys.exit(tools.main())