codexctl 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
codexctl/__init__.py ADDED
@@ -0,0 +1,462 @@
1
+ ### Importing required general modules
2
+
3
+ import argparse
4
+ import os.path
5
+ import sys
6
+ import logging
7
+ import importlib.util
8
+ import tempfile
9
+ import shutil
10
+ import json
11
+ import re
12
+
13
+ from os import listdir
14
+
15
+ try:
16
+ from loguru import logger
17
+ except ImportError:
18
+ logger = logging.getLogger(__name__)
19
+
20
+ if importlib.util.find_spec("requests") is None:
21
+ raise ImportError(
22
+ "Requests is required for accessing remote files. Please install it."
23
+ )
24
+
25
+ from .updates import UpdateManager
26
+
27
+
28
+ class Manager:
29
+ """
30
+ Main class for codexctl
31
+ """
32
+
33
+ def __init__(self, device: str, logger: logging.Logger) -> None:
34
+ """Initializes the Manager class for codexctl
35
+
36
+ Args:
37
+ device (str): Type of device that is running the script
38
+ logger (logger): Logger object
39
+ """
40
+ self.device = device
41
+ self.logger = logger
42
+ self.updater = UpdateManager(logger)
43
+
44
+ def call_func(self, function: str, args: dict) -> None:
45
+ """Runs a command based on the function name and arguments provided
46
+
47
+ Args:
48
+ function: The function to run
49
+ args: What arguments to pass into the function
50
+ """
51
+
52
+ if "remarkable" not in self.device:
53
+ remarkable_version = args.get("hardware")
54
+ else:
55
+ remarkable_version = self.device
56
+
57
+ version = args.get("version", None)
58
+
59
+ if version == "latest":
60
+ version = self.updater.get_latest_version(remarkable_version)
61
+ elif version == "toltec":
62
+ version = self.updater.get_toltec_version(remarkable_version)
63
+
64
+ ### Download functionalities
65
+ if function == "list":
66
+ print(
67
+ f"""
68
+ ReMarkable Paper Pro:
69
+ {'\n'.join(list(self.updater.remarkablepp_versions.keys()))}
70
+
71
+ ReMarkable 2:
72
+ {'\n'.join(list(self.updater.remarkable2_versions.keys()))}
73
+
74
+ ReMarkable 1:
75
+ {'\n'.join(list(self.updater.remarkable1_versions.keys()))}
76
+ """
77
+ )
78
+
79
+ elif function == "download":
80
+ logger.debug(f"Downloading version {version}")
81
+ filename = self.updater.download_version(
82
+ remarkable_version, version, args["out"]
83
+ )
84
+
85
+ if filename:
86
+ print(f"Sucessfully downloaded to {filename}")
87
+
88
+ ### Mounting functionalities
89
+ elif function in ("extract", "mount"):
90
+ try:
91
+ from .analysis import get_update_image
92
+ except ImportError:
93
+ raise ImportError(
94
+ "remarkable_update_image is required for analysis. Please install it!"
95
+ )
96
+
97
+ if function == "extract":
98
+ if sys.platform != "linux":
99
+ raise NotImplementedError
100
+
101
+ if not args["out"]:
102
+ args["out"] = os.getcwd() + "/extracted"
103
+
104
+ logger.debug(f"Extracting {args['file']} to {args['out']}")
105
+ image, volume = get_update_image(args["file"])
106
+ image.seek(0)
107
+
108
+ with open(args["out"], "wb") as f:
109
+ f.write(image.read())
110
+ else:
111
+ if args["out"] is None:
112
+ args["out"] = "/opt/remarkable/"
113
+
114
+ if not os.path.exists(args["out"]):
115
+ os.mkdir(args["out"])
116
+
117
+ if not os.path.exists(args["filesystem"]):
118
+ raise SystemExit("Firmware file does not exist!")
119
+
120
+ from remarkable_update_fuse import UpdateFS
121
+
122
+ server = UpdateFS()
123
+ server.parse(
124
+ args=[args["filesystem"], args["out"]], values=server, errex=1
125
+ )
126
+ server.main()
127
+
128
+ ### Analysis functionalities
129
+ elif function in ("cat", "ls"):
130
+ try:
131
+ from .analysis import get_update_image
132
+ except ImportError:
133
+ raise ImportError(
134
+ "remarkable_update_image is required for analysis. Please install it. (Linux only!)"
135
+ )
136
+
137
+ try:
138
+ image, volume = get_update_image(args.file)
139
+ inode = volume.inode_at(args.target_path)
140
+
141
+ except FileNotFoundError:
142
+ print(f"'{args.target_path}': No such file or directory")
143
+ raise FileNotFoundError
144
+
145
+ except OSError:
146
+ print(f"'{args.target_path}': {os.strerror(e.errno)}")
147
+ sys.exit(e.errno)
148
+
149
+ if function == "cat":
150
+ sys.stdout.buffer.write(inode.open().read())
151
+
152
+ elif function == "ls":
153
+ print(" ".join([x.name_str for x, _ in inode.opendir()]))
154
+
155
+ ### WebInterface functionalities
156
+ elif function in ("backup", "upload"):
157
+ from .sync import RmWebInterfaceAPI
158
+
159
+ print(
160
+ "Please make sure the web-interface is enabled in the remarkable settings!\nStarting upload"
161
+ )
162
+
163
+ rmWeb = RmWebInterfaceAPI(BASE="http://10.11.99.1/", logger=logger)
164
+
165
+ if function == "backup":
166
+ rmWeb.sync(
167
+ localFolder=args["local"],
168
+ remoteFolder=args["remote"],
169
+ recursive=not args["no_recursion"],
170
+ overwrite=not args["no_overwrite"],
171
+ )
172
+ else:
173
+ rmWeb.upload(input_paths=args["paths"], remoteFolder=args["remote"])
174
+
175
+ ### Update & Version functionalities
176
+ elif function in ("install", "status", "restore"):
177
+ remote = False
178
+
179
+ if "remarkable" not in self.device:
180
+ if importlib.util.find_spec("paramiko") is None:
181
+ raise ImportError(
182
+ "Paramiko is required for SSH access. Please install it."
183
+ )
184
+ if importlib.util.find_spec("psutil") is None:
185
+ raise ImportError(
186
+ "Psutil is required for SSH access. Please install it."
187
+ )
188
+ remote = True
189
+
190
+ from .device import DeviceManager
191
+ from .server import get_available_version
192
+
193
+ remarkable = DeviceManager(
194
+ remote=remote,
195
+ address=args["address"],
196
+ logger=self.logger,
197
+ authentication=args["password"],
198
+ )
199
+
200
+ if function == "status":
201
+ beta, prev, current, version_id = remarkable.get_device_status()
202
+ print(
203
+ f"\nCurrent version: {current}\nOld update engine: {prev}\nBeta active: {beta}\nVersion id: {version_id}"
204
+ )
205
+
206
+ elif function == "restore":
207
+ remarkable.restore_previous_version()
208
+ print(
209
+ f"Device restored to previous version [{remarkable.get_device_status()[1]}]"
210
+ )
211
+ remarkable.reboot_device()
212
+ print("Device rebooted")
213
+
214
+ else:
215
+ temp_path = None
216
+ made_update_folder = False
217
+ orig_cwd = os.getcwd()
218
+
219
+ # Do we have a specific update file to serve?
220
+
221
+ update_file = version if os.path.isfile(version) else None
222
+
223
+ version_lookup = lambda version: re.search(r'\b\d+\.\d+\.\d+\.\d+\b', version)
224
+ version_number = version_lookup(version)
225
+
226
+ if not version_number:
227
+ version_number = input("Failed to get the version number from the filename, please enter it: ")
228
+ if not version_lookup(version_number):
229
+ raise SystemError("Invalid version!")
230
+
231
+ version_number = version_number.group()
232
+
233
+ update_file_requires_new_engine = UpdateManager.uses_new_update_engine(
234
+ version_number
235
+ )
236
+ device_version_uses_new_engine = UpdateManager.uses_new_update_engine(
237
+ remarkable.get_device_status()[2]
238
+ )
239
+
240
+ #### PREVENT USERS FROM INSTALLING NON-COMPATIBLE IMAGES ####
241
+
242
+ # TODO: Downgrade from versions above 3.11 to versions below 3.11 (We alredy know how to do this with #71)
243
+ # TODO: Upgrade from versions below 3.11 to versions above 3.11 (Easy way: upgrade to 3.11.2.5 to get the new update engine, then upgrade again to the specific version)
244
+
245
+ if device_version_uses_new_engine != update_file_requires_new_engine:
246
+ raise SystemError(
247
+ "Incompatible update file with current reMarkable update engine. See #71"
248
+ )
249
+
250
+ #############################################################
251
+
252
+ if not update_file_requires_new_engine:
253
+ if not os.path.exists("updates"):
254
+ os.mkdir("updates")
255
+ if update_file:
256
+ shutil.move(update_file, "updates")
257
+ update_file = get_available_version(version)
258
+ made_update_folder = True # Delete at end
259
+
260
+ # If version was a valid location file, update_file will be the location.
261
+
262
+ if not update_file:
263
+ temp_path = tempfile.mkdtemp()
264
+ os.chdir(temp_path)
265
+
266
+ print(f"Version {version} not found. Attempting to download")
267
+
268
+ location = "./"
269
+ if not update_file_requires_new_engine:
270
+ location += "updates"
271
+
272
+ result = self.updater.download_version(
273
+ remarkable.hardware, version, location
274
+ )
275
+ if result:
276
+ print(f"Downloaded version {version} to {result}")
277
+
278
+ if device_version_uses_new_engine:
279
+ update_file = result
280
+ else:
281
+ update_file = get_available_version(version)
282
+
283
+ else:
284
+ raise SystemExit(
285
+ f"Failed to download version {version}! Does this version or location exist?"
286
+ )
287
+
288
+ if device_version_uses_new_engine:
289
+ remarkable.install_sw_update(update_file)
290
+ else:
291
+ remarkable.install_ohma_update(update_file)
292
+
293
+ if made_update_folder: # Move update file back out
294
+ shutil.move(os.listdir("updates")[0], "../")
295
+ shutil.rmtree("updates")
296
+
297
+ os.chdir(orig_cwd)
298
+ if temp_path:
299
+ logger.debug(f"Removing temporary folder {temp_path}")
300
+ shutil.rmtree(temp_path)
301
+
302
+
303
+ def main() -> None:
304
+ """Main function for codexctl"""
305
+
306
+ ### Setting up the argument parser
307
+ parser = argparse.ArgumentParser("Codexctl")
308
+ parser.add_argument(
309
+ "--verbose",
310
+ "-v",
311
+ required=False,
312
+ help="Enable verbose logging",
313
+ action="store_true",
314
+ )
315
+ parser.add_argument(
316
+ "--address",
317
+ "-a",
318
+ required=False,
319
+ help="Specify the address of the device",
320
+ default=None,
321
+ )
322
+ parser.add_argument(
323
+ "--password",
324
+ "-p",
325
+ required=False,
326
+ help="Specify password or path to SSH key for remote access",
327
+ )
328
+ subparsers = parser.add_subparsers(dest="command")
329
+ subparsers.required = True # This fixes a bug with older versions of python
330
+
331
+ ### Install subcommand
332
+ install = subparsers.add_parser(
333
+ "install",
334
+ help="Install the specified version (will download if not available on the device)",
335
+ )
336
+ install.add_argument("version", help="Version (or location to file) to install")
337
+
338
+ ### Download subcommand
339
+ download = subparsers.add_parser(
340
+ "download", help="Download the specified version firmware file"
341
+ )
342
+ download.add_argument("version", help="Version to download")
343
+ download.add_argument("--out", "-o", help="Folder to download to", default=None)
344
+ download.add_argument(
345
+ "--hardware", "--h", help="Hardware to download for", required=True
346
+ )
347
+
348
+ ### Backup subcommand
349
+ backup = subparsers.add_parser(
350
+ "backup", help="Download remote files to local directory"
351
+ )
352
+ backup.add_argument(
353
+ "-r",
354
+ "--remote",
355
+ help="Remote directory to backup. Defaults to download folder",
356
+ default="",
357
+ )
358
+ backup.add_argument(
359
+ "-l",
360
+ "--local",
361
+ help="Local directory to backup to. Defaults to download folder",
362
+ default="./",
363
+ )
364
+ backup.add_argument(
365
+ "-nr",
366
+ "--no-recursion",
367
+ help="Disables recursively backup remote directory",
368
+ action="store_true",
369
+ )
370
+ backup.add_argument(
371
+ "-no-ow", "--no-overwrite", help="Disables overwrite", action="store_true"
372
+ )
373
+
374
+ ### Cat subcommand
375
+ cat = subparsers.add_parser(
376
+ "cat", help="Cat the contents of a file inside an update image"
377
+ )
378
+ cat.add_argument("file", help="Path to update file to cat", default=None)
379
+ cat.add_argument("target_path", help="Path inside the image to list", default=None)
380
+
381
+ ### Ls subcommand
382
+ ls = subparsers.add_parser("ls", help="List files inside an update image")
383
+ ls.add_argument("file", help="Path to update file to extract", default=None)
384
+ ls.add_argument("target_path", help="Path inside the image to list", default=None)
385
+
386
+ ### Extract subcommand
387
+ extract = subparsers.add_parser(
388
+ "extract", help="Extract the specified version update file"
389
+ )
390
+ extract.add_argument("file", help="Path to update file to extract", default=None)
391
+ extract.add_argument("--out", help="Folder to extract to", default=None)
392
+
393
+ ### Mount subcommand
394
+ mount = subparsers.add_parser(
395
+ "mount", help="Mount the specified version firmware filesystem"
396
+ )
397
+ mount.add_argument(
398
+ "filesystem",
399
+ help="Path to version firmware filesystem to extract",
400
+ default=None,
401
+ )
402
+ mount.add_argument("--out", help="Folder to mount to", default=None)
403
+
404
+ ### Upload subcommand
405
+ upload = subparsers.add_parser(
406
+ "upload", help="Upload folder/files to device (pdf only)"
407
+ )
408
+ upload.add_argument(
409
+ "paths", help="Path to file(s)/folder to upload", default=None, nargs="+"
410
+ )
411
+ upload.add_argument(
412
+ "-r",
413
+ "--remote",
414
+ help="Remote directory to upload to. Defaults to root folder",
415
+ default="",
416
+ )
417
+
418
+ ### Status subcommand
419
+ subparsers.add_parser(
420
+ "status", help="Get the current version of the device and other information"
421
+ )
422
+
423
+ ### Restore subcommand
424
+ subparsers.add_parser(
425
+ "restore", help="Restores to previous version installed on device"
426
+ )
427
+
428
+ ### List subcommand
429
+ subparsers.add_parser("list", help="List all available versions")
430
+
431
+ ### Setting logging level
432
+ args = parser.parse_args()
433
+ logging_level, paramiko_level = (
434
+ ("DEBUG", logging.DEBUG) if args.verbose else ("ERROR", logging.ERROR)
435
+ )
436
+
437
+ try:
438
+ logger.remove()
439
+ logger.add(sys.stderr, level=logging_level)
440
+ logging.basicConfig(level=paramiko_level)
441
+ except AttributeError: # For non-loguru
442
+ logger.level = logging_level
443
+
444
+ ### Detecting device information
445
+ device = None
446
+
447
+ if os.path.exists("/sys/devices/soc0/machine"):
448
+ with open("/sys/devices/soc0/machine") as machine_file:
449
+ contents = machine_file.read().strip()
450
+
451
+ if "reMarkable" in contents:
452
+ device = contents # reMarkable 1, reMarkable 2, reMarkable Ferrari
453
+
454
+ if device is None:
455
+ device = sys.platform
456
+
457
+ logger.debug(f"Running on platform: {device}")
458
+ logger.debug(f"Running with args: {args}")
459
+
460
+ ### Call function
461
+ man = Manager(device, logger)
462
+ man.call_func(args.command, vars(args))
codexctl/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from . import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
codexctl/analysis.py ADDED
@@ -0,0 +1,30 @@
1
+ import ext4
2
+ from remarkable_update_image import UpdateImage
3
+ from remarkable_update_image import UpdateImageSignatureException
4
+
5
+
6
+ def get_update_image(file: str):
7
+ """Extracts files from an update image (<3.11 currently)"""
8
+
9
+ image = UpdateImage(file)
10
+ volume = ext4.Volume(image, offset=0)
11
+ try:
12
+ inode = volume.inode_at("/usr/share/update_engine/update-payload-key.pub.pem")
13
+ if inode is None:
14
+ raise FileNotFoundError()
15
+
16
+ inode.verify()
17
+ image.verify(inode.open().read())
18
+
19
+ except UpdateImageSignatureException:
20
+ warnings.warn("Signature doesn't match contents", RuntimeWarning)
21
+
22
+ except FileNotFoundError:
23
+ warnings.warn("Public key missing", RuntimeWarning)
24
+
25
+ except OSError as e:
26
+ if e.errno != errno.ENOTDIR:
27
+ raise
28
+ warnings.warn("Unable to open public key", RuntimeWarning)
29
+
30
+ return image, volume