diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4a18eec..998ba52 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -77,9 +77,11 @@ jobs: - name: Test codexctl shell: bash run: make test + - name: Make script executable + run: chmod +x ./scripts/github-make-executable.sh - name: Build codexctl shell: bash - run: ./github-make-executable.sh + run: ./scripts/github-make-executable.sh env: nuitka_cache: ${{ github.workspace }}/.nuitka - name: Upload Compilation Report @@ -89,9 +91,6 @@ jobs: name: ${{ matrix.os }}-compilation-report path: compilation-report.xml if-no-files-found: warn - - name: Test Built version - shell: bash - run: make test-executable - name: Move .ccache shell: bash run: | @@ -103,7 +102,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: ${{ matrix.os }} - path: dist/codexctl.bin + path: dist/codexctl if-no-files-found: error - name: Upload executable if: matrix.os == 'windows-latest' @@ -145,7 +144,8 @@ jobs: libfuse-dev cd /src source /opt/lib/nuitka/bin/activate - ./github-make-executable.sh + chmod +x ./scripts/github-make-executable.sh + ./scripts/github-make-executable.sh - name: Upload Compilation Report uses: actions/upload-artifact@v4 if: runner.debug == '1' @@ -157,7 +157,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: remarkable - path: dist/codexctl.bin + path: dist/codexctl if-no-files-found: error test_device: name: Test for reMarkable ${{ matrix.fw_version }} @@ -179,8 +179,8 @@ jobs: path: artifacts fw_version: ${{ matrix.fw_version }} run: | - chmod +x ./codexctl.bin - ./codexctl.bin download --out /tmp toltec + chmod +x ./codexctl + ./codexctl download --hardware rm2 --out /tmp toltec release: name: Release needs: [remote,device,test_device] @@ -210,4 +210,4 @@ jobs: tag: ${{ env.TAG }} commit: ${{ github.sha }} generateReleaseNotes: true - makeLatest: true + makeLatest: true \ No newline at end of file diff --git a/.gitignore b/.gitignore index 86e4712..068324d 100644 --- a/.gitignore +++ b/.gitignore @@ -133,4 +133,4 @@ updates/ rm-docker/ nuitka-crash-report.xml .nuitka/ -compilation-report.xml +compilation-report.xml \ No newline at end of file diff --git a/LICENSE b/LICENSE index f288702..3877ae0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,674 +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 -. + 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/Makefile b/Makefile index b3772dd..36193af 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ else ifeq ($(VENV_BIN_ACTIVATE),) VENV_BIN_ACTIVATE := .venv/bin/activate endif - CODEXCTL_BIN := codexctl.bin + CODEXCTL_BIN := codexctl endif OBJ := $(wildcard codexctl/**) @@ -33,20 +33,20 @@ $(VENV_BIN_ACTIVATE): requirements.remote.txt requirements.txt @set -e; \ . $(VENV_BIN_ACTIVATE); \ python -m pip install \ - --extra-index-url=https://wheels.eeems.codes/ \ + --extra-index-url=https://wheels.eeems.codes/ \ -r requirements.remote.txt .venv/${FW_VERSION}_reMarkable2-${FW_DATA}.signed: $(VENV_BIN_ACTIVATE) $(OBJ) @echo "[info] Downloading remarkable update file" @set -e; \ . $(VENV_BIN_ACTIVATE); \ - python -m codexctl download --out .venv ${FW_VERSION} + python -m codexctl download --hardware rm2 --out .venv ${FW_VERSION} test: $(VENV_BIN_ACTIVATE) .venv/${FW_VERSION}_reMarkable2-${FW_DATA}.signed @echo "[info] Running test" @set -e; \ . $(VENV_BIN_ACTIVATE); \ - python test.py; \ + python tests/test.py; \ if [[ "linux" == "$$(python -c 'import sys;print(sys.platform)')" ]]; then \ if [ -d .venv/mnt ] && mountpoint -q .venv/mnt; then \ umount -ql .venv/mnt; \ @@ -97,7 +97,7 @@ executable: $(VENV_BIN_ACTIVATE) @set -e; \ . $(VENV_BIN_ACTIVATE); \ python -m pip install \ - --extra-index-url=https://wheels.eeems.codes/ \ + --extra-index-url=https://wheels.eeems.codes/ \ nuitka==2.4.8 @echo "[info] Building codexctl" @set -e; \ @@ -108,7 +108,9 @@ executable: $(VENV_BIN_ACTIVATE) --remove-output \ --output-dir=dist \ --report=compilation-report.xml \ - codexctl.py + --output-filename=codexctl \ + main.py + if [ -d dist/codexctl.build ]; then \ rm -r dist/codexctl.build; \ fi @@ -122,4 +124,4 @@ all: executable executable \ clean \ test \ - test-executable + test-executable \ No newline at end of file diff --git a/README.md b/README.md index 00f0912..b8857c8 100644 --- a/README.md +++ b/README.md @@ -4,88 +4,74 @@ # Codexctl A utility program that helps to manage the remarkable device version utilizing [ddvks update server](https://github.com/ddvk/remarkable-update) +## Caveat for downgrading to a version below 3.11 -## PLEASE READ BEFORE USING -If your remarkable device is at version >= 3.11, codexctl will not be able to automatically install updates on it due to a major overhaul in the update engine. You can still use the other functions like downloading the image files and then manually extracting it, using `dd` to write to the other partition and then using the `restore` command. More information can be found in https://github.com/Jayy001/codexctl/issues/71#issuecomment-2099115757. ---- +If your reMarkable device is above 3.11 and you want to downgrade to a version below 3.11, codexctl cannot do this currently. Please refer to #71 for manual instructions. -## Installation & Use +## Installation -You can find pre-compiled binaries on the [releases](https://github.com/Jayy001/codexctl/releases/) page. This includes a build for the reMarkable itself, as well as well as builds for linux, macOS, and Windows. It currently only has support for **command line interfaces** but a graphical interface is soon to come. +You can find pre-compiled binaries on the [releases](https://github.com/Jayy001/codexctl/releases/) page. This includes a build for the reMarkable itself, as well as well as builds for linux, macOS, and Windows. Alternatively, you can install directly from pypi with `pip install codexctl`. Codexctl currently only has support for a **command line interfaces** but a graphical interface is soon to come. -### Usage +Finally, if you want to build it yourself, you can run `make executable` which requires python 3.11 or newer, python-venv and pip. Linux also requires libfuse-dev. -The script is designed to have as little interactivity as possible, meaning arguments are directly taken from the command to run the script. +## General useage ``` ❯ codexctl --help -usage: Codexctl app [-h] [--debug] [--rm1] [--auth AUTH] [--verbose] {install,download,backup,extract,mount,status,restore,list} ... +usage: Codexctl [-h] [--verbose] [--address ADDRESS] [--password PASSWORD] + {install,download,backup,cat,ls,extract,mount,upload,status,restore,list} ... positional arguments: - {install,download,backup,extract,mount,status,restore,list} + {install,download,backup,cat,ls,extract,mount,upload,status,restore,list} install Install the specified version (will download if not available on the device) download Download the specified version firmware file backup Download remote files to local directory - extract Extract the specified version update file + cat Cat the contents of a file inside a firmware image + ls List files inside a firmware image + extract Extract the specified version firmware file mount Mount the specified version firmware filesystem + upload Upload folder/files to device (pdf only) status Get the current version of the device and other information restore Restores to previous version installed on device - list List all versions available for use + list List all available versions options: -h, --help show this help message and exit - --debug Print debug info - --rm1 Use rm1 - --auth AUTH Specify password or SSH key for SSH - --verbose Enable verbose logging + --verbose, -v Enable verbose logging + --address ADDRESS, -a ADDRESS + Specify the address of the device + --password PASSWORD, -p PASSWORD + Specify password or path to SSH key for remote access ``` -### Examples +## Examples +- Installing the latest for device (will automatically figure out the version) ``` -codexctl install latest # Downloads and installs latest version -codexctl download toltec # Downloads latest version that has full support for toltec -codexctl download 3.0.4.1305 --rm1 # Downloads 3.0.4.1305 firmware file for remarkable 1 -codexctl status # Prints current & previous version (can only be used when running on device itself) -codexctl list # Lists all available versions -codexctl restore # Restores previous version -codexctl --verbose # Enables logging -codexctl --backup # Exports all files to local directory -codexctl --backup -l root -r FM --no-recursion --no-overwrite # Exports all files from FM directory to root folder on localhost -codexctl extract 3.8.0.1944_reMarkable2-7eGpAv7sYB.signed # Extracts contents to filesystem named "extracted" -codexctl mount extracted /opt/remarkable # Mounts extracted filesystem to /opt/remarkable -codexctl ls 3.8.0.1944_reMarkable2-7eGpAv7sYB.signed / # Lists the root directory of the update image -codexctl cat 3.8.0.1944_reMarkable2-7eGpAv7sYB.signed /etc/version # Outputs the contents of /etc/version from the update image +codexctl install latest ``` - -## Running from source - -Codexctl can be run from source on both the reMarkable, as well as on a remote device. - -### Running on reMarkable - +- Downloading rmpp version 3.15.4.2 to a folder named `out` and then installing it ``` -git clone https://github.com/Jayy001/codexctl.git -cd codexctl -pip install -r requirements.txt -python codexctl.py --help +codexctl download 3.0.4.1305 -hw rmpp -o out +codexctl install ./out/remarkable-ct-prototype-image-3.15.4.2-ferrari-public.swu ``` - -### Running on a remote device - -This requires python 3.11 or newer. - +- Backing up all documents to the cwd +``` +codexctl backup ``` -git clone https://github.com/Jayy001/codexctl.git -cd codexctl -pip install wheel -pip install -r requirements.remote.txt -python codexctl.py --help +- Backing up only documents in a folder named "FM" to cwd, without overwriting any current files +``` +codexctl backup -l root -r FM --no-recursion --no-overwrite +``` +- Getting the version of the device and then switching to previous version (restore only for rm1/rm2) +``` +codexctl status +codexctl restore +``` +- Download 3.8.0.1944 for rm2, then cat the /etc/version file from it +``` +codexctl download 3.8.0.1944 --hardware rm2 +codexctl cat 3.8.0.1944_reMarkable2-7eGpAv7sYB.signed /etc/version ``` -## Building executables from source -This requires python 3.11 or newer, python-venv, pip. Linux also requires libfuse-dev. -``` -make executable -``` diff --git a/codexctl/__init__.py b/codexctl/__init__.py index 9e65435..3727555 100644 --- a/codexctl/__init__.py +++ b/codexctl/__init__.py @@ -1,733 +1,364 @@ +### Importing required general modules + import argparse -import errno -import subprocess -import re -import threading import os.path -import socket import sys +import logging +import importlib.util import tempfile import shutil -import logging -import warnings - -from pathlib import Path -from loguru import logger - -from .sync import RmWebInterfaceAPI -from .updates import UpdateManager -from .server import startUpdate, scanUpdates +import json +import re -REMOTE_DEPS_MET = True +from os import listdir try: - import paramiko + from loguru import logger except ImportError: - REMOTE_DEPS_MET = False + logger = logging.getLogger(__name__) -RESTORE_CODE = """ -# switches the active root partition - -/sbin/fw_setenv "upgrade_available" "1" -/sbin/fw_setenv "bootcount" "0" - -OLDPART=$(/sbin/fw_printenv -n active_partition) -if [ $OLDPART == "2" ]; then - NEWPART="3" -else - NEWPART="2" -fi -echo "new: ${NEWPART}" -echo "fallback: ${OLDPART}" - -/sbin/fw_setenv "fallback_partition" "${OLDPART}" -/sbin/fw_setenv "active_partition" "${NEWPART}" -""" - - -def get_host_ip(): - possible_ips = [] - try: - if "psutil" not in sys.modules: - import psutil - - for interface, snics in psutil.net_if_addrs().items(): - logger.debug(f"New interface found: {interface}") - for snic in snics: - if snic.family == socket.AF_INET: - if snic.address.startswith("10.11.99"): - return [snic.address] - logger.debug(f"Adding new address: {snic.address}") - possible_ips.append(snic.address) - except Exception as error: - logger.error(f"Error getting interfaces: {error}") - - return possible_ips - - -def version_lookup(version, device): - logger.debug(f"Looking up {version} for ReMarkable {device}") - if version == "latest": - return updateman.get_latest_version(device=device) - - if version == "toltec": - return updateman.get_toltec_version(device=device) - - if device == 2: - version_dict = updateman.id_lookups_rm2 - elif device == 1: - version_dict = updateman.id_lookups_rm1 - else: - raise SystemError("Error: Invalid device given!") - - if version in version_dict: - return version - - raise SystemExit( - "Error: Invalid version! Examples: latest, toltec, 3.2.3.1595, 2.15.0.1067" +if importlib.util.find_spec("requests") is None: + raise ImportError( + "Requests is required for accessing remote files. Please install it." ) +from .updates import UpdateManager -def connect_to_rm(args, ip="10.11.99.1"): - client = paramiko.client.SSHClient() - client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - - if args.auth: - logger.debug("Using authentication argument") - try: - if os.path.isfile(args.auth): - logger.debug(f"Interpreting as key file location: {args.auth}") - client.connect(ip, username="root", key_filename=args.auth) - else: - logger.debug(f"Interpreting as password: [REDACTED]") - client.connect(ip, username="root", password=args.auth) - - print("Connected to device") - return client - - except paramiko.ssh_exception.AuthenticationException: - print("Incorrect password or ssh path given in arguments!") - - if "n" in input("Would you like to use a password to connect? (Y/n): ").lower(): - while True: - key_path = input("Enter path to SSH key: ") - - if not os.path.isfile(key_path): - print("Invalid path given") - - continue - try: - logger.debug(f"Attempting to connect with {key_path}") - client.connect(ip, username="root", key_filename=key_path) - except Exception as error: - print("Error while connecting to device: {error}") - - continue - break - else: - while True: - password = input("Enter RM SSH password: ") - - try: - logger.debug(f"Attempting to connect with {password}") - client.connect(ip, username="root", password=password) - except paramiko.ssh_exception.AuthenticationException: - print("Incorrect password given") - - continue - break - - print("Connected to device") - return client - - -def set_server_config(contents, server_host_name): - data_attributes = contents.split("\n") - line = 0 - - logger.debug(f"Contents are:\n{contents}") - - for i in range(0, len(data_attributes)): - if data_attributes[i].startswith("[General]"): - logger.debug("Found [General] line") - line = i + 1 - if not data_attributes[i].startswith("SERVER="): - continue - - data_attributes[i] = f"#{data_attributes[i]}" - logger.debug(f"Using {data_attributes[i]}") - - data_attributes.insert(line, f"SERVER={server_host_name}") - converted = "\n".join(data_attributes) - - logger.debug(f"Converted contents are:\n{converted}") - - return converted - - -""" -This works as intended, but the remarkable device seems to ignore it... - -def enable_web_over_usb(remarkable_remote=None): - if remarkable_remote is None: - with open(r'/home/root/.config/remarkable/xochitl.conf', 'r') as file: - fileContents = file.read() - fileContents = re.sub("WebInterfaceEnabled=.*", "WebInterfaceEnabled=true", fileContents) - - with open(r'/home/root/.config/remarkable/xochitl.conf', 'w') as file: - file.write(fileContents) - - else: - remarkable_remote.exec_command("sed -i 's/WebInterfaceEnabled=.*/WebInterfaceEnabled=true/g' /home/root/.config/remarkable/xochitl.conf") -""" - - -def edit_config(server_ip, port=8080, remarkable_remote=None): - server_host_name = f"http://{server_ip}:{port}" - logger.debug(f"Hostname is: {server_host_name}") - - if not remarkable_remote: - logger.debug("Detected running on local device") - with open("/usr/share/remarkable/update.conf", encoding="utf-8") as file: - modified_conf_version = set_server_config(file.read(), server_host_name) - - with open("/usr/share/remarkable/update.conf", "w") as file: - file.write(modified_conf_version) - - return - - logger.debug("Connecting to FTP") - ftp = remarkable_remote.open_sftp() # or ssh - logger.debug("Connected") - - with ftp.file("/usr/share/remarkable/update.conf") as update_conf_file: - modified_conf_version = set_server_config( - update_conf_file.read().decode("utf-8"), server_host_name - ) - - with ftp.file( - "/usr/share/remarkable/update.conf", "w+" - ) as update_conf_file: # w/w+ mode - update_conf_file.write(modified_conf_version) - - -def get_remarkable_ip(): - while True: - remote_ip = input("Please enter the IP of the remarkable device: ") - if input("Are you sure? (Y/n) ").lower() != "n": - break - - return remote_ip - - -def do_download(args, device_type): - version = version_lookup(version=args.version, device=device_type) - print(f"Downloading {version} to {args.out if args.out else 'downloads folder'}") - filename = updateman.get_version( - version=version, device=device_type, download_folder=args.out - ) - - if filename is None: - raise SystemExit("Error: Was not able to download firmware file!") - - if filename == "Download folder does not exist": - raise SystemExit("Error: Download folder does not exist!") - - if filename == "Not in version list": - raise SystemExit("Error: This version is not currently supported!") - - print(f"Done! ({filename})") - - -def is_rm(): - if not os.path.exists("/sys/devices/soc0/machine"): - return False - - with open("/sys/devices/soc0/machine") as f: - return f.read().strip().startswith("reMarkable") - - -def get_update_image(file): - import ext4 - from remarkable_update_image import UpdateImage - from remarkable_update_image import UpdateImageSignatureException - - image = UpdateImage(file) - volume = ext4.Volume(image, offset=0) - try: - inode = volume.inode_at("/usr/share/update_engine/update-payload-key.pub.pem") - if inode is None: - raise FileNotFoundError() - - inode.verify() - image.verify(inode.open().read()) - - except UpdateImageSignatureException: - warnings.warn("Signature doesn't match contents", RuntimeWarning) - - except FileNotFoundError: - warnings.warn("Public key missing", RuntimeWarning) - except OSError as e: - if e.errno != errno.ENOTDIR: - raise - warnings.warn("Unable to open public key", RuntimeWarning) +class Manager: + """ + Main class for codexctl + """ - return image, volume + def __init__(self, device: str, logger: logging.Logger) -> None: + """Initializes the Manager class for codexctl + Args: + device (str): Type of device that is running the script + logger (logger): Logger object + """ + self.device = device + self.logger = logger + self.updater = UpdateManager(logger) -def do_status(args): - if is_rm(): - if os.path.exists("/etc/remarkable.conf"): - with open("/etc/remarkable.conf") as file: - config_contents = file.read() - else: - config_contents = "" + def call_func(self, function: str, args: dict) -> None: + """Runs a command based on the function name and arguments provided - if os.path.exists("/etc/version"): - with open("/etc/version") as file: - version_id = file.read().rstrip() - else: - version_id = "" + Args: + function: The function to run + args: What arguments to pass into the function + """ - if os.path.exists("/usr/share/remarkable/update.conf"): - with open("/usr/share/remarkable/update.conf") as file: - version_contents = file.read().rstrip() + if "remarkable" not in self.device: + remarkable_version = args.get("hardware") else: - version_contents = "" - - elif not REMOTE_DEPS_MET: - raise SystemExit( - "Error: Detected as running on the remote device, but could not resolve dependencies. " - 'Please install them with "pip install -r requirements.txt' - ) - - else: - ip = "10.11.99.1" if len(get_host_ip()) == 1 else get_remarkable_ip() - logger.debug(f"IP of remarkable is {ip}") - remarkable_remote = connect_to_rm(args, ip=ip) - - logger.debug("Connecting to FTP") - ftp = remarkable_remote.open_sftp() # or ssh - logger.debug("Connected") - - with ftp.file("/etc/remarkable.conf") as file: - config_contents = file.read().decode("utf-8") - - with ftp.file("/etc/version") as file: - version_id = file.read().decode("utf-8").strip("\n") - - with ftp.file("/usr/share/remarkable/update.conf") as file: - version_contents = file.read().decode("utf-8") - - beta = re.search("(?<=BetaProgram=).*", config_contents) - m = re.search("(?<=[Pp]reviousVersion=).*", config_contents) - prev = m.group() if m is not None else "unknown" - current = re.search("(?<=REMARKABLE_RELEASE_VERSION=).*", version_contents).group() - - print( - f'You are running {current} [{version_id}]{"[BETA]" if beta is not None and beta.group() else ""}, previous version was {prev}' - ) - - -def get_available_version(version): - available_versions = scanUpdates() - - logger.debug(f"Available versions found are: {available_versions}") - for device, ids in available_versions.items(): - if version in ids: - available_version = {device: ids} - - return available_version - - -def do_install(args, device_type): - temp_path = None - orig_cwd = os.getcwd() - - if args.serve_folder: # update folder - os.chdir(args.serve_folder) - else: - temp_path = tempfile.mkdtemp() - os.chdir(temp_path) + remarkable_version = self.device - if not os.path.exists("updates"): - os.mkdir("updates") + version = args.get("version", None) - logger.debug(f"Serve path: {os.getcwd()}") - available_versions = scanUpdates() + if remarkable_version: + if version == "latest": + version = self.updater.get_latest_version(remarkable_version) + elif version == "toltec": + version = self.updater.get_toltec_version(remarkable_version) - version = version_lookup(version=args.version, device=device_type) - available_versions = get_available_version(version) + ### Download functionalities + if function == "list": + remarkable_pp_versions = "\n".join(self.updater.remarkablepp_versions.keys()) + remarkable_2_versions = "\n".join(self.updater.remarkable2_versions.keys()) + remarkable_1_versions = "\n".join(self.updater.remarkable1_versions.keys()) - if available_versions is None: - print( - f"The version firmware file you specified could not be found, attempting to download ({version})" - ) - result = updateman.get_version( - version=version, - device=device_type, - download_folder=f"{os.getcwd()}/updates", - ) - - logger.debug(f"Result of downloading version is {result}") - - if result is None: - raise SystemExit("Error: Was not able to download firmware file!") - - if result == "Not in version list": - raise SystemExit("Error: This version is not supported!") - - available_versions = get_available_version(version) - if available_versions is None: - raise SystemExit( - "Error: Something went wrong trying to download update file!" + print( + f"ReMarkable Paper Pro:\n{remarkable_pp_versions}\n\nReMarkable 2:\n{remarkable_2_versions}\n\nReMarkable 1:\n{remarkable_1_versions}" ) - server_host = "0.0.0.0" - remarkable_remote = None - - if not is_rm(): - if not REMOTE_DEPS_MET: - raise SystemExit( - "Error: Detected as running on the remote device, but could not resolve dependencies. " - 'Please install them with "pip install -r requirements.txt' + elif function == "download": + logger.debug(f"Downloading version {version}") + filename = self.updater.download_version( + remarkable_version, version, args["out"] ) - server_host = get_host_ip() + if filename: + print(f"Sucessfully downloaded to {filename}") - logger.debug(f"Server host is {server_host}") - - if server_host is None: - raise SystemExit( - "Error: This device does not seem to have a network connection." - ) - - if len(server_host) == 1: # This means its found the USB interface - server_host = server_host[0] - remarkable_remote = connect_to_rm(args) - else: - host_interfaces = "\n".join(server_host) - - print( - f"\n{host_interfaces}\nCould not find USB interface, assuming connected over WiFi (interfaces list above)" - ) - while True: - server_host = input( - "\nPlease enter your IP for the network the device is connected to: " + ### Mounting functionalities + elif function in ("extract", "mount"): + try: + from .analysis import get_update_image + except ImportError: + raise ImportError( + "remarkable_update_image is required for analysis. Please install it!" ) - if server_host not in host_interfaces.split("\n"): # Really...? This co - print("Error: Invalid IP given") - continue - if "n" in input("Are you sure? (Y/n): ").lower(): - continue - - break - - remote_ip = get_remarkable_ip() + if function == "extract": + if not args["out"]: + args["out"] = os.getcwd() + "/extracted" - remarkable_remote = connect_to_rm(args, remote_ip) + logger.debug(f"Extracting {args['file']} to {args['out']}") + image, volume = get_update_image(args["file"]) + image.seek(0) - logger.debug("Editing config file") - edit_config(remarkable_remote=remarkable_remote, server_ip=server_host, port=8080) - - print( - f"Available versions to update to are: {available_versions}\nThe device will update to the latest one." - ) + with open(args["out"], "wb") as f: + f.write(image.read()) + else: + if args["out"] is None: + args["out"] = "/opt/remarkable/" - logger.debug("Starting server thread") - thread = threading.Thread( - target=startUpdate, args=(available_versions, server_host), daemon=True - ) - thread.start() - - # Is it worth mapping the messages to a variable? - if remarkable_remote is None: - print("Enabling update service") - subprocess.run( - ["/bin/systemctl", "start", "update-engine"], - text=True, - check=True, - env={"PATH": "/bin:/usr/bin:/sbin"}, - ) - - with subprocess.Popen( - ["/usr/bin/update_engine_client", "-update"], - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - env={"PATH": "/bin:/usr/bin:/sbin"}, - ) as process: - if process.wait() != 0: - print("".join(process.stderr.readlines())) - - raise SystemExit("There was an error updating :(") - - logger.debug( - f'Stdout of update checking service is {"".join(process.stderr.readlines())}' - ) + if not os.path.exists(args["out"]): + os.mkdir(args["out"]) - if "y" in input("Done! Would you like to shutdown? (y/N): ").lower(): - subprocess.run( - ["/sbin/shutdown", "now"], - check=True, - env={"PATH": "/bin:/usr/bin:/sbin"}, - ) - else: - print("Checking if device can connect to this machine") - _stdin, stdout, _stderr = remarkable_remote.exec_command( - f"sleep 2 && echo | nc {server_host} 8080" - ) - check = stdout.channel.recv_exit_status() - - logger.debug(f"Stdout of nc checking: {stdout.readlines()}") - if check != 0: - raise SystemExit( - "Device cannot connect to this machine! Is the firewall blocking connections?" - ) + if not os.path.exists(args["filesystem"]): + raise SystemExit("Firmware file does not exist!") - print("Starting update service on device") - remarkable_remote.exec_command("systemctl start update-engine") + from remarkable_update_fuse import UpdateFS - _stdin, stdout, _stderr = remarkable_remote.exec_command( - "update_engine_client -update" - ) - exit_status = stdout.channel.recv_exit_status() + server = UpdateFS() + server.parse( + args=[args["filesystem"], args["out"]], values=server, errex=1 + ) + server.main() - if exit_status != 0: - print("".join(_stderr.readlines())) - raise SystemExit("There was an error updating :(") + ### Analysis functionalities + elif function in ("cat", "ls"): + try: + from .analysis import get_update_image + except ImportError: + raise ImportError( + "remarkable_update_image is required for analysis. Please install it. (Linux only!)" + ) - logger.debug( - f'Stdout of update checking service is {"".join(_stderr.readlines())}' - ) + try: + image, volume = get_update_image(args["file"]) + inode = volume.inode_at(args["target_path"]) - print("Success! Please restart the reMarkable device!") + except FileNotFoundError: + print(f"{args['target_path']}: No such file or directory") + raise FileNotFoundError - os.chdir(orig_cwd) - if temp_path: - logger.debug(f"Removing {temp_path}") - shutil.rmtree(temp_path) + except OSError as e: + print(f"{args['target_path']}: {os.strerror(e.errno)}") + sys.exit(e.errno) + if function == "cat": + sys.stdout.buffer.write(inode.open().read()) -def do_restore(args): - if "y" not in input("Are you sure you want to restore? (y/N): ").lower(): - raise SystemExit("Aborted!!!") + elif function == "ls": + print(" ".join([x.name_str for x, _ in inode.opendir()])) - if os.path.isfile("/usr/share/remarkable/update.conf"): - subprocess.run( - ["/bin/bash", "-l", "-c", RESTORE_CODE], - text=True, - check=True, - env={"PATH": "/bin:/usr/bin:/sbin"}, - ) + ### WebInterface functionalities + elif function in ("backup", "upload"): + from .sync import RmWebInterfaceAPI - if "y" in input("Done! Would you like to shutdown? (y/N): ").lower(): - subprocess.run( - ["shutdown", "now"], - check=True, - env={"PATH": "/bin:/usr/bin:/sbin"}, + print( + "Please make sure the web-interface is enabled in the remarkable settings!\nStarting upload" ) - elif not REMOTE_DEPS_MET: - raise SystemExit( - "Error: Detected as running on the remote device, but could not resolve dependencies. " - 'Please install them with "pip install -r requirements.txt"' - ) - - else: - if len(get_host_ip()) == 1: - print("Detected as USB connection") - remote_ip = "10.11.99.1" - else: - print("Detected as WiFi connection") - remote_ip = get_remarkable_ip() - - remarkable_remote = connect_to_rm(args, remote_ip) - - _stdin, stdout, _stderr = remarkable_remote.exec_command(RESTORE_CODE) - stdout.channel.recv_exit_status() + rmWeb = RmWebInterfaceAPI(BASE="http://10.11.99.1/", logger=logger) - logger.debug(f"Output of switch command: {stdout}") - - print("Done, Please reboot the device!") + if function == "backup": + rmWeb.sync( + localFolder=args["local"], + remoteFolder=args["remote"], + recursive=not args["no_recursion"], + overwrite=not args["no_overwrite"], + ) + else: + rmWeb.upload(input_paths=args["paths"], remoteFolder=args["remote"]) + + ### Update & Version functionalities + elif function in ("install", "status", "restore"): + remote = False + + if "remarkable" not in self.device: + if importlib.util.find_spec("paramiko") is None: + raise ImportError( + "Paramiko is required for SSH access. Please install it." + ) + if importlib.util.find_spec("psutil") is None: + raise ImportError( + "Psutil is required for SSH access. Please install it." + ) + remote = True + + from .device import DeviceManager + from .server import get_available_version + + remarkable = DeviceManager( + remote=remote, + address=args["address"], + logger=self.logger, + authentication=args["password"], + ) + if version == "latest": + version = self.updater.get_latest_version(remarkable.hardware) + elif version == "toltec": + version = self.updater.get_toltec_version(remarkable.hardware) -def do_list(): - print("\nRM2:") - [print(codexID) for codexID in updateman.id_lookups_rm2] - print("\nRM1:") - [print(codexID) for codexID in updateman.id_lookups_rm1] + if function == "status": + beta, prev, current, version_id = remarkable.get_device_status() + print( + f"\nCurrent version: {current}\nOld update engine: {prev}\nBeta active: {beta}\nVersion id: {version_id}" + ) + elif function == "restore": + if remarkable.hardware == "ferrari": + raise SystemError("Restore not available for rmpro.") + remarkable.restore_previous_version() + print( + f"Device restored to previous version [{remarkable.get_device_status()[1]}]" + ) + remarkable.reboot_device() + print("Device rebooted") -def do_upload(args): - print( - "Please make sure the web-interface is enabled in the remarkable settings!\nStarting upload..." - ) + else: + temp_path = None + made_update_folder = False + orig_cwd = os.getcwd() - rmWeb = RmWebInterfaceAPI(BASE="http://10.11.99.1/", logger=logger) + # Do we have a specific update file to serve? - rmWeb.upload( - input_paths=args.paths, - remoteFolder=args.remote, - ) + update_file = version if os.path.isfile(version) else None + + version_lookup = lambda version: re.search(r'\b\d+\.\d+\.\d+\.\d+\b', version) + version_number = version_lookup(version) + if not version_number: + version_number = input("Failed to get the version number from the filename, please enter it: ") + if not version_lookup(version_number): + raise SystemError("Invalid version!") -def do_backup(args): - print( - "Please make sure the web-interface is enabled in the remarkable settings!\nStarting backup..." - ) + version_number = version_number.group() - rmWeb = RmWebInterfaceAPI(BASE="http://10.11.99.1/", logger=logger) + update_file_requires_new_engine = UpdateManager.uses_new_update_engine( + version_number + ) + device_version_uses_new_engine = UpdateManager.uses_new_update_engine( + remarkable.get_device_status()[2] + ) - rmWeb.sync( - localFolder=args.local, - remoteFolder=args.remote, - recursive=not args.no_recursion, - overwrite=not args.no_overwrite, + #### PREVENT USERS FROM INSTALLING NON-COMPATIBLE IMAGES #### + + if device_version_uses_new_engine: + if not update_file_requires_new_engine: + raise SystemError("Cannot downgrade to this version as it uses the old update engine, please manually downgrade.") + # TODO: Implement manual downgrading. + # `codexctl download --out . 3.11.2.5` + # `codexctl extract --out 3.11.2.5.img 3.11.2.5_reMarkable2-qLFGoqPtPL.signed` + # `codexctl transfer 3.11.2.5.img ~/root` + # `dd if=/home/root/3.11.2.5.img of=/dev/mmcblk2p2` (depending on fallback partition) + # `codexctl restore` + + else: + if update_file_requires_new_engine: + raise SystemError("This version requires the new update engine, please upgrade your device to version 3.11.2.5 first.") + + ############################################################# + + update_file_requires_new_engine = False + device_version_uses_new_engine = False + + if not update_file_requires_new_engine: + if update_file: # Check if file exists + if not (os.path.dirname(os.path.abspath(update_file)) == os.path.abspath("updates")): + if not os.path.exists("updates"): + os.mkdir("updates") + shutil.move(update_file, "updates") + update_file = get_available_version(version) + made_update_folder = True # Delete at end + + # If version was a valid location file, update_file will be the location else it'll be a version number + + if not update_file: + temp_path = tempfile.mkdtemp() + os.chdir(temp_path) + + print(f"Version {version} not found. Attempting to download") + + location = "./" + if not update_file_requires_new_engine: + location += "updates" + + result = self.updater.download_version( + remarkable.hardware, version, location + ) + if result: + print(f"Downloaded version {version} to {result}") + + if device_version_uses_new_engine: + update_file = result + else: + update_file = get_available_version(version) + + else: + raise SystemExit( + f"Failed to download version {version}! Does this version or location exist?" + ) + + if device_version_uses_new_engine: + remarkable.install_sw_update(update_file) + else: + remarkable.install_ohma_update(update_file) + + if made_update_folder: # Move update file back out + shutil.move(os.listdir("updates")[0], "../") + shutil.rmtree("updates") + + os.chdir(orig_cwd) + if temp_path: + logger.debug(f"Removing temporary folder {temp_path}") + shutil.rmtree(temp_path) + + +def main() -> None: + """Main function for codexctl""" + + ### Setting up the argument parser + parser = argparse.ArgumentParser("Codexctl") + parser.add_argument( + "--verbose", + "-v", + required=False, + help="Enable verbose logging", + action="store_true", ) - - -def do_ls(args): - image, volume = get_update_image(args.file) - try: - inode = volume.inode_at(args.target_path) - print(" ".join([x.name_str for x, _ in inode.opendir()])) - - except FileNotFoundError: - print(f"cannot access '{args.target_path}': No such file or directory") - sys.exit(1) - - except OSError as e: - print(f"cannot access '{args.target_path}': {os.strerror(e.errno)}") - sys.exit(e.errno) - - -def do_cat(args): - image, volume = get_update_image(args.file) - try: - inode = volume.inode_at(args.target_path) - sys.stdout.buffer.write(inode.open().read()) - - except FileNotFoundError: - print(f"'{args.target_path}': No such file or directory") - sys.exit(1) - - except OSError: - print(f"'{args.target_path}': {os.strerror(e.errno)}") - sys.exit(e.errno) - - -def do_extract(args): - if not args.out: - args.out = os.getcwd() + "/extracted" - - logger.debug(f"Extracting {args.file} to {args.out}") - image, volume = get_update_image(args.file) - image.seek(0) - with open(args.out, "wb") as f: - f.write(image.read()) - - -def do_mount(args): - if sys.platform != "linux": - raise NotImplementedError( - f"Mounting has not been implemented on {sys.platform}" - ) - - if args.out is None: - args.out = "/opt/remarkable/" - - if not os.path.exists(args.out): - os.mkdir(args.out) - - if not os.path.exists(args.filesystem): - raise SystemExit("Firmware file does not exist!") - - from remarkable_update_fuse import UpdateFS - - server = UpdateFS() - server.parse(args=[args.filesystem, args.out], values=server, errex=1) - server.main() - - -def main(): - parser = argparse.ArgumentParser("Codexctl app") - parser.add_argument("--debug", action="store_true", help="Print debug info") - parser.add_argument("--rm1", action="store_true", default=False, help="Use rm1") parser.add_argument( - "--auth", required=False, help="Specify password or SSH key for SSH" + "--address", + "-a", + required=False, + help="Specify the address of the device", + default=None, ) parser.add_argument( - "--verbose", required=False, help="Enable verbose logging", action="store_true" + "--password", + "-p", + required=False, + help="Specify password or path to SSH key for remote access", ) - subparsers = parser.add_subparsers(dest="command") subparsers.required = True # This fixes a bug with older versions of python + ### Install subcommand install = subparsers.add_parser( "install", help="Install the specified version (will download if not available on the device)", ) + install.add_argument("version", help="Version (or location to file) to install") + + ### Download subcommand download = subparsers.add_parser( "download", help="Download the specified version firmware file" ) - backup = subparsers.add_parser( - "backup", help="Download remote files to local directory" - ) - extract = subparsers.add_parser( - "extract", help="Extract the specified version update file" - ) - mount = subparsers.add_parser( - "mount", help="Mount the specified version firmware filesystem" - ) - upload = subparsers.add_parser( - "upload", help="Upload folder/files to device (pdf only)" - ) - subparsers.add_parser( - "status", help="Get the current version of the device and other information" - ) - subparsers.add_parser( - "restore", help="Restores to previous version installed on device" - ) - subparsers.add_parser("list", help="List all versions available for use") - ls = subparsers.add_parser("ls", help="List files inside an update image") - cat = subparsers.add_parser( - "cat", help="Cat the contents of a file inside an update image" - ) - - install.add_argument("version", help="Version to install") - install.add_argument( - "-sf", - "--serve-folder", - help="Location of folder containing update folder & files", - default=None, - ) - download.add_argument("version", help="Version to download") - download.add_argument("--out", help="Folder to download to", default=None) - - extract.add_argument("file", help="Path to update file to extract", default=None) - extract.add_argument("--out", help="Folder to extract to", default=None) - - mount.add_argument( - "filesystem", - help="Path to version firmware filesystem to extract", - default=None, + download.add_argument("--out", "-o", help="Folder to download to", default=None) + download.add_argument( + "--hardware", "-hd", help="Hardware to download for", required=True ) - mount.add_argument("--out", help="Folder to mount to", default=None) - upload.add_argument( - "paths", help="Path to file(s)/folder to upload", default=None, nargs="+" - ) - upload.add_argument( - "-r", - "--remote", - help="Remote directory to upload to. Defaults to root folder", - default="", + ### Backup subcommand + backup = subparsers.add_parser( + "backup", help="Download remote files to local directory" ) - backup.add_argument( "-r", "--remote", @@ -750,72 +381,92 @@ def main(): "-no-ow", "--no-overwrite", help="Disables overwrite", action="store_true" ) - ls.add_argument("file", help="Path to update file to extract", default=None) - ls.add_argument("target_path", help="Path inside the image to list", default=None) - + ### Cat subcommand + cat = subparsers.add_parser( + "cat", help="Cat the contents of a file inside a firmwareimage" + ) cat.add_argument("file", help="Path to update file to cat", default=None) cat.add_argument("target_path", help="Path inside the image to list", default=None) - args = parser.parse_args() - level = "ERROR" - - if args.verbose: - level = "DEBUG" - - logger.remove() - logger.add(sys.stderr, level=level) - logging.basicConfig( - level=logging.DEBUG if args.verbose else logging.ERROR - ) # For paramioko - - global updateman - updateman = UpdateManager(logger=logger) - - logger.debug(f"Remote deps met: {REMOTE_DEPS_MET}") - - choice = args.command - - device_type = 2 - - if args.rm1: - device_type = 1 + ### Ls subcommand + ls = subparsers.add_parser("ls", help="List files inside a firmware image") + ls.add_argument("file", help="Path to update file to extract", default=None) + ls.add_argument("target_path", help="Path inside the image to list", default=None) - logger.debug(f"Inputs are: {args}") + ### Extract subcommand + extract = subparsers.add_parser( + "extract", help="Extract the specified version update file" + ) + extract.add_argument("file", help="Path to update file to extract", default=None) + extract.add_argument("--out", help="Folder to extract to", default=None) - ### Decision making ### - if choice == "install": - do_install(args, device_type) + ### Mount subcommand + mount = subparsers.add_parser( + "mount", help="Mount the specified version firmware filesystem" + ) + mount.add_argument( + "filesystem", + help="Path to version firmware filesystem to extract", + default=None, + ) + mount.add_argument("--out", help="Folder to mount to", default=None) - elif choice == "download": - do_download(args, device_type) + ### Upload subcommand + upload = subparsers.add_parser( + "upload", help="Upload folder/files to device (pdf only)" + ) + upload.add_argument( + "paths", help="Path to file(s)/folder to upload", default=None, nargs="+" + ) + upload.add_argument( + "-r", + "--remote", + help="Remote directory to upload to. Defaults to root folder", + default="", + ) - elif choice == "status": - do_status(args) + ### Status subcommand + subparsers.add_parser( + "status", help="Get the current version of the device and other information" + ) - elif choice == "restore": - do_restore(args) + ### Restore subcommand + subparsers.add_parser( + "restore", help="Restores to previous version installed on device" + ) - elif choice == "list": - do_list() + ### List subcommand + subparsers.add_parser("list", help="List all available versions") - elif choice == "backup": - do_backup(args) + ### Setting logging level + args = parser.parse_args() + logging_level, paramiko_level = ( + ("DEBUG", logging.DEBUG) if args.verbose else ("ERROR", logging.ERROR) + ) - elif choice == "upload": - do_upload(args) + try: + logger.remove() + logger.add(sys.stderr, level=logging_level) + logging.basicConfig(level=paramiko_level) + except AttributeError: # For non-loguru + logger.level = logging_level - elif choice == "extract": - do_extract(args) + ### Detecting device information + device = None - elif choice == "mount": - do_mount(args) + if os.path.exists("/sys/devices/soc0/machine"): + with open("/sys/devices/soc0/machine") as machine_file: + contents = machine_file.read().strip() - elif choice == "ls": - do_ls(args) + if "reMarkable" in contents: + device = contents # reMarkable 1, reMarkable 2, reMarkable Ferrari - elif choice == "cat": - do_cat(args) + if device is None: + device = sys.platform + logger.debug(f"Running on platform: {device}") + logger.debug(f"Running with args: {args}") -if __name__ == "__main__": - main() + ### Call function + man = Manager(device, logger) + man.call_func(args.command, vars(args)) diff --git a/codexctl/__main__.py b/codexctl/__main__.py index 868d99e..09326c0 100644 --- a/codexctl/__main__.py +++ b/codexctl/__main__.py @@ -1,4 +1,4 @@ -from . import main - -if __name__ == "__main__": - main() +from . import main + +if __name__ == "__main__": + main() diff --git a/codexctl/analysis.py b/codexctl/analysis.py new file mode 100644 index 0000000..8e8fe85 --- /dev/null +++ b/codexctl/analysis.py @@ -0,0 +1,33 @@ +import ext4 +import warnings +import errno + +from remarkable_update_image import UpdateImage +from remarkable_update_image import UpdateImageSignatureException + + +def get_update_image(file: str): + """Extracts files from an update image (<3.11 currently)""" + + image = UpdateImage(file) + volume = ext4.Volume(image, offset=0) + try: + inode = volume.inode_at("/usr/share/update_engine/update-payload-key.pub.pem") + if inode is None: + raise FileNotFoundError() + + inode.verify() + image.verify(inode.open().read()) + + except UpdateImageSignatureException: + warnings.warn("Signature doesn't match contents", RuntimeWarning) + + except FileNotFoundError: + warnings.warn("Public key missing", RuntimeWarning) + + except OSError as e: + if e.errno != errno.ENOTDIR: + raise + warnings.warn("Unable to open public key", RuntimeWarning) + + return image, volume diff --git a/codexctl/device.py b/codexctl/device.py new file mode 100644 index 0000000..8313419 --- /dev/null +++ b/codexctl/device.py @@ -0,0 +1,643 @@ +import socket +import subprocess +import logging +import threading +import re +import os +import time + +from .server import startUpdate + +try: + import paramiko + import psutil +except ImportError: + pass + + +class DeviceManager: + def __init__( + self, logger=None, remote=False, address=None, authentication=None + ) -> None: + """Initializes the DeviceManager for codexctl + + Args: + remote (bool, optional): Whether the device is remote. Defaults to False. + address (bool, optional): Known IP of remote device, if applicable. Defaults to None. + logger (logger, optional): Logger object for logging. Defaults to None. + Authentication (str, optional): Authentication method. Defaults to None. + """ + self.logger = logger + self.address = address + self.authentication = authentication + self.client = None + + if self.logger is None: + self.logger = logging + + if remote: + self.client = self.connect_to_device( + authentication=authentication, remote_address=address + ) + + self.client.authentication = authentication + self.client.address = address + + ftp = self.client.open_sftp() + with ftp.file("/sys/devices/soc0/machine") as file: + machine_contents = file.read().decode("utf-8").strip("\n") + else: + try: + with open("/sys/devices/soc0/machine") as file: + machine_contents = file.read().decode("utf-8").strip("\n") + except FileNotFoundError: + machine_contents = "tests" + + if "reMarkable Ferrari" in machine_contents: + self.hardware = "ferrari" + elif "reMarkable 1" in machine_contents: + self.hardware = "reMarkable1" + else: + self.hardware = "reMarkable2" + + def get_host_address(self) -> list[str] | list | None: # Interaction required + """Gets the IP address of the host machine + + Returns: + str | None: IP address of the host machine, or None if not found + """ + + possible_ips = [] + try: + for interface, snics in psutil.net_if_addrs().items(): + self.logger.debug(f"New interface found: {interface}") + for snic in snics: + if snic.family == socket.AF_INET: + if snic.address.startswith("10.11.99"): + return snic.address + self.logger.debug(f"Adding new address: {snic.address}") + possible_ips.append(snic.address) + + except Exception as error: + self.logger.error(f"Error automatically getting interfaces: {error}") + + if possible_ips: + host_interfaces = "\n".join(possible_ips) + else: + host_interfaces = "Could not find any available interfaces." + + print(f"\n{host_interfaces}") + while True: + host_address = input( + "\nPlease enter your host IP for the network the device is connected to: " + ) + + if possible_ips and host_address not in host_interfaces.split("\n"): + print("Error: Invalid IP given") + continue + + if "n" in input("Are you sure? (Y/n): ").lower(): + continue + + break + + return host_address + + def get_remarkable_address(self) -> str: + """Gets the IP address of the remarkable device + + Returns: + str: IP address of the remarkable device + """ + + if self.check_is_address_reachable("10.11.99.1"): + return "10.11.99.1" + + while True: + remote_ip = input("Please enter the IP of the remarkable device: ") + + if self.check_is_address_reachable(remote_ip): + return remote_ip + + print(f"Error: Device {remote_ip} is not reachable. Please try again.") + + def check_is_address_reachable(self, remote_ip="10.11.99.1") -> bool: + """Checks if the given IP address is reachable over SSH + + Args: + remote_ip (str, optional): IP to check. Defaults to '10.11.99.1'. + + Returns: + bool: True if reachable, False otherwise + """ + self.logger.debug(f"Checking if {remote_ip} is reachable") + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(1) + + sock.connect((remote_ip, 22)) + sock.shutdown(2) + + return True + + except Exception: + self.logger.debug(f"Device {remote_ip} is not reachable") + return False + + def connect_to_device( + self, remote_address=None, authentication=None + ) -> paramiko.client.SSHClient: + """Connects to the device using the given IP address + + Args: + remote_address (str, optional): IP address of the device. + authentication (str, optional): Authentication credentials. Defaults to None. + + Returns: + paramiko.client.SSHClient: SSH client object for the device. + """ + + if remote_address is None: + remote_address = self.get_remarkable_address() + self.address = remote_address # For future reference + else: + if self.check_is_address_reachable(remote_address) is False: + raise SystemError(f"Error: Device {remote_address} is not reachable!") + + client = paramiko.client.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + if authentication: + self.logger.debug(f"Using authentication: {authentication}") + try: + if os.path.isfile(authentication): + self.logger.debug( + f"Attempting to connect to {remote_address} with key file {authentication}" + ) + client.connect( + remote_address, username="root", key_filename=authentication + ) + else: + self.logger.debug( + f"Attempting to connect to {remote_address} with password {authentication}" + ) + client.connect( + remote_address, username="root", password=authentication + ) + + except paramiko.ssh_exception.AuthenticationException: + print("Incorrect password or ssh path given in arguments!") + + elif ( + "n" in input("Would you like to use a password to connect? (Y/n): ").lower() + ): + while True: + key_path = input("Enter path to SSH key: ") + + if not os.path.isfile(key_path): + print("Invalid path given") + + continue + try: + self.logger.debug( + f"Attempting to connect to {remote_address} with key file {key_path}" + ) + client.connect( + remote_address, username="root", key_filename=key_path + ) + except Exception: + print("Error while connecting to device: {error}") + + continue + break + else: + while True: + password = input("Enter RM SSH password: ") + + try: + self.logger.debug( + f"Attempting to connect to {remote_address} with password {password}" + ) + client.connect(remote_address, username="root", password=password) + except paramiko.ssh_exception.AuthenticationException: + print("Incorrect password given") + + continue + break + + print("Success: Connected to device") + + return client + + def get_device_status(self) -> tuple[str | None, str, str]: + """Gets the status of the device + + Returns: + tuple: Beta status, previous version, and current version (in that order) + """ + old_update_engine = True + + if self.client: + self.logger.debug("Connecting to FTP") + ftp = self.client.open_sftp() + self.logger.debug("Connected") + + try: + with ftp.file("/usr/share/remarkable/update.conf") as file: + xochitl_version = re.search( + "(?<=REMARKABLE_RELEASE_VERSION=).*", + file.read().decode("utf-8").strip("\n"), + ).group() + except Exception: + with ftp.file("/etc/os-release") as file: + xochitl_version = ( + re.search("(?<=IMG_VERSION=).*", file.read().decode("utf-8")) + .group() + .strip('"') + ) + old_update_engine = False + + with ftp.file("/etc/version") as file: + version_id = file.read().decode("utf-8").strip("\n") + + with ftp.file("/home/root/.config/remarkable/xochitl.conf") as file: + beta_contents = file.read().decode("utf-8") + + else: + if os.path.exists("/usr/share/remarkable/update.conf"): + with open("/usr/share/remarkable/update.conf") as file: + xochitl_version = re.search( + "(?<=REMARKABLE_RELEASE_VERSION=).*", + file.read().decode("utf-8").strip("\n"), + ).group() + else: + with open("/etc/os-release") as file: + xochitl_version = ( + re.search("(?<=IMG_VERSION=).*", file.read().decode("utf-8")) + .group() + .strip('"') + ) + + old_update_engine = False + if os.path.exists("/etc/version"): + with open("/etc/version") as file: + version_id = file.read().rstrip() + else: + version_id = "" + + if os.path.exists("/home/root/.config/remarkable/xochitl.conf"): + with open("/home/root/.config/remarkable/xochitl.conf") as file: + beta_contents = file.read().rstrip() + else: + beta_contents = "" + + beta_possible = re.search("(?<=GROUP=).*", beta_contents) + beta = "Release" + + if beta_possible is not None: + beta = re.search("(?<=GROUP=).*", beta_contents).group() + + return beta, old_update_engine, xochitl_version, version_id + + def set_server_config(self, contents: str, server_host_name: str) -> str: + """Converts the contents given to point to the given server IP and port + + Args: + contents (str): Contents of the update.conf file + server_host_name (str): Hostname of the server + + Returns: + str: Converted contents + """ + data_attributes = contents.split("\n") + line = 0 + + self.logger.debug(f"Contents are:\n{contents}") + + for i in range(0, len(data_attributes)): + if data_attributes[i].startswith("[General]"): + self.logger.debug("Found [General] line") + line = i + 1 + if not data_attributes[i].startswith("SERVER="): + continue + + data_attributes[i] = f"#{data_attributes[i]}" + self.logger.debug(f"Using {data_attributes[i]}") + + data_attributes.insert(line, f"SERVER={server_host_name}") + converted = "\n".join(data_attributes) + + self.logger.debug(f"Converted contents are:\n{converted}") + + return converted + + def edit_update_conf(self, server_ip: str, server_port: str) -> bool: + """Edits the update.conf file to point to the given server IP and port + + Args: + server_ip (str): IP of update server + server_port (str): Port of update service + + Returns: + bool: True if successful, False otherwise + """ + server_host_name = f"http://{server_ip}:{server_port}" + self.logger.debug(f"Hostname is: {server_host_name}") + try: + if not self.client: + self.logger.debug("Detected running on local device") + with open( + "/usr/share/remarkable/update.conf", encoding="utf-8" + ) as file: + modified_conf_version = self.set_server_config( + file.read(), server_host_name + ) + + with open("/usr/share/remarkable/update.conf", "w") as file: + file.write(modified_conf_version) + + return True + + self.logger.debug("Connecting to FTP") + ftp = self.client.open_sftp() # or ssh + self.logger.debug("Connected") + + with ftp.file("/usr/share/remarkable/update.conf") as update_conf_file: + modified_conf_version = self.set_server_config( + update_conf_file.read().decode("utf-8"), server_host_name + ) + + with ftp.file("/usr/share/remarkable/update.conf", "w") as update_conf_file: + update_conf_file.write(modified_conf_version) + + return True + except Exception as error: + self.logger.error(f"Error while editing update.conf: {error}") + return False + + def restore_previous_version(self) -> None: + """Restores the previous version of the device""" + + RESTORE_CODE = """/sbin/fw_setenv "upgrade_available" "1" +/sbin/fw_setenv "bootcount" "0" + +OLDPART=$(/sbin/fw_printenv -n active_partition) +if [ $OLDPART == "2" ]; then + NEWPART="3" +else + NEWPART="2" +fi +echo "new: ${NEWPART}" +echo "fallback: ${OLDPART}" + +/sbin/fw_setenv "fallback_partition" "${OLDPART}" +/sbin/fw_setenv "active_partition" "${NEWPART}\"""" + + if self.client: + self.logger.debug("Connecting to FTP") + ftp = self.client.open_sftp() + self.logger.debug("Connected") + + with ftp.file("/usr/bin/restore.sh", "w") as file: + file.write(RESTORE_CODE) + + self.logger.debug("Setting permissions and running restore.sh") + + self.client.exec_command("chmod +x /usr/bin/restore.sh") + self.client.exec_command("bash /usr/bin/restore.sh") + else: + with open("/usr/bin/restore.sh", "w") as file: + file.write(RESTORE_CODE) + + self.logger.debug("Setting permissions and running restore.sh") + + os.system("chmod +x /usr/bin/restore.sh") + os.system("/usr/bin/restore.sh") + + self.logger.debug("Restore script ran") + + def install_sw_update(self, version_file: str) -> None: + """ + Installs new version from version file path, utilising swupdate + + Args: + version_file (str): Path to img file + + Raises: + SystemExit: If there was an error installing the update + + """ + command = f'/usr/bin/swupdate -v -i VERSION_FILE -k /usr/share/swupdate/swupdate-payload-key-pub.pem -H "{self.hardware}:1.0" -e "stable,copy1"' + + if self.client: + ftp_client = self.client.open_sftp() + + print(f"Uploading {version_file} image") + + out_location = f'/tmp/{os.path.basename(version_file)}.swu' + ftp_client.put( + version_file, out_location, callback=self.output_put_progress + ) + + print("\nDone! Running swupdate (PLEASE BE PATIENT, ~5 MINUTES)") + + command = command.replace("VERSION_FILE", out_location) + + for num in (1, 2): + command = command.replace( + "stable,copy1", f"stable,copy{num}" + ) # terrible hack but it works + self.logger.debug(command) + _stdin, stdout, _stderr = self.client.exec_command(command) + + self.logger.debug(f"Stdout of swupdate checking: {stdout.readlines()}") + + exit_status = stdout.channel.recv_exit_status() + + if exit_status != 0: + if "over our current root" in "".join(_stderr.readlines()): + continue + else: + print("".join(_stderr.readlines())) + raise SystemError("Update failed!") + + print("Done! Now rebooting the device and disabling update service") + + #### Now disable automatic updates + + self.client.exec_command("sleep 1 && reboot") # Should be enough + self.client.close() + + time.sleep( + 2 + ) # Somehow the code runs faster than the time it takes for the device to reboot + + print("Trying to connect to device") + + while not self.check_is_address_reachable(self.address): + time.sleep(1) + + self.client = self.connect_to_device( + remote_address=self.address, authentication=self.authentication + ) + self.client.exec_command("systemctl stop swupdate memfaultd") + + print( + "Update complete and update service disabled, restart device to enable it" + ) + + else: + print("Running swupdate") + command = command.replace("VERSION_FILE", version_file) + + for num in (1, 2): + command = command.replace( + "stable,copy1", f"stable,copy{num}" + ) # terrible hack but it works + self.logger.debug(command) + + with subprocess.Popen( + command, + text=True, + shell=True, # Being lazy... + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env={"PATH": "/bin:/usr/bin:/sbin"}, + ) as process: + if process.wait() != 0: + if "installing over our current root" in "".join( + process.stderr.readlines() + ): + continue + else: + print("".join(process.stderr.readlines())) + raise SystemError("Update failed") + + self.logger.debug( + f'Stdout of update checking service is {"".join(process.stdout.readlines())}' + ) + + print("Update complete and device rebooting") + os.system("reboot") + + def install_ohma_update(self, version_available: dict) -> None: + """Installs version from update folder on the device + + Args: + version_available (dict): Version available for installation from `get_available_version` + + Raises: + SystemExit: If there was an error installing the update + """ + + server_host = self.get_host_address() + + self.logger.debug("Editing config file") + + if ( + self.edit_update_conf(server_ip=server_host, server_port=8085) is False + ): # We want a port that probably isn't being used + self.logger.error("Error while editing update.conf") + + return + + thread = threading.Thread( + target=startUpdate, args=(version_available, server_host), daemon=True + ) + thread.start() + + self.logger.debug("Thread started") + + if self.client: + print("Checking if device can connect to this machine") + + _stdin, stdout, _stderr = self.client.exec_command( + f"sleep 2 && echo | nc {server_host} 8085" + ) + check = stdout.channel.recv_exit_status() + self.logger.debug(f"Stdout of nc checking: {stdout.readlines()}") + + if check != 0: + raise SystemError( + "Device cannot connect to this machine! Is the firewall blocking connections?" + ) + + print("Starting update service on device") + + self.client.exec_command("systemctl start update-engine") + + _stdin, stdout, _stderr = self.client.exec_command( + "/usr/bin/update_engine_client -update" + ) + exit_status = stdout.channel.recv_exit_status() + + if exit_status != 0: + print("".join(_stderr.readlines())) + raise SystemError("There was an error updating :(") + + self.logger.debug( + f'Stdout of update checking service is {"".join(_stderr.readlines())}' + ) + + #### Now disable automatic updates + + print("Done! Now rebooting the device and disabling update service") + + self.client.exec_command("sleep 1 && reboot") # Should be enough + self.client.close() + + time.sleep( + 2 + ) # Somehow the code runs faster than the time it takes for the device to reboot + + print("Trying to connect to device") + + while not self.check_is_address_reachable(self.address): + time.sleep(1) + + self.client = self.connect_to_device( + remote_address=self.address, authentication=self.authentication + ) + self.client.exec_command("systemctl stop update-engine") + + print( + "Update complete and update service disabled. Restart device to enable it" + ) + + else: + print("Enabling update service") + + subprocess.run( + ["/bin/systemctl", "start", "update-engine"], + text=True, + check=True, + env={"PATH": "/bin:/usr/bin:/sbin"}, + ) + + with subprocess.Popen( + ["/usr/bin/update_engine_client", "-update"], + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env={"PATH": "/bin:/usr/bin:/sbin"}, + ) as process: + if process.wait() != 0: + print("".join(process.stderr.readlines())) + + raise SystemError("There was an error updating :(") + + self.logger.debug( + f'Stdout of update checking service is {"".join(process.stderr.readlines())}' + ) + + print("Update complete and device rebooting") + os.system("reboot") + + @staticmethod + def output_put_progress(transferred: int, toBeTransferred: int) -> None: + """Used for displaying progress for paramiko ftp.put function""" + + print( + f"Transferring progress{int((transferred/toBeTransferred)*100)}%", + end="\r", + ) diff --git a/codexctl/server.py b/codexctl/server.py index 35cb39b..766d8f7 100644 --- a/codexctl/server.py +++ b/codexctl/server.py @@ -1,162 +1,172 @@ -from http.server import HTTPServer, SimpleHTTPRequestHandler -import http.server -import xml.etree.ElementTree as ET -import os, sys, io, socket, hashlib, base64 -import binascii -import time - -response_ok = """ - - - - - - - -""" - -response_template = """ - - - - - - - - - - - - - - - - - - - - -""" - - -def getupdateinfo(platform, version, update_name): - full_path = os.path.join("updates", update_name) - - update_size = str(os.path.getsize(full_path)) - - BUF_SIZE = 8192 - - sha1 = hashlib.sha1() - sha256 = hashlib.sha256() - with open(full_path, "rb") as f: - while True: - data = f.read(BUF_SIZE) - if not data: - break - sha1.update(data) - sha256.update(data) - update_sha1 = binascii.b2a_base64(sha1.digest(), newline=False).decode() - update_sha256 = binascii.b2a_base64(sha256.digest(), newline=False).decode() - return (update_sha1, update_sha256, update_size) - - -def scanUpdates(): - files = os.listdir("updates") - versions = {} - - for f in files: - p = f.split("_") - if len(p) != 2: - continue - t = p[1].split(".") - if len(t) != 2: - continue - - z = t[0].split("-") - - version = p[0] - # print(version) - product = z[0] - - if not product in versions or versions[product][0] < version: - versions[product] = (version, f) - - return versions - - -class MySimpleHTTPRequestHandler(SimpleHTTPRequestHandler): - def do_POST(self): - length = int(self.headers.get("Content-Length")) - body = self.rfile.read(length).decode("utf-8") - print(body) - xml = ET.fromstring(body) - updatecheck_node = xml.find("app/updatecheck") - - # check for update - if updatecheck_node is not None: - version = xml.attrib["version"] - platform = xml.find("os").attrib["platform"] - print("requested: ", version) - print("platform: ", platform) - - version, update_name = available_versions[platform] - - update_sha1, update_sha256, update_size = getupdateinfo( - platform, version, update_name - ) - params = { - "version": version, - "update_name": f"updates/{update_name}", - "update_sha1": update_sha1, - "update_sha256": update_sha256, - "update_size": update_size, - "codebase_url": host_url, - } - - response = response_template.format(**params) - print("Response:") - print(response) - self.send_response(200) - self.end_headers() - self.wfile.write(response.encode()) - return - - event_node = xml.find("app/event") - event_type = int(event_node.attrib["eventtype"]) - event_result = int(event_node.attrib["eventresult"]) - - # post install status - if event_result != 0: - print("Update done") - if "errorcode" in event_node.attrib: - print("With errorcode:", event_node.attrib["errorcode"]) - return - - # update done - if event_type == 14: - print("OK Response:") - print(response_ok) - self.send_response(200) - self.end_headers() - self.wfile.write(response_ok.encode()) - return - - -def startUpdate(versionsGiven, host, port=8080): - global available_versions - global host_url # I am aware globals are generally bad practice, but this is a quick and dirty solution - - host_url = f"http://{host}:{port}/" - available_versions = versionsGiven - - if not available_versions: - raise FileNotFoundError("Could not find any update files") - - handler = MySimpleHTTPRequestHandler - print(f"Starting fake updater at {host}:{port}") - try: - httpd = HTTPServer((host, port), handler) - except OSError as e: - print("Error: Could not start fake updater. Is the port already in use?") - return - httpd.serve_forever() +from http.server import HTTPServer, SimpleHTTPRequestHandler +import xml.etree.ElementTree as ET +import os +import hashlib +import binascii + +response_ok = """ + + + + + + + +""" + +response_template = """ + + + + + + + + + + + + + + + + + + + + +""" + + +def getupdateinfo(platform, version, update_name): + full_path = os.path.join("updates", update_name) + + update_size = str(os.path.getsize(full_path)) + + BUF_SIZE = 8192 + + sha1 = hashlib.sha1() + sha256 = hashlib.sha256() + with open(full_path, "rb") as f: + while True: + data = f.read(BUF_SIZE) + if not data: + break + sha1.update(data) + sha256.update(data) + update_sha1 = binascii.b2a_base64(sha1.digest(), newline=False).decode() + update_sha256 = binascii.b2a_base64(sha256.digest(), newline=False).decode() + return (update_sha1, update_sha256, update_size) + + +def get_available_version(version): + available_versions = scanUpdates() + + for device, ids in available_versions.items(): + if version in ids: + available_version = {device: ids} + + return available_version + + +def scanUpdates(): + files = os.listdir("updates") + versions = {} + + for f in files: + p = f.split("_") + if len(p) != 2: + continue + t = p[1].split(".") + if len(t) != 2: + continue + + z = t[0].split("-") + + version = p[0] + # print(version) + product = z[0] + + if product not in versions or versions[product][0] < version: + versions[product] = (version, f) + + return versions + + +class MySimpleHTTPRequestHandler(SimpleHTTPRequestHandler): + def do_POST(self): + length = int(self.headers.get("Content-Length")) + body = self.rfile.read(length).decode("utf-8") + # print(body) + print("Updating...") + xml = ET.fromstring(body) + updatecheck_node = xml.find("app/updatecheck") + + # check for update + if updatecheck_node is not None: + version = xml.attrib["version"] + platform = xml.find("os").attrib["platform"] + print("requested: ", version) + print("platform: ", platform) + + version, update_name = available_versions[platform] + + update_sha1, update_sha256, update_size = getupdateinfo( + platform, version, update_name + ) + params = { + "version": version, + "update_name": f"updates/{update_name}", + "update_sha1": update_sha1, + "update_sha256": update_sha256, + "update_size": update_size, + "codebase_url": host_url, + } + + response = response_template.format(**params) + # print("Response:") + # print(response) + self.send_response(200) + self.end_headers() + self.wfile.write(response.encode()) + return + + event_node = xml.find("app/event") + event_type = int(event_node.attrib["eventtype"]) + event_result = int(event_node.attrib["eventresult"]) + + # post install status + if event_result != 0: + print("Update downloaded, please wait for device to install...") + if "errorcode" in event_node.attrib: + print("With errorcode:", event_node.attrib["errorcode"]) + return + + # update done + if event_type == 14: + print("OK Response:") + print(response_ok) + self.send_response(200) + self.end_headers() + self.wfile.write(response_ok.encode()) + return + + +def startUpdate(versionsGiven, host, port=8080): + global available_versions + global host_url # I am aware globals are generally bad practice, but this is a quick and dirty solution + + host_url = f"http://{host}:{port}/" + available_versions = versionsGiven + + if not available_versions: + raise FileNotFoundError("Could not find any update files") + + handler = MySimpleHTTPRequestHandler + print(f"Starting fake updater at {host}:{port}") + try: + httpd = HTTPServer((host, port), handler) + except OSError: + print("Error: Could not start fake updater. Is the port already in use?") + return + httpd.serve_forever() diff --git a/codexctl/sync.py b/codexctl/sync.py index aece925..2bce4b4 100644 --- a/codexctl/sync.py +++ b/codexctl/sync.py @@ -1,215 +1,218 @@ -import requests -import os -import glob - -import logging - - -class RmWebInterfaceAPI(object): - def __init__(self, BASE="http://10.11.99.1/", logger=None): - self.logger = logger - - if self.logger is None: - self.logger = logging - - self.BASE = BASE - self.NAME_ATTRIBUTE = "VissibleName" - self.ID_ATTRIBUTE = "ID" - - self.logger.debug(f"Base is: {BASE}") - - def __POST(self, endpoint, data={}, fileUpload=False): - try: - logging.debug( - f"Sending POST request to {self.BASE + endpoint} with data {data}" - ) - - if fileUpload: - result = requests.post(self.BASE + endpoint, files=data) - else: - result = requests.post(self.BASE + endpoint, data=data) - - if result.status_code == 408: - self.logger.error("Request timed out!") - - logging.debug(f"Result headers: {result.headers}") - if "application/json" in result.headers["Content-Type"]: - return result.json() - return result.content - - except Exception: - return None - - def __get_documents_recursive( - self, folderId="", currentLocation="", currentDocuments=[] - ): - data = self.__POST(f"documents/{folderId}") - - for item in data: - self.logger.debug(f"Checking item: {item}") - - if "fileType" in item: - item["location"] = currentLocation - currentDocuments.append(item) - else: - self.logger.debug( - f"Getting documents over {item[self.ID_ATTRIBUTE]}, current location is {currentLocation}/{item[self.NAME_ATTRIBUTE]}" - ) - self.__get_documents_recursive( - item[self.ID_ATTRIBUTE], - f"{currentLocation}/{item[self.NAME_ATTRIBUTE]}", - currentDocuments, - ) - - return currentDocuments - - def __get_folder_id(self, folderName, _from=""): - results = self.__POST(f"documents/{_from}") - - if results is None: - return None - - results.reverse() # We only want folders - - for data in results: - self.logger.debug(f"Folder: {data}") - - if "fileType" in data: - return None - - if data[self.NAME_ATTRIBUTE].strip() == folderName.strip(): - return data[self.ID_ATTRIBUTE] - - self.logger.debug( - f"Getting folders over {folderName}, {data[self.ID_ATTRIBUTE]}" - ) - - recursiveResults = self.__get_folder_id(folderName, data[self.ID_ATTRIBUTE]) - if recursiveResults is None: - continue - else: - return recursiveResults - - def __get_docs(self, folderName="", recursive=True): - folderId = "" - - if folderName: - folderId = self.__get_folder_id(folderName) - - if folderId is None: - return {} - - if recursive: - self.logger.debug(f"Calling recursive function on {folderName}") - return self.__get_documents_recursive( - folderId=folderId, currentLocation=folderName - ) - - data = self.__POST(f"documents/{folderId}") - - for item in data: - item["location"] = "" - - return [item for item in data if "fileType" in item] - - def download(self, document, location="", overwrite=False): - filename = document[self.NAME_ATTRIBUTE] - if "/" in filename: - filename = filename.replace("/", "_") - - self.logger.debug(f"Downloading {filename}, location {location}") - - if not os.path.exists(location): - self.logger.debug("Download folder does not exist, creating it") - os.makedirs(location) - - try: - fileLocation = f"{location}/{filename}.pdf" - - if os.path.isfile(fileLocation) and overwrite is False: - self.logger.debug(f"Not overwriting file") - return True - - binaryData = self.__POST( - f"download/{document[self.ID_ATTRIBUTE]}/placeholder" - ) - - if isinstance(binaryData, dict): - print(f"Error trying to download {filename}: {binaryData}") - return False - - with open(fileLocation, "wb") as outFile: - outFile.write(binaryData) - - return True - - except Exception as error: - print(f"Error trying to download {filename}: {error}") - return False - - def upload(self, input_paths, remoteFolder): - folderId = "" - if remoteFolder: - folderId = self.__get_folder_id(remoteFolder) - - if folderId is None: - raise SystemExit(f"Error: Folder {remoteFolder} does not exist!") - - self.__POST(f"documents/{folderId}") # Setting up for upload... - - errors, documents = [], [] - - for document in input_paths: # This needs improvement... - if os.path.isdir(document): - for file in glob.glob(f'{document}/*'): - if not file.endswith('.pdf'): - self.logger.error(f"Error: {document} is not a pdf!") - else: - documents.append(file) - elif os.path.isfile(document): - if not document.endswith('.pdf'): - errors.append(document) - self.logger.error(f"Error: {document} is not a pdf!") - else: - documents.append(document) - else: - errors.append(document) - self.logger.error(f"Error: {document} is not a file or directory!") - - for document in documents: - self.logger.debug(f"Uploading {document} to {remoteFolder if remoteFolder else 'root'}") - with open(document, 'rb') as inFile: - response = self.__POST(f"upload", data={'file': inFile}, fileUpload=True) - - if response is None: - self.logger.error(f"Error: Unknown error while uploading {document}!") - errors.append(document) - elif response == {'status': 'Upload successful'}: - self.logger.debug(f"Uploaded {document} successfully!") - - if len(errors) > 0: - print('The following files failed to upload: ' + ','.join(errors)) - - print(f"Done! {len(documents)-len(errors)} files were uploaded.") - - - def sync(self, localFolder, remoteFolder="", overwrite=False, recursive=True): - count = 0 - - if not os.path.exists(localFolder): - self.logger.debug("Local folder does not exist, creating it") - os.mkdir(localFolder) - - documents = self.__get_docs(remoteFolder, recursive) - - if documents == {}: - print("No documents were found!") - - else: - for doc in documents: - self.logger.debug(f"Processing {doc}") - count += 1 - self.download( - doc, f"{localFolder}/{doc['location']}", overwrite=overwrite - ) - print(f"Done! {count} files were exported.") +import requests +import os +import glob + +import logging + + +class RmWebInterfaceAPI: # TODO: Add docstrings + def __init__(self, BASE="http://10.11.99.1/", logger=None): + self.logger = logger + + if self.logger is None: + self.logger = logging + + self.BASE = BASE + self.NAME_ATTRIBUTE = "VissibleName" + self.ID_ATTRIBUTE = "ID" + + self.logger.debug(f"Base is: {BASE}") + + def __POST(self, endpoint, data={}, fileUpload=False): + try: + logging.debug( + f"Sending POST request to {self.BASE + endpoint} with data {data}" + ) + + if fileUpload: + result = requests.post(self.BASE + endpoint, files=data) + else: + result = requests.post(self.BASE + endpoint, data=data) + + if result.status_code == 408: + self.logger.error("Request timed out!") + + logging.debug(f"Result headers: {result.headers}") + if "application/json" in result.headers["Content-Type"]: + return result.json() + return result.content + + except Exception: + return None + + def __get_documents_recursive( + self, folderId="", currentLocation="", currentDocuments=[] + ): + data = self.__POST(f"documents/{folderId}") + + for item in data: + self.logger.debug(f"Checking item: {item}") + + if "fileType" in item: + item["location"] = currentLocation + currentDocuments.append(item) + else: + self.logger.debug( + f"Getting documents over {item[self.ID_ATTRIBUTE]}, current location is {currentLocation}/{item[self.NAME_ATTRIBUTE]}" + ) + self.__get_documents_recursive( + item[self.ID_ATTRIBUTE], + f"{currentLocation}/{item[self.NAME_ATTRIBUTE]}", + currentDocuments, + ) + + return currentDocuments + + def __get_folder_id(self, folderName, _from=""): + results = self.__POST(f"documents/{_from}") + + if results is None: + return None + + results.reverse() # We only want folders + + for data in results: + self.logger.debug(f"Folder: {data}") + + if "fileType" in data: + return None + + if data[self.NAME_ATTRIBUTE].strip() == folderName.strip(): + return data[self.ID_ATTRIBUTE] + + self.logger.debug( + f"Getting folders over {folderName}, {data[self.ID_ATTRIBUTE]}" + ) + + recursiveResults = self.__get_folder_id(folderName, data[self.ID_ATTRIBUTE]) + if recursiveResults is None: + continue + else: + return recursiveResults + + def __get_docs(self, folderName="", recursive=True): + folderId = "" + + if folderName: + folderId = self.__get_folder_id(folderName) + + if folderId is None: + return {} + + if recursive: + self.logger.debug(f"Calling recursive function on {folderName}") + return self.__get_documents_recursive( + folderId=folderId, currentLocation=folderName + ) + + data = self.__POST(f"documents/{folderId}") + + for item in data: + item["location"] = "" + + return [item for item in data if "fileType" in item] + + def download(self, document, location="", overwrite=False): + filename = document[self.NAME_ATTRIBUTE] + if "/" in filename: + filename = filename.replace("/", "_") + + self.logger.debug(f"Downloading {filename}, location {location}") + + if not os.path.exists(location): + self.logger.debug("Download folder does not exist, creating it") + os.makedirs(location) + + try: + fileLocation = f"{location}/{filename}.pdf" + + if os.path.isfile(fileLocation) and overwrite is False: + self.logger.debug("Not overwriting file") + return True + + binaryData = self.__POST( + f"download/{document[self.ID_ATTRIBUTE]}/placeholder" + ) + + if isinstance(binaryData, dict): + print(f"Error trying to download {filename}: {binaryData}") + return False + + with open(fileLocation, "wb") as outFile: + outFile.write(binaryData) + + return True + + except Exception as error: + print(f"Error trying to download {filename}: {error}") + return False + + def upload(self, input_paths, remoteFolder): + folderId = "" + if remoteFolder: + folderId = self.__get_folder_id(remoteFolder) + + if folderId is None: + raise SystemError(f"Error: Folder {remoteFolder} does not exist!") + + self.__POST(f"documents/{folderId}") # Setting up for upload... + + errors, documents = [], [] + + for document in input_paths: # This needs improvement... + if os.path.isdir(document): + for file in glob.glob(f"{document}/*"): + if not file.endswith(".pdf"): + self.logger.error(f"Error: {document} is not a pdf!") + else: + documents.append(file) + elif os.path.isfile(document): + if not document.endswith(".pdf"): + errors.append(document) + self.logger.error(f"Error: {document} is not a pdf!") + else: + documents.append(document) + else: + errors.append(document) + self.logger.error(f"Error: {document} is not a file or directory!") + + for document in documents: + self.logger.debug( + f"Uploading {document} to {remoteFolder if remoteFolder else 'root'}" + ) + with open(document, "rb") as inFile: + response = self.__POST("upload", data={"file": inFile}, fileUpload=True) + + if response is None: + self.logger.error( + f"Error: Unknown error while uploading {document}!" + ) + errors.append(document) + elif response == {"status": "Upload successful"}: + self.logger.debug(f"Uploaded {document} successfully!") + + if len(errors) > 0: + print("The following files failed to upload: " + ",".join(errors)) + + print(f"Done! {len(documents)-len(errors)} files were uploaded.") + + def sync(self, localFolder, remoteFolder="", overwrite=False, recursive=True): + count = 0 + + if not os.path.exists(localFolder): + self.logger.debug("Local folder does not exist, creating it") + os.mkdir(localFolder) + + documents = self.__get_docs(remoteFolder, recursive) + + if documents == {}: + print("No documents were found!") + + else: + for doc in documents: + self.logger.debug(f"Processing {doc}") + count += 1 + self.download( + doc, f"{localFolder}/{doc['location']}", overwrite=overwrite + ) + print(f"Done! {count} files were exported.") diff --git a/codexctl/updates.py b/codexctl/updates.py index e50c255..7efee9a 100644 --- a/codexctl/updates.py +++ b/codexctl/updates.py @@ -1,271 +1,372 @@ -import os, time, requests, re, uuid, sys, json, hashlib - -from pathlib import Path -from datetime import datetime - -import xml.etree.ElementTree as ET - - -class UpdateManager: - def __init__(self, device_version=None, logger=None): - self.updates_url = ( - "https://updates.cloud.remarkable.engineering/service/update2" - ) - - self.logger = logger - self.DOWNLOAD_FOLDER = Path( - os.environ["XDG_DOWNLOAD_DIR"] - if ( - "XDG_DOWNLOAD_DIR" in os.environ - and os.path.exists(os.environ["XDG_DOWNLOAD_DIR"]) - ) - else Path.home() / "Downloads" - ) - self.device_version = ( - device_version if device_version else "3.2.3.1595" - ) # Earliest 3.x.x version - - self.logger.debug(f"Download folder is {self.DOWNLOAD_FOLDER}") - - versions = self.get_version_ids() - - self.id_lookups_rm1 = versions["remarkable1"] - self.id_lookups_rm2 = versions["remarkable2"] - - def update_version_ids(self, location): - with open(location, "w", newline="\n") as f: - try: - self.logger.debug("Downloading version-ids.json") - contents = requests.get( - "https://raw.githubusercontent.com/Jayy001/codexctl/main/data/version-ids.json" - ).json() - json.dump(contents, f, indent=4) - f.write("\n") - except requests.exceptions.Timeout: - raise SystemExit( - "Error: Connection timed out while downloading version-ids.json! Do you have an internet connection?" - ) - except Exception as error: - raise SystemExit( - f"Error: Unknown error while downloading version-ids.json! {error}" - ) - - def get_version_ids(self): - if os.path.exists("data/version-ids.json"): - file_location = "data/version-ids.json" - self.logger.debug("Found version-ids at data/version-ids.json") - else: - if os.name == "nt": - folder_location = os.getenv("APPDATA") + "/codexctl" - elif os.name == "posix": - folder_location = os.path.expanduser("~/.config/codexctl") - - self.logger.debug(f"Folder location is {folder_location}") - if not os.path.exists(folder_location): - os.makedirs(folder_location, exist_ok=True) - - file_location = folder_location + "/version-ids.json" - - if not os.path.exists(file_location): - self.update_version_ids(file_location) - - try: - with open(file_location) as f: - contents = json.load(f) - except ValueError: - raise SystemExit( - f"Error: version-ids.json @ {file_location} is corrupted! Please delete it and try again." - ) - - if ( - int(datetime.now().timestamp()) - contents["last-updated"] > 2628000 - ): # 1 month - self.update_version_ids(file_location) - with open(file_location) as f: - contents = json.load(f) - - self.logger.debug(f"Contents are {contents}") - return contents - - def get_toltec_version(self, device=2): - response = requests.get("https://toltec-dev.org/stable/Compatibility") - if response.status_code != 200: - raise SystemExit( - f"Error: Failed to get toltec compatibility table: {response.status_code} {response.reason}" - ) - - return self.__max_version( - [ - x.split("=")[1] - for x in response.text.splitlines() - if x.startswith(f"rm{device}=") - ] - ) - - def get_version(self, device=2, version=None, download_folder=None): - if download_folder is None: - download_folder = self.DOWNLOAD_FOLDER - - # Check if download folder exists - if not os.path.exists(download_folder): - return "Download folder does not exist" - - if device == 1: - versionDict = self.id_lookups_rm1 - else: - versionDict = self.id_lookups_rm2 - - if version is None: - version = self.get_latest_version(device) - - if version not in versionDict: - return "Not in version list" - - BASE_URL = "https://updates-download.cloud.remarkable.engineering/build/reMarkable%20Device%20Beta/RM110" - - BASE_URL_V3 = "https://updates-download.cloud.remarkable.engineering/build/reMarkable%20Device/reMarkable2" - BASE_URL_RM1_V3 = "https://updates-download.cloud.remarkable.engineering/build/reMarkable%20Device/reMarkable" - - if int(version.split(".")[0]) > 2: - if device == 1: - BASE_URL = BASE_URL_RM1_V3 - else: - BASE_URL = BASE_URL_V3 - - data = versionDict[version] - id = f"-{data[0]}" - checksum = data[1] - if len(data) > 2: - print(f"Warning for {version}: {data[2]}") - - file_name = f"{version}_reMarkable{'2' if device == 2 else ''}{id}.signed" - file_url = f"{BASE_URL}/{version}/{file_name}" - - self.logger.debug(f"File URL is {file_url}, File name is {file_name}") - return self.download_file(file_url, file_name, download_folder, checksum) - - @staticmethod - def __max_version(versions): - return sorted(versions, key=lambda v: tuple(map(int, v.split("."))))[-1] - - def get_latest_version(self, device): # Hardcoded for now - if device == 2: - return self.__max_version(self.id_lookups_rm2.keys()) - - return self.__max_version(self.id_lookups_rm1.keys()) - - def _generate_xml_data(self): # TODO: Support for remarkable1 - params = { - "installsource": "scheduler", - "requestid": str(uuid.uuid4()), - "sessionid": str(uuid.uuid4()), - "machineid": "00".zfill(32), - "oem": "RM100-753-12345", - "appid": "98DA7DF2-4E3E-4744-9DE6-EC931886ABAB", - "bootid": str(uuid.uuid4()), - "current": self.device_version, - "group": "Prod", - "platform": "reMarkable2", - } - - return """ - - - - - -""".format( - **params - ) - - def _make_request(self, data): - tries = 1 - - while tries < 3: - tries += 1 - - self.logger.debug( - f"Sending POST request to {self.updates_url} with data {data} [Try {tries}]" - ) - - response = requests.post(self.updates_url, data) - - if response.status_code == 429: - print("Too many requests sent, retrying after 20") - time.sleep(20) - - continue - - response.raise_for_status() - - break - - return response.text - - def _parse_response(self, resp): - xml_data = ET.fromstring(resp) - - if "noupdate" in resp or xml_data is None: # Is none? - return None # Or False maybe? - - file_name = xml_data.find("app/updatecheck/manifest/packages/package").attrib[ - "name" - ] - file_uri = ( - f"{xml_data.find('app/updatecheck/urls/url').attrib['codebase']}{file_name}" - ) - file_version = xml_data.find("app/updatecheck/manifest").attrib["version"] - - self.logger.debug( - f"File version is {file_version}, file uri is {file_uri}, file name is {file_name}" - ) - return file_version, file_uri, file_name - - def download_file( - self, uri, name, download_folder, checksum - ): # Credit to https://stackoverflow.com/questions/15644964/python-progress-bar-and-downloads - response = requests.get(uri, stream=True) - total_length = response.headers.get("content-length") - - self.logger.debug(f"Downloading file from {uri} to {download_folder}/{name}") - try: - total_length = int(total_length) - - if int(total_length) < 10000000: # 10MB - return None - except TypeError: - return None - - self.logger.debug(f"Total length is {total_length}") - - filename = f"{download_folder}/{name}" - with open(filename, "wb") as f: - dl = 0 - - for data in response.iter_content(chunk_size=4096): - dl += len(data) - f.write(data) - if sys.stdout.isatty(): - done = int(50 * dl / total_length) - sys.stdout.write("\r[%s%s]" % ("=" * done, " " * (50 - done))) - sys.stdout.flush() - - if sys.stdout.isatty(): - print(end="\r\n") - - self.logger.debug("Downloaded filename") - - if os.path.getsize(filename) != total_length: - os.remove(filename) - raise SystemExit("Error: File size mismatch! Is your connection stable?") - - with open(filename, "rb") as f: - file_checksum = hashlib.sha256(f.read()).hexdigest() - - if file_checksum != checksum: - os.remove(filename) - raise SystemExit( - f"Error: File checksum mismatch! Expected {checksum}, got {file_checksum}" - ) - - return name +import os +import requests +import uuid +import sys +import json +import hashlib +import logging + +from pathlib import Path +from datetime import datetime + +import xml.etree.ElementTree as ET + + +class UpdateManager: + def __init__(self, logger=None) -> None: + """Manager for downloading update versions + + Args: + logger (logger, optional): Logger object for logging. Defaults to None. + """ + + self.logger = logger + + if self.logger is None: + self.logger = logging + + ( + self.remarkablepp_versions, + self.remarkable2_versions, + self.remarkable1_versions, + self.external_provider_url, + ) = self.get_remarkable_versions() + + def get_remarkable_versions(self) -> tuple[dict, dict, dict, str, str]: + """Gets the avaliable versions for the device, by checking the local version-ids.json file and then updating it if necessary + + Returns: + tuple: A tuple containing the version ids for the remarkablepp, remarkable2, remarkable1, toltec version and external provider (in that order) + """ + + if os.path.exists("data/version-ids.json"): + file_location = "data/version-ids.json" + + self.logger.debug("Found version-ids at data/version-ids.json") + + else: + if os.name == "nt": # Windows + folder_location = os.getenv("APPDATA") + "/codexctl" + elif os.name in ("posix", "darwin"): # Linux or MacOS + folder_location = os.path.expanduser("~/.config/codexctl") + else: + raise SystemError("Unsupported OS") + + self.logger.debug(f"Version config folder location is {folder_location}") + if not os.path.exists(folder_location): + os.makedirs(folder_location, exist_ok=True) + + file_location = folder_location + "/version-ids.json" + + if not os.path.exists(file_location): + self.update_version_ids(file_location) + + try: + with open(file_location) as f: + contents = json.load(f) + except ValueError: + raise SystemError( + f"Version-ids.json @ {file_location} is corrupted! Please delete it and try again. Also, PLEASE open an issue on the repo showing the contents of the file." + ) + + if ( + int(datetime.now().timestamp()) - contents["last-updated"] + > 5256000 # 2 months + ): + self.update_version_ids(file_location) + + with open(file_location) as f: + contents = json.load(f) + + self.logger.debug(f"Version ids contents are {contents}") + + return ( + contents["remarkablepp"], + contents["remarkable2"], + contents["remarkable1"], + contents["external-provider-url"], + ) + + def update_version_ids(self, location: str) -> None: + """Updates the version-ids.json file + + Args: + location (str): Location to save the file + + Raises: + SystemExit: If the file cannot be updated + """ + with open(location, "w", newline="\n") as f: + try: + self.logger.debug("Downloading version-ids.json") + contents = requests.get( + "https://raw.githubusercontent.com/Jayy001/codexctl/main/data/version-ids.json" + ).json() + json.dump(contents, f, indent=4) + f.write("\n") + except requests.exceptions.Timeout: + raise SystemExit( + "Connection timed out while downloading version-ids.json! Do you have an internet connection?" + ) + except Exception as error: + raise SystemExit( + f"Unknown error while downloading version-ids.json! {error}" + ) + + def get_latest_version(self, device_type: str) -> str: + """Gets the latest version available for the device + + Args: + device_type (str): Type of the device (remarkablepp or remarkable2 or remarkable1) + + Returns: + str: Latest version available for the device + """ + if "1" in device_type: + versions = self.remarkable1_versions + elif "2" in device_type: + versions = self.remarkable2_versions + elif "ferrari" in device_type or "pp" in device_type: + versions = self.remarkablepp_versions + else: + return None # Explicit? + + return self.__max_version(versions.keys()) + + def get_toltec_version(self, device_type: str) -> str: + """Gets the latest version available toltec for the device + + Args: + device_type (str): Type of the device (remarkablepp or remarkable2 or remarkable1) + + Returns: + str: Latest version available for the device + """ + + if "ferrari" in device_type: + raise SystemExit("ReMarkable Paper Pro does not support toltec") + elif "1" in device_type: + device_type = "rm1" + else: + device_type = "rm2" + + response = requests.get("https://toltec-dev.org/stable/Compatibility") + if response.status_code != 200: + raise SystemExit( + f"Error: Failed to get toltec compatibility table: {response.status_code}" + ) + + return self.__max_version( + [ + x.split("=")[1] + for x in response.text.splitlines() + if x.startswith(f"{device_type}=") + ] + ) + + def download_version( + self, device_type: str, update_version: str, download_folder: str = None + ) -> str | None: + """Downloads the specified version of the update + + Args: + device_type (str): Type of the device (remarkable2 or remarkable1) + update_version (str): Id of version to download. + download_folder (str, optional): Location of download folder. Defaults to download folder for OS. + + Returns: + str | None: Location of the file if the download was successful, None otherwise + """ + + if download_folder is None: + download_folder = Path( + os.environ["XDG_DOWNLOAD_DIR"] + if ( + "XDG_DOWNLOAD_DIR" in os.environ + and os.path.exists(os.environ["XDG_DOWNLOAD_DIR"]) + ) + else Path.home() / "Downloads" + ) + + if not os.path.exists(download_folder): + self.logger.error( + f"Download folder {download_folder} does not exist! Creating it now." + ) + os.makedirs(download_folder) + + BASE_URL = "https://updates-download.cloud.remarkable.engineering/build/reMarkable%20Device%20Beta/RM110" # Default URL for v2 versions + BASE_URL_V3 = "https://updates-download.cloud.remarkable.engineering/build/reMarkable%20Device/reMarkable" + + if device_type in ("rm1", "reMarkable 1", "remarkable1"): + version_lookup = self.remarkable1_versions + elif device_type in ("rm2", "reMarkable 2", "remarkable2"): + version_lookup = self.remarkable2_versions + BASE_URL_V3 += "2" + elif device_type in ("rmpp", "rmpro", "reMarkable Ferrari", "ferrari"): + version_lookup = self.remarkablepp_versions + else: + raise SystemError("Hardware version does not exist! (rm1,rm2,rmpp)") + + if update_version not in version_lookup: + self.logger.error( + f"Version {update_version} not found in version-ids.json! Please update your version-ids.json file." + ) + return + + version_major, version_minor, version_patch, version_build = ( + update_version.split(".") + ) + version_id, version_checksum = version_lookup[update_version] + version_external = False + + if int(version_major) >= 3: + BASE_URL = BASE_URL_V3 + + if int(version_minor) >= 11: + version_external = True + + if version_external: + file_url = self.external_provider_url.replace("REPLACE_ID", version_id) + file_name = f"remarkable-production-memfault-image-{update_version}-{device_type.replace(' ', '-')}-public" + else: + file_name = f"{update_version}_reMarkable{'2' if '2' in device_type else ''}-{version_id}.signed" + file_url = f"{BASE_URL}/{update_version}/{file_name}" + + self.logger.debug(f"File URL is {file_url}, File name is {file_name}") + + return self.__download_version_file( + file_url, file_name, download_folder, version_checksum + ) + + def __generate_xml_data(self) -> str: + """Generates and returns XML data for the update request""" + params = { + "installsource": "scheduler", + "requestid": str(uuid.uuid4()), + "sessionid": str(uuid.uuid4()), + "machineid": "00".zfill(32), + "oem": "RM100-753-12345", + "appid": "98DA7DF2-4E3E-4744-9DE6-EC931886ABAB", + "bootid": str(uuid.uuid4()), + "current": "3.2.3.1595", + "group": "Prod", + "platform": "reMarkable2", + } + + return """ + + + + + +""".format( + **params + ) + + def __parse_response(self, resp: str) -> tuple[str, str, str] | None: + """Parses the response from the update server and returns the file name, uri, and version if an update is available + + Args: + resp (str): Response from the server + + Returns: + tuple[str, str, str] | None: File name, uri, and version if an update is available, None otherwise + """ + xml_data = ET.fromstring(resp) + + if "noupdate" in resp or xml_data is None: + return None + + file_name = xml_data.find("app/updatecheck/manifest/packages/package").attrib[ + "name" + ] + file_uri = ( + f"{xml_data.find('app/updatecheck/urls/url').attrib['codebase']}{file_name}" + ) + file_version = xml_data.find("app/updatecheck/manifest").attrib["version"] + + self.logger.debug( + f"File version is {file_version}, file uri is {file_uri}, file name is {file_name}" + ) + return file_version, file_uri, file_name + + def __download_version_file( + self, uri: str, name: str, download_folder: str, checksum: str + ) -> str | None: + """Downloads the version file from the server and checks the checksum + + Args: + uri (str): Location to the file + name (str): Name of the file + download_folder (str): Location of download folder + checksum (str): Sha256 Checksum of the file + + Returns: + str | None: Location of the file if the checksum matches, None otherwise + """ + response = requests.get(uri, stream=True) + file_length = response.headers.get("content-length") + + self.logger.debug(f"Downloading {name} from {uri} to {download_folder}") + try: + file_length = int(file_length) + + if int(file_length) < 10000000: # 10MB, invalid version file + self.logger.error( + f"File {name} is too small to be a valid version file" + ) + return None + except TypeError: + self.logger.error( + f"Could not get content length for {name}. Do you have an internet connection?" + ) + return None + + self.logger.debug(f"{name} is {file_length} bytes") + + filename = f"{download_folder}/{name}" + with open(filename, "wb") as out_file: + dl = 0 + + for data in response.iter_content(chunk_size=4096): + dl += len(data) + out_file.write(data) + if sys.stdout.isatty(): + done = int(50 * dl / file_length) + sys.stdout.write("\r[%s%s]" % ("=" * done, " " * (50 - done))) + sys.stdout.flush() + + if sys.stdout.isatty(): + print(end="\r\n") + + self.logger.debug(f"Downloaded {name}") + + with open(filename, "rb") as f: + file_checksum = hashlib.sha256(f.read()).hexdigest() + + if file_checksum != checksum: + os.remove(filename) + self.logger.error( + f"File checksum mismatch! Expected {checksum}, got {file_checksum}" + ) + return None + + return filename + + @staticmethod + def __max_version(versions: list) -> str: + """Returns the highest avaliable version from a list with semantic versioning""" + return sorted(versions, key=lambda v: tuple(map(int, v.split("."))))[-1] + + @staticmethod + def uses_new_update_engine(version: str) -> bool: + """ + Checks if the version given is above 3.11 and so requires the newer update engine + + Args: + version (str): version to check against + + Returns: + bool: If it uses the new update engine or not + """ + return int(version.split(".")[0]) >= 3 and int(version.split(".")[1]) >= 11 \ No newline at end of file diff --git a/data/version-ids.json b/data/version-ids.json index f41111c..f8ba9bd 100644 --- a/data/version-ids.json +++ b/data/version-ids.json @@ -1,338 +1,338 @@ -{ - "remarkable1": { - "3.15.3.1": [ - "remarkable-production-memfault-image-3.15.3.1-rm1-public.swu", - "03a6ff64df69da292ebf82286e361b95d3ddeb1958618fe1c9e302bc6403c7f4" - ], - "3.14.1.9": [ - "remarkable-production-memfault-image-3.14.1.9-rm1-public.swu", - "d2293e3395bb966708465efbf9c1d28718f8d7f2d14c95bb69c62b0153f07bae" - ], - "3.13.2.0": [ - "remarkable-production-memfault-image-3.13.2.0-rm1-public.swu", - "a0be8e1d53ea3b7dc500c8b8564ab21e14e9dbaaec665549787c444cbc428eb9" - ], - "3.13.1.2": [ - "remarkable-production-memfault-image-3.13.1.2-rm1-public.swu", - "0e7f92c46e355c2e35b45f8fa2bb5a6e3ba1191424da692fb6facac0d5233a6a" - ], - "3.12.4.4": [ - "remarkable-production-memfault-image-3.12.4.4-rm1-public.swu", - "a735ccc2175625d38a270b523ff9c1c29f801cb662da085c06af23d945152e07" - ], - "3.12.4.3": [ - "remarkable-production-memfault-image-3.12.4.3-rm1-public.swu", - "1bd3d76cb773aaa40a1cc3c745a5ed0b3313ad9147316b589853efa4d1a5865d" - ], - "3.11.3.3": [ - "remarkable-production-memfault-image-3.11.3.3-rm1-public.swu", - "37c8a4a61a59fdd6e893e75ce48b9f0283e41718b00aeebc3da81f9f7fa1006c" - ], - "3.11.2.5": [ - "ShunAN3V1W", - "2ad14c56caca60f4db19b55297519f1507a36b042c6aad3fb31fde41ba15ffb8" - ], - "3.10.2.2063": [ - "NXiSL3XohA", - "508d97e773f86fd67d1f7b6379b7b9b9bf7eb4945148286d898b1ae00f3d5c72" - ], - "3.9.5.2026": [ - "dkcJxJw3e5", - "a141cb1381660670630d2132a287470dd385e2e6ff9354bb23969b7ac64d9234" - ], - "3.9.4.2018": [ - "EptYHsKUvv", - "005ef63d04f60ea575c44843c8c5603e8c0905b3e6cc0c9f3d567ef610815f1a" - ], - "3.9.3.1986": [ - "0141xi6GBe", - "874d213c13aa6fadcc6b018efdc224ce1a7b826174058db474d30d87a587dc84" - ], - "3.8.2.1965": [ - "ENRo0iD1yG", - "d395bbb1282ad9b8719c59bbfac888e9598bb2143570a8b31f687e745a0c4163" - ], - "3.6.1.1894": [ - "CGaIOXfzAA", - "2f1f36d9895fad68ef06ecef1d70065fddca4311d3c5adcfb17f365197e09216" - ], - "3.6.0.1865": [ - "nnuJzg6Jj4-", - "d5df978f1240dff73b71849d1c225afb0868adcea072547254d87191eeaa9573" - ], - "3.5.2.1807": [ - "UGWiACaUG0-", - "d80e4d8c7a2fb54c0d4a900b5d7124a18d33f1aec1039627a8086a54b5caed35" - ], - "3.5.1.1798": [ - "cR2nzMvbcW-", - "8a9ac4e80e175681d83b627ca8d68a49223a88910a781be8c2e9bd3a29980cdb" - ], - "3.4.1.1790": [ - "SgjzSG56eK-", - "6e885bb4ecf5fc9bb2c64403a0dadcb5c41237c12e8f1c8f148a82c067a07b66" - ], - "3.4.0.1784": [ - "yY5ri2SbLv-", - "a1baaa6eb3ecafba54030d9cb22ee6c915a277c8cd3b288a57c80fc72a112876" - ], - "3.3.2.1666": [ - "Ak1sdSSGWD-", - "9f1ed453241901ecbbae9bc62a567b5cea1df10cc69d9be38ebb1e56a88f8d68" - ], - "3.2.3.1595": [ - "f99Lsgx3BG-", - "f7ae13c24cbf6259f48b6434167f2060e1543ccfbefd00608097f4c85ee5b3bb" - ], - "3.2.2.1581": [ - "rsTV6U3FlH-", - "6a7bcf1dfef61bed9c72da9627f32a0ea27c166479322c88b55819fddc4293cb" - ], - "3.0.4.1305": [ - "zo0ubu5Wle-", - "3ec0f158f98a481dadee47c18c310a8efdefd3c41c1c7f259507c617464db6a9" - ], - "2.15.1.1189": [ - "Xdvv3lBmE4-", - "ef4529e3f11e17cffeaad203cc7d3704ecdfdc0bc7f5a81a7bd092a6edc99b3a" - ], - "2.15.0.1067": [ - "qbGuCuFIX7-", - "490b82d23ef415523ae5df5141216f60af2f6fe9a47f2e4fa0c7546dc81720b2" - ], - "2.14.3.977": [ - "uzKCb761HZ-", - "ca3a5e501467e58ac6adbd384525d1ad3d91701a001ba2e3d9c53e298f60a9b3" - ], - "2.14.3.958": [ - "DgsusjCrfd-", - "d2330b5051fec82df3d06722ec6e6b61a863040f407694aadcfb77c60663afb2" - ], - "2.14.3.1047": [ - "mQ3OMMnn0v-", - "55be77d62b587af7116fff9564669e5f7efa33780d375667491e990e2778713d" - ], - "2.14.3.1005": [ - "nKHKu5aVAR-", - "f539e5499e223d1463ed7e0a1c557b8bcf3bcddf352a4701dfd33fa7a7df9f6f" - ], - "2.14.1.866": [ - "kPA6NrLNoo-", - "8f1df03a057d98839eab2b769455330d51b483a4bfde3831379a66adbc426dea" - ], - "2.14.0.861": [ - "r6qAtH141a-", - "657ecfcf86820e28b9f636a77947609ce9d52141c40a9529313f1a543eaab41d" - ], - "2.13.0.758": [ - "K7IjoRHW9V-", - "79f3ff86b0d4f8ab2bdc7f11bf5e8721ef0ebf84acace1910f2b2d79849c8d33" - ], - "2.12.3.606": [ - "DtbTMBso9w-", - "70108b45de2033fa42dd3c5ac915743b6fb312ac546b6232b5e3419b7723435a" - ], - "2.12.2.573": [ - "F7KCxN9Zpp-", - "76705e3a00cd87489aaa52df7e9b98e1002b64ce3916a2379d2ce68725bc2ba0" - ], - "2.12.1.527": [ - "xKvumVvxqC-", - "d76304a2a0e1c27433ff627461a3af1032c902cc508edc2614df815f2395d02e" - ], - "2.11.0.442": [ - "3QEYXIWu4Z-", - "b9398f5c9ef82a8f9394dc2fec6d1fe30fef0458ec927c492a54f88849760b17" - ], - "2.10.3.379": [ - "2UgGBK40nD-", - "52c44ec69148190771b72bbc56b3b96d543ecd2b2ff8c57ed0a2be095a5aae28" - ], - "2.10.2.356": [ - "Lp90j3g4at-", - "d12390956ceea52a3cf6c8e412e8e489c2b4ef262f9cba66695bfc43d7cb1dff" - ] - }, - "remarkable2": { - "3.14.1.9": [ - "remarkable-production-memfault-image-3.14.1.9-rm2-public.swu", - "f3394019dbfec3628eec1aee4502d00bd8af1921ed2bcd9edfa01221e445df32" - ], - "3.13.2.0": [ - "remarkable-production-memfault-image-3.13.2.0-rm2-public.swu", - "331be43421a3a655a5b886c583917341c9912f0aa056d3475b83906dfa9720f5" - ], - "3.13.1.2": [ - "remarkable-production-memfault-image-3.13.1.2-rm2-public.swu", - "cbd3fc3de77b152aa04e4098b621b52de0f1b053d3652f7d54e14b654711f80c" - ], - "3.12.4.4": [ - "remarkable-production-memfault-image-3.12.4.4-rm2-public.swu", - "a15448464e34c868674134b656707823aac305e6b7e1fe1882d3d60182947088" - ], - "3.12.4.3": [ - "remarkable-production-memfault-image-3.12.4.3-rm2-public.swu", - "a1780b93e27f226e03496b41419acd53eb8caf531c4768d7675f2589ae419aaa" - ], - "3.11.3.3": [ - "remarkable-production-memfault-image-3.11.3.3-rm2-public.swu", - "cb4a598c575acbab2a4480b13521a97def846337a46b856feb778a190f9d5b47" - ], - "3.11.2.5": [ - "qLFGoqPtPL", - "a86afd842c6f1e5b33f4bbc10ec8360ce1789ab1c6caef75324619e0ab859cdb" - ], - "3.10.2.2063": [ - "zKnOgdh8c5", - "db0da1138106d62f01e533919fe9f4800207361b1ecbf34732cbfe7020291d54" - ], - "3.9.5.2026": [ - "7Mtxk3FUms", - "e91232517986d58faffb4ff0cbd034f5a2f5e7af9920b43d12fd86e7bf510051" - ], - "3.9.4.2018": [ - "dg5knKZECj", - "3907334416f57e574c60d0dc9f8b116da9c869da7b04a866d848f08f39fe1399" - ], - "3.9.3.1986": [ - "T1fuxwo40r", - "f2270150ce2c838c5217f0bf75f43402cb65895d52b3ea555cc61d587a57b5ab" - ], - "3.8.3.1976": [ - "M795riZ0Z8", - "ca14b36305f88376d8da99b0e143d7bad1997aa08fd5eb832bd9532421ef4839" - ], - "3.8.2.1965": [ - "D1pFxylu6p", - "6505e93c7e1815f001761d10225812fa534bdb44baefae8143167b6db4d907f6" - ], - "3.8.0.1944": [ - "7eGpAv7sYB", - "b286ce36371687cbfc55ce2b269659b347f1daec6993058a35d569b91f4ec75d" - ], - "3.7.0.1930": [ - "XSMSQgBATy", - "dc9975dca40826f6dd53e6790056a0128dd7c76dbb956001dad0c56d4c408a5f" - ], - "3.6.1.1894": [ - "T2dkdktE1H", - "1b759556c16269cba957897e943d871e8ba6528fc3b2a3a025f05594346abce0" - ], - "3.6.0.1865": [ - "7wgexMSZP5-", - "f90e1973cfca6e40fdb018c29aa194df501fa1016a7f3ce6b11e17fa05ceb36b" - ], - "3.5.2.1807": [ - "3bZjC0Xn5C-", - "98c2609aac772af18df8083b2d1c46bcf7ee048192ff650a982c5046044127f4" - ], - "3.5.1.1798": [ - "9CfoVp8qCU-", - "71ef0398e1b2fef3bd7188472b7a6e5a8e503105bad51a0b8e253f0c179881b3" - ], - "3.4.1.1790": [ - "rYfHxYmwC8-", - "034d76a872476eabffeb5edc1b1d4f2dc183a3677500f3ba454fe384dbe4fb54" - ], - "3.4.0.1784": [ - "fD3GCOcU9m-", - "6e3e5783ebfdcb634ee69b8e826646cb51ea243a468f9bd615037889a8e5d53d" - ], - "3.3.2.1666": [ - "ihUirIf133-", - "aa19d53094f5c620d9ac8d594befd27c0a7e7f9a2ab44f6bf2b4637860ac7cb6" - ], - "3.2.3.1595": [ - "fTtpld3Mvn-", - "9e04a52f2a92cbe5cbe704bac7182b7e0c5575cb64eac3b3b86da3c17e818083" - ], - "3.2.2.1581": [ - "dwyp884gLP-", - "4e4765140524f9926c40ac32c02bf404bfffda3d05ca5f3becdebfd9de77b57a" - ], - "3.0.4.1305": [ - "tdwbuIzhYP-", - "dbfa0ae483b0f9fe4b8f96a809b05c95615e294607af600fc3dc75b01a2ed8ab" - ], - "2.15.1.1189": [ - "wVbHkgKisg-", - "07dc873072e7fe52e6c4d508efe70bdb463dcd4bfe6b9165ba3d5d259776d04c" - ], - "2.15.0.1067": [ - "lyC7KAjjSB-", - "ada1a2a62f955d5c598d12bbd30d651076c01a536f99b045e79f83e9e4acea98" - ], - "2.14.3.977": [ - "joPqAABTAW-", - "59d4e024a145703fa0325ffff3f600711a4af323c50655b9343c348056d73ebb" - ], - "2.14.3.958": [ - "B7yhC887i1-", - "97bf2d4a68b448f43948d08b0542795909420b8dd87ca8ba4aa15d58f6f37abe" - ], - "2.14.3.1047": [ - "RGLmy8Jb39-", - "0bc58296187211b0efe9b171e76882ad060ceed9f6d3e3a6cf07224fd56732ed" - ], - "2.14.3.1005": [ - "4HCZ7J2Bse-", - "74c384bbe68f114e7a8ac780104f57f7ffd7cebba4e101271d235cd45bc96ac7" - ], - "2.14.1.866": [ - "JLWa2mnXu1-", - "c87490101ca3b817ca58dfdf472e363edf4ef670362c1321ca595b41c87aebe9" - ], - "2.14.0.861": [ - "TfpToxrexR-", - "efc9b184def5ba95120a2d989cdd90549703b62829c98d49f5cf4ad38219615d" - ], - "2.13.0.758": [ - "2N5B5nvpZ4-", - "9d5dcd475b65f31295bd4247ab149984a572eb226c29c8e907de56aa4ad7330b" - ], - "2.12.3.606": [ - "XOSYryyJXI-", - "93819d75de3b4f79a271d022698456ed08ceda9b463992ec191fb2827c265b0f" - ], - "2.12.2.573": [ - "XnE1EL7ojK-", - "9e7eaf89b9973e90a48c722ba208322f6e62ce54f1343c2b98fc1ac2b823ceca" - ], - "2.12.1.527": [ - "8OkCxOJFn9-", - "0bdebf6c60b9634f3df645db286d690585ab55b803a50b0fb9636d91c5853f26" - ], - "2.11.0.442": [ - "tJDDwsDM4V-", - "1aeeb086be0b22bc9e8e3b966b9d29ca50e0d4561c29bb6e496ed7ff2c24ea27" - ], - "2.10.3.379": [ - "n16KxgSfCk-", - "9e68e89b4128318094113f11d4accc650e43c8a66dc33dd68f762e1d4de33804" - ], - "2.10.2.356": [ - "JLB6Ax3hnJ-", - "abde0fac3d12f7599a167414e2871fd340fe10312bc5cb1b65af958b4f5f0736" - ] - }, - "remarkablepp": { - "3.15.4.2": [ - "remarkable-ct-prototype-image-3.15.4.2-ferrari-public.swu", - "e0db4681888d2294c768906e7a564bc06d936dbb5e9fefc1529cf7a438dae628" - ], - "3.14.4.0": [ - "remarkable-ct-prototype-image-3.14.4.0-ferrari-public.swu", - "4c93cbc85c061520421c71a4b99ec30ef25e41c077e40d542e068db272900a1e" - ], - "3.14.3.0": [ - "remarkable-ct-prototype-image-3.14.3.0-ferrari-public.swu", - "ad1c28c9031f0b14a6a897b50ccfd402e6c0711d5d129edd8cfa03879d473073" - ], - "3.14.1.10": [ - "remarkable-ct-prototype-image-3.14.1.10-ferrari-public.swu", - "8c92f589900e7e355697206c71e2256d909313fcd96aa2c5fd9910ff04b062f1" - ] - }, - "last-updated": 1733663974, - "external-provider-url": "https://storage.googleapis.com/remarkable-versions/REPLACE_ID" -} +{ + "remarkable1": { + "3.15.3.1": [ + "remarkable-production-memfault-image-3.15.3.1-rm1-public.swu", + "03a6ff64df69da292ebf82286e361b95d3ddeb1958618fe1c9e302bc6403c7f4" + ], + "3.14.1.9": [ + "remarkable-production-memfault-image-3.14.1.9-rm1-public.swu", + "d2293e3395bb966708465efbf9c1d28718f8d7f2d14c95bb69c62b0153f07bae" + ], + "3.13.2.0": [ + "remarkable-production-memfault-image-3.13.2.0-rm1-public.swu", + "a0be8e1d53ea3b7dc500c8b8564ab21e14e9dbaaec665549787c444cbc428eb9" + ], + "3.13.1.2": [ + "remarkable-production-memfault-image-3.13.1.2-rm1-public.swu", + "0e7f92c46e355c2e35b45f8fa2bb5a6e3ba1191424da692fb6facac0d5233a6a" + ], + "3.12.4.4": [ + "remarkable-production-memfault-image-3.12.4.4-rm1-public.swu", + "a735ccc2175625d38a270b523ff9c1c29f801cb662da085c06af23d945152e07" + ], + "3.12.4.3": [ + "remarkable-production-memfault-image-3.12.4.3-rm1-public.swu", + "1bd3d76cb773aaa40a1cc3c745a5ed0b3313ad9147316b589853efa4d1a5865d" + ], + "3.11.3.3": [ + "remarkable-production-memfault-image-3.11.3.3-rm1-public.swu", + "37c8a4a61a59fdd6e893e75ce48b9f0283e41718b00aeebc3da81f9f7fa1006c" + ], + "3.11.2.5": [ + "ShunAN3V1W", + "2ad14c56caca60f4db19b55297519f1507a36b042c6aad3fb31fde41ba15ffb8" + ], + "3.10.2.2063": [ + "NXiSL3XohA", + "508d97e773f86fd67d1f7b6379b7b9b9bf7eb4945148286d898b1ae00f3d5c72" + ], + "3.9.5.2026": [ + "dkcJxJw3e5", + "a141cb1381660670630d2132a287470dd385e2e6ff9354bb23969b7ac64d9234" + ], + "3.9.4.2018": [ + "EptYHsKUvv", + "005ef63d04f60ea575c44843c8c5603e8c0905b3e6cc0c9f3d567ef610815f1a" + ], + "3.9.3.1986": [ + "0141xi6GBe", + "874d213c13aa6fadcc6b018efdc224ce1a7b826174058db474d30d87a587dc84" + ], + "3.8.2.1965": [ + "ENRo0iD1yG", + "d395bbb1282ad9b8719c59bbfac888e9598bb2143570a8b31f687e745a0c4163" + ], + "3.6.1.1894": [ + "CGaIOXfzAA", + "2f1f36d9895fad68ef06ecef1d70065fddca4311d3c5adcfb17f365197e09216" + ], + "3.6.0.1865": [ + "nnuJzg6Jj4-", + "d5df978f1240dff73b71849d1c225afb0868adcea072547254d87191eeaa9573" + ], + "3.5.2.1807": [ + "UGWiACaUG0-", + "d80e4d8c7a2fb54c0d4a900b5d7124a18d33f1aec1039627a8086a54b5caed35" + ], + "3.5.1.1798": [ + "cR2nzMvbcW-", + "8a9ac4e80e175681d83b627ca8d68a49223a88910a781be8c2e9bd3a29980cdb" + ], + "3.4.1.1790": [ + "SgjzSG56eK-", + "6e885bb4ecf5fc9bb2c64403a0dadcb5c41237c12e8f1c8f148a82c067a07b66" + ], + "3.4.0.1784": [ + "yY5ri2SbLv-", + "a1baaa6eb3ecafba54030d9cb22ee6c915a277c8cd3b288a57c80fc72a112876" + ], + "3.3.2.1666": [ + "Ak1sdSSGWD-", + "9f1ed453241901ecbbae9bc62a567b5cea1df10cc69d9be38ebb1e56a88f8d68" + ], + "3.2.3.1595": [ + "f99Lsgx3BG-", + "f7ae13c24cbf6259f48b6434167f2060e1543ccfbefd00608097f4c85ee5b3bb" + ], + "3.2.2.1581": [ + "rsTV6U3FlH-", + "6a7bcf1dfef61bed9c72da9627f32a0ea27c166479322c88b55819fddc4293cb" + ], + "3.0.4.1305": [ + "zo0ubu5Wle-", + "3ec0f158f98a481dadee47c18c310a8efdefd3c41c1c7f259507c617464db6a9" + ], + "2.15.1.1189": [ + "Xdvv3lBmE4-", + "ef4529e3f11e17cffeaad203cc7d3704ecdfdc0bc7f5a81a7bd092a6edc99b3a" + ], + "2.15.0.1067": [ + "qbGuCuFIX7-", + "490b82d23ef415523ae5df5141216f60af2f6fe9a47f2e4fa0c7546dc81720b2" + ], + "2.14.3.977": [ + "uzKCb761HZ-", + "ca3a5e501467e58ac6adbd384525d1ad3d91701a001ba2e3d9c53e298f60a9b3" + ], + "2.14.3.958": [ + "DgsusjCrfd-", + "d2330b5051fec82df3d06722ec6e6b61a863040f407694aadcfb77c60663afb2" + ], + "2.14.3.1047": [ + "mQ3OMMnn0v-", + "55be77d62b587af7116fff9564669e5f7efa33780d375667491e990e2778713d" + ], + "2.14.3.1005": [ + "nKHKu5aVAR-", + "f539e5499e223d1463ed7e0a1c557b8bcf3bcddf352a4701dfd33fa7a7df9f6f" + ], + "2.14.1.866": [ + "kPA6NrLNoo-", + "8f1df03a057d98839eab2b769455330d51b483a4bfde3831379a66adbc426dea" + ], + "2.14.0.861": [ + "r6qAtH141a-", + "657ecfcf86820e28b9f636a77947609ce9d52141c40a9529313f1a543eaab41d" + ], + "2.13.0.758": [ + "K7IjoRHW9V-", + "79f3ff86b0d4f8ab2bdc7f11bf5e8721ef0ebf84acace1910f2b2d79849c8d33" + ], + "2.12.3.606": [ + "DtbTMBso9w-", + "70108b45de2033fa42dd3c5ac915743b6fb312ac546b6232b5e3419b7723435a" + ], + "2.12.2.573": [ + "F7KCxN9Zpp-", + "76705e3a00cd87489aaa52df7e9b98e1002b64ce3916a2379d2ce68725bc2ba0" + ], + "2.12.1.527": [ + "xKvumVvxqC-", + "d76304a2a0e1c27433ff627461a3af1032c902cc508edc2614df815f2395d02e" + ], + "2.11.0.442": [ + "3QEYXIWu4Z-", + "b9398f5c9ef82a8f9394dc2fec6d1fe30fef0458ec927c492a54f88849760b17" + ], + "2.10.3.379": [ + "2UgGBK40nD-", + "52c44ec69148190771b72bbc56b3b96d543ecd2b2ff8c57ed0a2be095a5aae28" + ], + "2.10.2.356": [ + "Lp90j3g4at-", + "d12390956ceea52a3cf6c8e412e8e489c2b4ef262f9cba66695bfc43d7cb1dff" + ] + }, + "remarkable2": { + "3.14.1.9": [ + "remarkable-production-memfault-image-3.14.1.9-rm2-public.swu", + "f3394019dbfec3628eec1aee4502d00bd8af1921ed2bcd9edfa01221e445df32" + ], + "3.13.2.0": [ + "remarkable-production-memfault-image-3.13.2.0-rm2-public.swu", + "331be43421a3a655a5b886c583917341c9912f0aa056d3475b83906dfa9720f5" + ], + "3.13.1.2": [ + "remarkable-production-memfault-image-3.13.1.2-rm2-public.swu", + "cbd3fc3de77b152aa04e4098b621b52de0f1b053d3652f7d54e14b654711f80c" + ], + "3.12.4.4": [ + "remarkable-production-memfault-image-3.12.4.4-rm2-public.swu", + "a15448464e34c868674134b656707823aac305e6b7e1fe1882d3d60182947088" + ], + "3.12.4.3": [ + "remarkable-production-memfault-image-3.12.4.3-rm2-public.swu", + "a1780b93e27f226e03496b41419acd53eb8caf531c4768d7675f2589ae419aaa" + ], + "3.11.3.3": [ + "remarkable-production-memfault-image-3.11.3.3-rm2-public.swu", + "cb4a598c575acbab2a4480b13521a97def846337a46b856feb778a190f9d5b47" + ], + "3.11.2.5": [ + "qLFGoqPtPL", + "a86afd842c6f1e5b33f4bbc10ec8360ce1789ab1c6caef75324619e0ab859cdb" + ], + "3.10.2.2063": [ + "zKnOgdh8c5", + "db0da1138106d62f01e533919fe9f4800207361b1ecbf34732cbfe7020291d54" + ], + "3.9.5.2026": [ + "7Mtxk3FUms", + "e91232517986d58faffb4ff0cbd034f5a2f5e7af9920b43d12fd86e7bf510051" + ], + "3.9.4.2018": [ + "dg5knKZECj", + "3907334416f57e574c60d0dc9f8b116da9c869da7b04a866d848f08f39fe1399" + ], + "3.9.3.1986": [ + "T1fuxwo40r", + "f2270150ce2c838c5217f0bf75f43402cb65895d52b3ea555cc61d587a57b5ab" + ], + "3.8.3.1976": [ + "M795riZ0Z8", + "ca14b36305f88376d8da99b0e143d7bad1997aa08fd5eb832bd9532421ef4839" + ], + "3.8.2.1965": [ + "D1pFxylu6p", + "6505e93c7e1815f001761d10225812fa534bdb44baefae8143167b6db4d907f6" + ], + "3.8.0.1944": [ + "7eGpAv7sYB", + "b286ce36371687cbfc55ce2b269659b347f1daec6993058a35d569b91f4ec75d" + ], + "3.7.0.1930": [ + "XSMSQgBATy", + "dc9975dca40826f6dd53e6790056a0128dd7c76dbb956001dad0c56d4c408a5f" + ], + "3.6.1.1894": [ + "T2dkdktE1H", + "1b759556c16269cba957897e943d871e8ba6528fc3b2a3a025f05594346abce0" + ], + "3.6.0.1865": [ + "7wgexMSZP5-", + "f90e1973cfca6e40fdb018c29aa194df501fa1016a7f3ce6b11e17fa05ceb36b" + ], + "3.5.2.1807": [ + "3bZjC0Xn5C-", + "98c2609aac772af18df8083b2d1c46bcf7ee048192ff650a982c5046044127f4" + ], + "3.5.1.1798": [ + "9CfoVp8qCU-", + "71ef0398e1b2fef3bd7188472b7a6e5a8e503105bad51a0b8e253f0c179881b3" + ], + "3.4.1.1790": [ + "rYfHxYmwC8-", + "034d76a872476eabffeb5edc1b1d4f2dc183a3677500f3ba454fe384dbe4fb54" + ], + "3.4.0.1784": [ + "fD3GCOcU9m-", + "6e3e5783ebfdcb634ee69b8e826646cb51ea243a468f9bd615037889a8e5d53d" + ], + "3.3.2.1666": [ + "ihUirIf133-", + "aa19d53094f5c620d9ac8d594befd27c0a7e7f9a2ab44f6bf2b4637860ac7cb6" + ], + "3.2.3.1595": [ + "fTtpld3Mvn-", + "9e04a52f2a92cbe5cbe704bac7182b7e0c5575cb64eac3b3b86da3c17e818083" + ], + "3.2.2.1581": [ + "dwyp884gLP-", + "4e4765140524f9926c40ac32c02bf404bfffda3d05ca5f3becdebfd9de77b57a" + ], + "3.0.4.1305": [ + "tdwbuIzhYP-", + "dbfa0ae483b0f9fe4b8f96a809b05c95615e294607af600fc3dc75b01a2ed8ab" + ], + "2.15.1.1189": [ + "wVbHkgKisg-", + "07dc873072e7fe52e6c4d508efe70bdb463dcd4bfe6b9165ba3d5d259776d04c" + ], + "2.15.0.1067": [ + "lyC7KAjjSB-", + "ada1a2a62f955d5c598d12bbd30d651076c01a536f99b045e79f83e9e4acea98" + ], + "2.14.3.977": [ + "joPqAABTAW-", + "59d4e024a145703fa0325ffff3f600711a4af323c50655b9343c348056d73ebb" + ], + "2.14.3.958": [ + "B7yhC887i1-", + "97bf2d4a68b448f43948d08b0542795909420b8dd87ca8ba4aa15d58f6f37abe" + ], + "2.14.3.1047": [ + "RGLmy8Jb39-", + "0bc58296187211b0efe9b171e76882ad060ceed9f6d3e3a6cf07224fd56732ed" + ], + "2.14.3.1005": [ + "4HCZ7J2Bse-", + "74c384bbe68f114e7a8ac780104f57f7ffd7cebba4e101271d235cd45bc96ac7" + ], + "2.14.1.866": [ + "JLWa2mnXu1-", + "c87490101ca3b817ca58dfdf472e363edf4ef670362c1321ca595b41c87aebe9" + ], + "2.14.0.861": [ + "TfpToxrexR-", + "efc9b184def5ba95120a2d989cdd90549703b62829c98d49f5cf4ad38219615d" + ], + "2.13.0.758": [ + "2N5B5nvpZ4-", + "9d5dcd475b65f31295bd4247ab149984a572eb226c29c8e907de56aa4ad7330b" + ], + "2.12.3.606": [ + "XOSYryyJXI-", + "93819d75de3b4f79a271d022698456ed08ceda9b463992ec191fb2827c265b0f" + ], + "2.12.2.573": [ + "XnE1EL7ojK-", + "9e7eaf89b9973e90a48c722ba208322f6e62ce54f1343c2b98fc1ac2b823ceca" + ], + "2.12.1.527": [ + "8OkCxOJFn9-", + "0bdebf6c60b9634f3df645db286d690585ab55b803a50b0fb9636d91c5853f26" + ], + "2.11.0.442": [ + "tJDDwsDM4V-", + "1aeeb086be0b22bc9e8e3b966b9d29ca50e0d4561c29bb6e496ed7ff2c24ea27" + ], + "2.10.3.379": [ + "n16KxgSfCk-", + "9e68e89b4128318094113f11d4accc650e43c8a66dc33dd68f762e1d4de33804" + ], + "2.10.2.356": [ + "JLB6Ax3hnJ-", + "abde0fac3d12f7599a167414e2871fd340fe10312bc5cb1b65af958b4f5f0736" + ] + }, + "remarkablepp": { + "3.15.4.2": [ + "remarkable-ct-prototype-image-3.15.4.2-ferrari-public.swu", + "e0db4681888d2294c768906e7a564bc06d936dbb5e9fefc1529cf7a438dae628" + ], + "3.14.4.0": [ + "remarkable-ct-prototype-image-3.14.4.0-ferrari-public.swu", + "4c93cbc85c061520421c71a4b99ec30ef25e41c077e40d542e068db272900a1e" + ], + "3.14.3.0": [ + "remarkable-ct-prototype-image-3.14.3.0-ferrari-public.swu", + "ad1c28c9031f0b14a6a897b50ccfd402e6c0711d5d129edd8cfa03879d473073" + ], + "3.14.1.10": [ + "remarkable-ct-prototype-image-3.14.1.10-ferrari-public.swu", + "8c92f589900e7e355697206c71e2256d909313fcd96aa2c5fd9910ff04b062f1" + ] + }, + "last-updated": 1733663974, + "external-provider-url": "https://storage.googleapis.com/remarkable-versions/REPLACE_ID" +} \ No newline at end of file diff --git a/codexctl.py b/main.py similarity index 100% rename from codexctl.py rename to main.py diff --git a/media/demoLocal.gif b/media/demoLocal.gif deleted file mode 100644 index 57863eb..0000000 Binary files a/media/demoLocal.gif and /dev/null differ diff --git a/media/demoRemote.gif b/media/demoRemote.gif deleted file mode 100644 index a826511..0000000 Binary files a/media/demoRemote.gif and /dev/null differ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..75bc1f3 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,24 @@ +[tool.poetry] +name = "codexctl" +version = "1.0.0" +description = "Automated update managment for the ReMarkable tablet" +authors = ["Jayy001 "] +license = "GPLv3" +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.12" +paramiko = "3.4.1" +psutil = "6.0.0" +requests = "2.31.0" +loguru = "0.7.2" +remarkable-update-image = { version = "1.1.3", markers = "sys_platform != 'linux'" } +remarkable-update-fuse = { version = "1.1.2", markers = "sys_platform == 'linux'" } + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry.scripts] +codexctl = "codexctl.__main__:main" +cxtl = "codexctl.__main__:main" diff --git a/requirements.remote.txt b/requirements.remote.txt index 8eff4b0..69bf03f 100644 --- a/requirements.remote.txt +++ b/requirements.remote.txt @@ -1,3 +1,3 @@ -r requirements.txt paramiko==3.4.1 -psutil==6.0.0 +psutil==6.0.0 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e638594..ad2cab5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ requests==2.31.0 loguru==0.7.2 remarkable-update-image==1.1.3; sys_platform != 'linux' -remarkable-update-fuse==1.2.2; sys_platform == 'linux' +remarkable-update-fuse==1.2.2; sys_platform == 'linux' \ No newline at end of file diff --git a/scripts/build.sh b/scripts/build.sh old mode 100755 new mode 100644 index 463d2df..e371f37 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -12,4 +12,4 @@ python -m PyInstaller \ --runtime-tmpdir /tmp \ --onefile \ --strip \ - codexctl.py + main.py \ No newline at end of file diff --git a/github-make-executable.sh b/scripts/github-make-executable.sh old mode 100755 new mode 100644 similarity index 69% rename from github-make-executable.sh rename to scripts/github-make-executable.sh index 202b343..b8bd6dd --- a/github-make-executable.sh +++ b/scripts/github-make-executable.sh @@ -5,16 +5,17 @@ make executable 2>&1 \ | while read -r line; do IFS=$'\n' read -r -a lines <<< "$line" if [[ "$line" == 'Nuitka'*':ERROR:'* ]] || [[ "$line" == 'FATAL:'* ]] || [[ "$line" == 'make: *** ['*'] Error'* ]] ; then - printf '::error file=codexctl.py,title=Nuitka Error::%s\n' "${lines[@]}" + printf '::error file=main.py,title=Nuitka Error::%s\n' "${lines[@]}" elif [[ "$line" == 'Nuitka'*':WARNING:'* ]]; then - printf '::warning file=codexctl.py,title=Nuitka Warning::%s\n' "${lines[@]}" + printf '::warning file=main.py,title=Nuitka Warning::%s\n' "${lines[@]}" elif [[ "$line" == 'Nuitka:INFO:'* ]] || [[ "$line" == '[info]'* ]]; then echo "$line" else printf '::debug::%s\n' "${lines[@]}" fi done + if ! make test-executable; then - printf '::error file=codexctl.bin,title=Test Error::Sanity test failed\n' + printf '::error file=codexctl,title=Test Error::Sanity test failed\n' exit 1 fi diff --git a/scripts/install_build_tools.sh b/scripts/install_build_tools.sh old mode 100755 new mode 100644 diff --git a/scripts/run_build.sh b/scripts/run_build.sh old mode 100755 new mode 100644 diff --git a/test.py b/tests/test.py similarity index 75% rename from test.py rename to tests/test.py index dd4eece..13a596a 100644 --- a/test.py +++ b/tests/test.py @@ -1,191 +1,197 @@ -import os -import sys -import difflib -import contextlib -import codexctl - -from collections import namedtuple -from io import StringIO -from io import BytesIO - -FAILED = False -UPDATE_FILE_PATH = ".venv/2.15.1.1189_reMarkable2-wVbHkgKisg-.signed" - -assert os.path.exists(UPDATE_FILE_PATH), "Update image missing" - - -class BufferWriter: - def __init__(self, buffer): - self._buffer = buffer - - def write(self, data): - self._buffer.write(data) - - -class BufferBytesIO(BytesIO): - @property - def buffer(self): - return BufferWriter(self) - - -def assert_value(msg, value, expected): - global FAILED - print(f"Testing {msg}: ", end="") - if value == expected: - print("pass") - return - - FAILED = True - print("fail") - print(f" {value} != {expected}") - - -def assert_gt(msg, value, expected): - global FAILED - print(f"Testing {msg}: ", end="") - if value >= expected: - print("pass") - return - - FAILED = True - print("fail") - print(f" {value} != {expected}") - - -def test_set_server_config(original, expected): - global FAILED - print("Testing set_server_config: ", end="") - result = codexctl.set_server_config(original, "test") - if result == expected: - print("pass") - return - - FAILED = True - print("fail") - for diff in difflib.ndiff( - expected.splitlines(keepends=True), result.splitlines(keepends=True) - ): - print(f" {diff}") - - -def test_ls(path, expected): - global FAILED - global UPDATE_FILE_PATH - print(f"Testing ls {path}: ", end="") - with contextlib.redirect_stdout(StringIO()) as f: - Args = namedtuple("Args", "file target_path") - try: - codexctl.do_ls(Args(file=UPDATE_FILE_PATH, target_path=path)) - - except SystemExit: - pass - - result = f.getvalue() - if result == expected: - print("pass") - return - - FAILED = True - print("fail") - for diff in difflib.ndiff( - expected.splitlines(keepends=True), result.splitlines(keepends=True) - ): - print(f" {diff}") - - -def test_cat(path, expected): - global FAILED - global UPDATE_FILE_PATH - print(f"Testing ls {path}: ", end="") - with contextlib.redirect_stdout(BufferBytesIO()) as f: - Args = namedtuple("Args", "file target_path") - try: - codexctl.do_cat(Args(file=UPDATE_FILE_PATH, target_path=path)) - - except SystemExit: - pass - - result = f.getvalue() - if result == expected: - print("pass") - return - - FAILED = True - print("fail") - for diff in difflib.ndiff( - expected.decode("utf-8").splitlines(keepends=True), - result.decode("utf-8").splitlines(keepends=True), - ): - print(f" {diff}") - - -test_set_server_config( - "", - "SERVER=test\n", -) - -test_set_server_config( - """[General] -#REMARKABLE_RELEASE_APPID={98DA7DF2-4E3E-4744-9DE6-EC931886ABAB} -#SERVER=https://updates.cloud.remarkable.engineering/service/update2 -#GROUP=Prod -#PLATFORM=reMarkable2 -REMARKABLE_RELEASE_VERSION=3.9.5.2026 -""", - """[General] -SERVER=test -#REMARKABLE_RELEASE_APPID={98DA7DF2-4E3E-4744-9DE6-EC931886ABAB} -#SERVER=https://updates.cloud.remarkable.engineering/service/update2 -#GROUP=Prod -#PLATFORM=reMarkable2 -REMARKABLE_RELEASE_VERSION=3.9.5.2026 -""", -) - -test_set_server_config( - """[General] -SERVER=testing -#REMARKABLE_RELEASE_APPID={98DA7DF2-4E3E-4744-9DE6-EC931886ABAB} -#SERVER=https://updates.cloud.remarkable.engineering/service/update2 -#GROUP=Prod -#PLATFORM=reMarkable2 -REMARKABLE_RELEASE_VERSION=3.9.5.2026 -""", - """[General] -SERVER=test -#SERVER=testing -#REMARKABLE_RELEASE_APPID={98DA7DF2-4E3E-4744-9DE6-EC931886ABAB} -#SERVER=https://updates.cloud.remarkable.engineering/service/update2 -#GROUP=Prod -#PLATFORM=reMarkable2 -REMARKABLE_RELEASE_VERSION=3.9.5.2026 -""", -) - -test_ls( - "/", - ". .. lost+found bin boot dev etc home lib media mnt postinst proc run sbin sys tmp uboot-postinst uboot-version usr var\n", -) -test_ls( - "/mnt", - ". ..\n", -) - -test_cat("/etc/version", b"20221026104022\n") - -codexctl.updateman = codexctl.UpdateManager(logger=codexctl.logger) -assert_value("latest rm1 version", codexctl.version_lookup("latest", 1), "3.11.2.5") -assert_value("latest rm2 version", codexctl.version_lookup("latest", 2), "3.11.2.5") -assert_gt( - "toltec rm1 version", - tuple(map(int, codexctl.version_lookup("toltec", 1).split("."))), - (3, 3, 2), -) -assert_gt( - "toltec rm2 version", - tuple(map(int, codexctl.version_lookup("toltec", 2).split("."))), - (3, 3, 2), -) - -if FAILED: - sys.exit(1) +import os +import sys +import difflib +import contextlib +import logging + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) + +from codexctl.device import DeviceManager +from codexctl.updates import UpdateManager +from codexctl import Manager + +set_server_config = DeviceManager().set_server_config +codexctl = Manager(device="reMarkable2", logger=logging.getLogger(__name__)) +updater = UpdateManager(logger=logging.getLogger(__name__)) + +from io import StringIO +from io import BytesIO + +FAILED = False +UPDATE_FILE_PATH = ".venv/2.15.1.1189_reMarkable2-wVbHkgKisg-.signed" + +assert os.path.exists(UPDATE_FILE_PATH), "Update image missing" + +class BufferWriter: + def __init__(self, buffer): + self._buffer = buffer + + def write(self, data): + self._buffer.write(data) + + +class BufferBytesIO(BytesIO): + @property + def buffer(self): + return BufferWriter(self) + + +def assert_value(msg, value, expected): + global FAILED + print(f"Testing {msg}: ", end="") + if value == expected: + print("pass") + return + + FAILED = True + print("fail") + print(f" {value} != {expected}") + + +def assert_gt(msg, value, expected): + global FAILED + print(f"Testing {msg}: ", end="") + if value >= expected: + print("pass") + return + + FAILED = True + print("fail") + print(f" {value} != {expected}") + + +def test_set_server_config(original, expected): + global FAILED + print("Testing set_server_config: ", end="") + result = set_server_config(original, "test") + if result == expected: + print("pass") + return + + FAILED = True + print("fail") + for diff in difflib.ndiff( + expected.splitlines(keepends=True), result.splitlines(keepends=True) + ): + print(f" {diff}") + + +def test_ls(path, expected): + global FAILED + global UPDATE_FILE_PATH + print(f"Testing ls {path}: ", end="") + with contextlib.redirect_stdout(StringIO()) as f: + try: + codexctl.call_func("ls", {'file': UPDATE_FILE_PATH, 'target_path': path}) + + except SystemExit: + pass + + result = f.getvalue() + if result == expected: + print("pass") + return + + FAILED = True + print("fail") + for diff in difflib.ndiff( + expected.splitlines(keepends=True), result.splitlines(keepends=True) + ): + print(f" {diff}") + + +def test_cat(path, expected): + global FAILED + global UPDATE_FILE_PATH + print(f"Testing cat {path}: ", end="") + with contextlib.redirect_stdout(BufferBytesIO()) as f: + try: + codexctl.call_func("cat", {'file': UPDATE_FILE_PATH, 'target_path': path}) + except SystemExit: + pass + + result = f.getvalue() + if result == expected: + print("pass") + return + + FAILED = True + print("fail") + for diff in difflib.ndiff( + expected.decode("utf-8").splitlines(keepends=True), + result.decode("utf-8").splitlines(keepends=True), + ): + print(f" {diff}") + + +test_set_server_config( + "", + "SERVER=test\n", +) + +test_set_server_config( + """[General] +#REMARKABLE_RELEASE_APPID={98DA7DF2-4E3E-4744-9DE6-EC931886ABAB} +#SERVER=https://updates.cloud.remarkable.engineering/service/update2 +#GROUP=Prod +#PLATFORM=reMarkable2 +REMARKABLE_RELEASE_VERSION=3.9.5.2026 +""", + """[General] +SERVER=test +#REMARKABLE_RELEASE_APPID={98DA7DF2-4E3E-4744-9DE6-EC931886ABAB} +#SERVER=https://updates.cloud.remarkable.engineering/service/update2 +#GROUP=Prod +#PLATFORM=reMarkable2 +REMARKABLE_RELEASE_VERSION=3.9.5.2026 +""", +) + +test_set_server_config( + """[General] +SERVER=testing +#REMARKABLE_RELEASE_APPID={98DA7DF2-4E3E-4744-9DE6-EC931886ABAB} +#SERVER=https://updates.cloud.remarkable.engineering/service/update2 +#GROUP=Prod +#PLATFORM=reMarkable2 +REMARKABLE_RELEASE_VERSION=3.9.5.2026 +""", + """[General] +SERVER=test +#SERVER=testing +#REMARKABLE_RELEASE_APPID={98DA7DF2-4E3E-4744-9DE6-EC931886ABAB} +#SERVER=https://updates.cloud.remarkable.engineering/service/update2 +#GROUP=Prod +#PLATFORM=reMarkable2 +REMARKABLE_RELEASE_VERSION=3.9.5.2026 +""", +) + +test_ls( + "/", + ". .. lost+found bin boot dev etc home lib media mnt postinst proc run sbin sys tmp uboot-postinst uboot-version usr var\n", +) +test_ls( + "/mnt", + ". ..\n", +) + +test_cat("/etc/version", b"20221026104022\n") + +# assert_value("latest rm1 version", updater.get_latest_version("reMarkable 1"), "3.11.2.5") +# assert_value("latest rm2 version", updater.get_latest_version("reMarkable 2"), "3.11.2.5") +# Don't think this test is needed. + +assert_gt( + "toltec rm1 version", + updater.get_toltec_version("reMarkable 1"), + "3.3.2.1666" +) +assert_gt( + "toltec rm2 version", + updater.get_toltec_version("reMarkable 2"), + "3.3.2.1666" +) + +if FAILED: + sys.exit(1)