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)