From: Erfan Abdi Date: Sat, 17 Jul 2021 06:53:14 +0000 (+0430) Subject: Waydroid: Initial commit X-Git-Tag: 1.1.0~45 X-Git-Url: https://glassweightruler.freedombox.rocks/gitweb/waydroid.git/commitdiff_plain/1f0393876d891d0e32a995d0bf7259cc6505afdc Waydroid: Initial commit --- 1f0393876d891d0e32a995d0bf7259cc6505afdc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a68a05 --- /dev/null +++ b/.gitignore @@ -0,0 +1,132 @@ +# 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/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + 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. + + + Copyright (C) + + 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 . + +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: + + Copyright (C) + 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 +. + + 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 +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9fe872d --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# 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). diff --git a/data/AppIcon.png b/data/AppIcon.png new file mode 100644 index 0000000..0bd597d Binary files /dev/null and b/data/AppIcon.png differ diff --git a/data/configs/config_1 b/data/configs/config_1 new file mode 100644 index 0000000..02d82a7 --- /dev/null +++ b/data/configs/config_1 @@ -0,0 +1,25 @@ +# 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 diff --git a/data/configs/config_2 b/data/configs/config_2 new file mode 100644 index 0000000..dd2af68 --- /dev/null +++ b/data/configs/config_2 @@ -0,0 +1,25 @@ +# 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 diff --git a/data/scripts/waydroid-net.sh b/data/scripts/waydroid-net.sh new file mode 100755 index 0000000..14da312 --- /dev/null +++ b/data/scripts/waydroid-net.sh @@ -0,0 +1,269 @@ +#!/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 $? diff --git a/tools/__init__.py b/tools/__init__.py new file mode 100644 index 0000000..555ea00 --- /dev/null +++ b/tools/__init__.py @@ -0,0 +1,135 @@ +# 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: ") + 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()) diff --git a/tools/actions/__init__.py b/tools/actions/__init__.py new file mode 100644 index 0000000..558ed83 --- /dev/null +++ b/tools/actions/__init__.py @@ -0,0 +1,8 @@ +# 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 diff --git a/tools/actions/app_manager.py b/tools/actions/app_manager.py new file mode 100644 index 0000000..7bb19a2 --- /dev/null +++ b/tools/actions/app_manager.py @@ -0,0 +1,113 @@ +# 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() diff --git a/tools/actions/container_manager.py b/tools/actions/container_manager.py new file mode 100644 index 0000000..994dcea --- /dev/null +++ b/tools/actions/container_manager.py @@ -0,0 +1,227 @@ +# 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)) diff --git a/tools/actions/initializer.py b/tools/actions/initializer.py new file mode 100644 index 0000000..c5783a4 --- /dev/null +++ b/tools/actions/initializer.py @@ -0,0 +1,88 @@ +# 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") diff --git a/tools/actions/session_manager.py b/tools/actions/session_manager.py new file mode 100644 index 0000000..7d89757 --- /dev/null +++ b/tools/actions/session_manager.py @@ -0,0 +1,51 @@ +# 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") diff --git a/tools/actions/status.py b/tools/actions/status.py new file mode 100644 index 0000000..00261ce --- /dev/null +++ b/tools/actions/status.py @@ -0,0 +1,19 @@ +# 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"]) diff --git a/tools/actions/upgrader.py b/tools/actions/upgrader.py new file mode 100644 index 0000000..cb1aaa5 --- /dev/null +++ b/tools/actions/upgrader.py @@ -0,0 +1,35 @@ +# 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) diff --git a/tools/config/__init__.py b/tools/config/__init__.py new file mode 100644 index 0000000..4c16de2 --- /dev/null +++ b/tools/config/__init__.py @@ -0,0 +1,79 @@ +# 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"] diff --git a/tools/config/load.py b/tools/config/load.py new file mode 100644 index 0000000..6826c21 --- /dev/null +++ b/tools/config/load.py @@ -0,0 +1,69 @@ +# 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 diff --git a/tools/config/save.py b/tools/config/save.py new file mode 100644 index 0000000..67e25ea --- /dev/null +++ b/tools/config/save.py @@ -0,0 +1,19 @@ +# 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) diff --git a/tools/helpers/__init__.py b/tools/helpers/__init__.py new file mode 100644 index 0000000..21357cf --- /dev/null +++ b/tools/helpers/__init__.py @@ -0,0 +1,10 @@ +# 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 diff --git a/tools/helpers/arch.py b/tools/helpers/arch.py new file mode 100644 index 0000000..3c0bb76 --- /dev/null +++ b/tools/helpers/arch.py @@ -0,0 +1,17 @@ +# 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") diff --git a/tools/helpers/arguments.py b/tools/helpers/arguments.py new file mode 100644 index 0000000..4c95287 --- /dev/null +++ b/tools/helpers/arguments.py @@ -0,0 +1,153 @@ +# 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 diff --git a/tools/helpers/drivers.py b/tools/helpers/drivers.py new file mode 100644 index 0000000..e5a0335 --- /dev/null +++ b/tools/helpers/drivers.py @@ -0,0 +1,135 @@ +# 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"] diff --git a/tools/helpers/http.py b/tools/helpers/http.py new file mode 100644 index 0000000..d05522b --- /dev/null +++ b/tools/helpers/http.py @@ -0,0 +1,89 @@ +# 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)) diff --git a/tools/helpers/images.py b/tools/helpers/images.py new file mode 100644 index 0000000..aaeadff --- /dev/null +++ b/tools/helpers/images.py @@ -0,0 +1,109 @@ +# 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"]) diff --git a/tools/helpers/logging.py b/tools/helpers/logging.py new file mode 100644 index 0000000..094bf82 --- /dev/null +++ b/tools/helpers/logging.py @@ -0,0 +1,99 @@ +# 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 diff --git a/tools/helpers/lxc.py b/tools/helpers/lxc.py new file mode 100644 index 0000000..c46f2a7 --- /dev/null +++ b/tools/helpers/lxc.py @@ -0,0 +1,269 @@ +# 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) diff --git a/tools/helpers/mount.py b/tools/helpers/mount.py new file mode 100644 index 0000000..4d77206 --- /dev/null +++ b/tools/helpers/mount.py @@ -0,0 +1,137 @@ +# 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) diff --git a/tools/helpers/props.py b/tools/helpers/props.py new file mode 100644 index 0000000..bcc7c34 --- /dev/null +++ b/tools/helpers/props.py @@ -0,0 +1,50 @@ +# 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") diff --git a/tools/helpers/run.py b/tools/helpers/run.py new file mode 100644 index 0000000..d3c7738 --- /dev/null +++ b/tools/helpers/run.py @@ -0,0 +1,78 @@ +# 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) diff --git a/tools/helpers/run_core.py b/tools/helpers/run_core.py new file mode 100644 index 0000000..dcc4c53 --- /dev/null +++ b/tools/helpers/run_core.py @@ -0,0 +1,346 @@ +# 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 diff --git a/tools/interfaces/IClipboard.py b/tools/interfaces/IClipboard.py new file mode 100644 index 0000000..7d09d92 --- /dev/null +++ b/tools/interfaces/IClipboard.py @@ -0,0 +1,48 @@ +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)) diff --git a/tools/interfaces/IHardware.py b/tools/interfaces/IHardware.py new file mode 100644 index 0000000..347a89c --- /dev/null +++ b/tools/interfaces/IHardware.py @@ -0,0 +1,66 @@ +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)) diff --git a/tools/interfaces/IPlatform.py b/tools/interfaces/IPlatform.py new file mode 100644 index 0000000..9887d13 --- /dev/null +++ b/tools/interfaces/IPlatform.py @@ -0,0 +1,292 @@ +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) diff --git a/tools/interfaces/IStatusBarService.py b/tools/interfaces/IStatusBarService.py new file mode 100644 index 0000000..0fe30a7 --- /dev/null +++ b/tools/interfaces/IStatusBarService.py @@ -0,0 +1,59 @@ +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) diff --git a/tools/interfaces/IUserMonitor.py b/tools/interfaces/IUserMonitor.py new file mode 100644 index 0000000..6652778 --- /dev/null +++ b/tools/interfaces/IUserMonitor.py @@ -0,0 +1,51 @@ +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)) + diff --git a/tools/services/__init__.py b/tools/services/__init__.py new file mode 100644 index 0000000..8510c33 --- /dev/null +++ b/tools/services/__init__.py @@ -0,0 +1,5 @@ +# 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 diff --git a/tools/services/clipboard_manager.py b/tools/services/clipboard_manager.py new file mode 100644 index 0000000..72fe62a --- /dev/null +++ b/tools/services/clipboard_manager.py @@ -0,0 +1,41 @@ +# 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") diff --git a/tools/services/hardware_manager.py b/tools/services/hardware_manager.py new file mode 100644 index 0000000..529348d --- /dev/null +++ b/tools/services/hardware_manager.py @@ -0,0 +1,44 @@ +# 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") diff --git a/tools/services/user_manager.py b/tools/services/user_manager.py new file mode 100644 index 0000000..41fea83 --- /dev/null +++ b/tools/services/user_manager.py @@ -0,0 +1,98 @@ +# 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") diff --git a/waydroid.py b/waydroid.py new file mode 100755 index 0000000..a79b015 --- /dev/null +++ b/waydroid.py @@ -0,0 +1,9 @@ +#!/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())