Skip to content

Commit be75774

Browse files
authored
Merge pull request #159 from rmitchellscott/fix-on-device
fix: on-device installs, rmpp 3.20 downgrade regression. Fixes #158
2 parents 6204258 + 87ded47 commit be75774

2 files changed

Lines changed: 147 additions & 85 deletions

File tree

codexctl/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ def version_lookup(version: str | None) -> re.Match[str] | None:
261261
if update_file:
262262
try:
263263
from remarkable_update_image import UpdateImage
264-
from remarkable_update_image.cpio import UpdateImage as CPIOUpdateImage
264+
from remarkable_update_image.image import CPIOUpdateImage
265265

266266
image = UpdateImage(update_file)
267267
if isinstance(image, CPIOUpdateImage):

codexctl/device.py

Lines changed: 146 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import logging
33
import os
44
import re
5+
import shlex
56
import socket
67
import subprocess
78
import tempfile
@@ -291,11 +292,11 @@ def connect_to_device(
291292

292293
return client
293294

294-
def _read_version_from_path(self, ftp, base_path: str = "") -> tuple[str, bool]:
295+
def _read_version_from_path(self, ftp=None, base_path: str = "") -> tuple[str, bool]:
295296
"""Reads version from a given path (current partition or mounted backup)
296297
297298
Args:
298-
ftp: SFTP client connection
299+
ftp: SFTP client connection (None for local file access)
299300
base_path: Base path prefix (empty for current partition, /tmp/mount_pX for backup)
300301
301302
Returns:
@@ -304,76 +305,133 @@ def _read_version_from_path(self, ftp, base_path: str = "") -> tuple[str, bool]:
304305
update_conf_path = f"{base_path}/usr/share/remarkable/update.conf" if base_path else "/usr/share/remarkable/update.conf"
305306
os_release_path = f"{base_path}/etc/os-release" if base_path else "/etc/os-release"
306307

307-
def file_exists(path: str) -> bool:
308-
try:
309-
ftp.stat(path)
310-
return True
311-
except FileNotFoundError:
312-
return False
308+
if ftp:
309+
def file_exists(path: str) -> bool:
310+
try:
311+
ftp.stat(path)
312+
return True
313+
except FileNotFoundError:
314+
return False
315+
316+
def read_file(path: str) -> str:
317+
with ftp.file(path) as file:
318+
return file.read().decode("utf-8")
319+
else:
320+
file_exists = os.path.exists
321+
322+
def read_file(path: str) -> str:
323+
with open(path, encoding="utf-8") as file:
324+
return file.read()
313325

314326
if file_exists(update_conf_path):
315-
with ftp.file(update_conf_path) as file:
316-
contents = file.read().decode("utf-8").strip("\n")
317-
match = re.search("(?<=REMARKABLE_RELEASE_VERSION=).*", contents)
318-
if match:
319-
return match.group(), True
320-
raise SystemError(f"REMARKABLE_RELEASE_VERSION not found in {update_conf_path}")
327+
contents = read_file(update_conf_path).strip("\n")
328+
match = re.search("(?<=REMARKABLE_RELEASE_VERSION=).*", contents)
329+
if match:
330+
return match.group(), True
331+
raise SystemError(f"REMARKABLE_RELEASE_VERSION not found in {update_conf_path}")
321332

322333
if file_exists(os_release_path):
323-
with ftp.file(os_release_path) as file:
324-
contents = file.read().decode("utf-8")
325-
match = re.search("(?<=IMG_VERSION=).*", contents)
326-
if match:
327-
return match.group().strip('"'), False
328-
raise SystemError(f"IMG_VERSION not found in {os_release_path}")
334+
contents = read_file(os_release_path)
335+
match = re.search("(?<=IMG_VERSION=).*", contents)
336+
if match:
337+
return match.group().strip('"'), False
338+
raise SystemError(f"IMG_VERSION not found in {os_release_path}")
329339

330340
raise SystemError(f"Cannot read version from {base_path or 'current partition'}: no version file found")
331341

332-
def _get_backup_partition_version(self) -> str:
333-
"""Gets the version installed on the backup (inactive) partition
342+
def _get_active_device(self) -> str:
343+
"""Gets the active root device path.
334344
335345
Returns:
336-
str: Version string
346+
str: Active device path (e.g., /dev/mmcblk2p2)
337347
338348
Raises:
339-
SystemError: If backup partition version cannot be determined
349+
SystemError: If command fails or returns no output
340350
"""
341-
if not self.client:
342-
raise SystemError("Cannot get backup partition version: no SSH client connection")
343-
344-
ftp = self.client.open_sftp()
345-
346351
if self.hardware in (HardwareType.RMPP, HardwareType.RMPPM):
347-
_stdin, stdout, _stderr = self.client.exec_command("swupdate -g")
348-
active_device = stdout.read().decode("utf-8").strip()
349-
active_part = int(active_device.split('p')[-1])
350-
inactive_part = 3 if active_part == 2 else 2
351-
device_base = re.sub(r'p\d+$', '', active_device)
352+
cmd = "swupdate -g"
352353
else:
353-
_stdin, stdout, _stderr = self.client.exec_command("rootdev")
354-
active_device = stdout.read().decode("utf-8").strip()
355-
active_part = int(active_device.split('p')[-1])
356-
inactive_part = 3 if active_part == 2 else 2
357-
device_base = re.sub(r'p\d+$', '', active_device)
354+
cmd = "rootdev"
358355

359-
mount_point = f"/tmp/mount_p{inactive_part}"
356+
if self.client:
357+
_stdin, stdout, stderr = self.client.exec_command(cmd)
358+
output = stdout.read().decode("utf-8").strip()
359+
exit_status = stdout.channel.recv_exit_status()
360+
if exit_status != 0 or not output:
361+
error = stderr.read().decode("utf-8", errors="ignore")
362+
raise SystemError(f"Failed to get active device using '{cmd}': {error or 'no output'}")
363+
return output
364+
else:
365+
result = subprocess.run(cmd.split(), capture_output=True, text=True)
366+
if result.returncode != 0 or not result.stdout.strip():
367+
raise SystemError(f"Failed to get active device using '{cmd}': {result.stderr or 'no output'}")
368+
return result.stdout.strip()
360369

361-
self.client.exec_command(f"mkdir -p {mount_point}")
362-
_stdin, stdout, _stderr = self.client.exec_command(
363-
f"mount -o ro {device_base}p{inactive_part} {mount_point}"
364-
)
365-
exit_status = stdout.channel.recv_exit_status()
370+
def _parse_partition_info(self, active_device: str) -> tuple[int, int, str]:
371+
"""Parse partition numbers from device path.
372+
373+
Args:
374+
active_device: Device path (e.g., /dev/mmcblk2p2)
375+
376+
Returns:
377+
tuple: (active_part, inactive_part, device_base)
378+
"""
379+
active_part = int(active_device.split('p')[-1])
380+
inactive_part = 3 if active_part == 2 else 2
381+
device_base = re.sub(r'p\d+$', '', active_device)
382+
return active_part, inactive_part, device_base
366383

367-
if exit_status != 0:
368-
error_msg = _stderr.read().decode('utf-8')
369-
raise SystemError(f"Failed to mount backup partition: {error_msg}")
384+
def _get_backup_partition_version(self) -> str:
385+
"""Gets the version installed on the backup (inactive) partition
386+
387+
Returns:
388+
str: Version string (empty string for RM1/RM2 on failure)
370389
390+
Raises:
391+
SystemError: If backup partition version cannot be determined (Paper Pro only)
392+
"""
371393
try:
372-
version, _ = self._read_version_from_path(ftp, mount_point)
373-
return version
374-
finally:
375-
self.client.exec_command(f"umount {mount_point}")
376-
self.client.exec_command(f"rm -rf {mount_point}")
394+
active_device = self._get_active_device()
395+
_, inactive_part, device_base = self._parse_partition_info(active_device)
396+
mount_point = f"/tmp/mount_p{inactive_part}"
397+
398+
if self.client:
399+
ftp = self.client.open_sftp()
400+
self.client.exec_command(f"mkdir -p {mount_point}")
401+
_stdin, stdout, _stderr = self.client.exec_command(
402+
f"mount -o ro {device_base}p{inactive_part} {mount_point}"
403+
)
404+
exit_status = stdout.channel.recv_exit_status()
405+
406+
if exit_status != 0:
407+
error_msg = _stderr.read().decode('utf-8')
408+
raise SystemError(f"Failed to mount backup partition: {error_msg}")
409+
410+
try:
411+
version, _ = self._read_version_from_path(ftp, mount_point)
412+
return version
413+
finally:
414+
self.client.exec_command(f"umount {mount_point}")
415+
self.client.exec_command(f"rm -rf {mount_point}")
416+
else:
417+
os.makedirs(mount_point, exist_ok=True)
418+
result = subprocess.run(
419+
["mount", "-o", "ro", f"{device_base}p{inactive_part}", mount_point],
420+
capture_output=True, text=True
421+
)
422+
if result.returncode != 0:
423+
raise SystemError(f"Failed to mount backup partition: {result.stderr}")
424+
425+
try:
426+
version, _ = self._read_version_from_path(base_path=mount_point)
427+
return version
428+
finally:
429+
subprocess.run(["umount", mount_point])
430+
subprocess.run(["rm", "-rf", mount_point])
431+
except SystemError:
432+
if self.hardware in (HardwareType.RMPP, HardwareType.RMPPM):
433+
raise
434+
return ""
377435

378436
def _get_paper_pro_partition_info(self, current_version: str) -> tuple[int, int, int]:
379437
"""Gets partition information for Paper Pro devices
@@ -384,13 +442,8 @@ def _get_paper_pro_partition_info(self, current_version: str) -> tuple[int, int,
384442
Returns:
385443
tuple: (current_partition, inactive_partition, next_boot_partition)
386444
"""
387-
if not self.client:
388-
raise SystemError("SSH client required for partition detection")
389-
390-
_stdin, stdout, _stderr = self.client.exec_command("swupdate -g")
391-
active_device = stdout.read().decode("utf-8").strip()
392-
current_part = int(active_device.split('p')[-1])
393-
inactive_part = 3 if current_part == 2 else 2
445+
active_device = self._get_active_device()
446+
current_part, inactive_part, _ = self._parse_partition_info(active_device)
394447

395448
parts = current_version.split('.')
396449
if len(parts) >= 2 and parts[0].isdigit() and parts[1].isdigit():
@@ -400,20 +453,42 @@ def _get_paper_pro_partition_info(self, current_version: str) -> tuple[int, int,
400453

401454
next_boot_part = current_part
402455

456+
if self.client:
457+
ftp = self.client.open_sftp()
458+
459+
def file_exists(path: str) -> bool:
460+
try:
461+
ftp.stat(path)
462+
return True
463+
except FileNotFoundError:
464+
return False
465+
466+
def read_file(path: str) -> str:
467+
with ftp.file(path) as file:
468+
return file.read().decode("utf-8")
469+
else:
470+
file_exists = os.path.exists
471+
472+
def read_file(path: str) -> str:
473+
with open(path, encoding="utf-8") as file:
474+
return file.read()
475+
403476
if is_new_version:
477+
boot_part_path = "/sys/bus/mmc/devices/mmc0:0001/boot_part"
404478
try:
405-
ftp = self.client.open_sftp()
406-
with ftp.file("/sys/bus/mmc/devices/mmc0:0001/boot_part") as file:
407-
boot_part_value = file.read().decode("utf-8").strip()
479+
if file_exists(boot_part_path):
480+
boot_part_value = read_file(boot_part_path).strip()
408481
next_boot_part = 2 if boot_part_value == "1" else 3
482+
else:
483+
is_new_version = False
409484
except (IOError, OSError):
410485
is_new_version = False
411486

412487
if not is_new_version:
488+
root_part_path = "/sys/devices/platform/lpgpr/root_part"
413489
try:
414-
ftp = self.client.open_sftp()
415-
with ftp.file("/sys/devices/platform/lpgpr/root_part") as file:
416-
root_part_value = file.read().decode("utf-8").strip()
490+
if file_exists(root_part_path):
491+
root_part_value = read_file(root_part_path).strip()
417492
next_boot_part = 2 if root_part_value == "a" else 3
418493
except (IOError, OSError) as e:
419494
self.logger.debug(f"Failed to read next boot partition: {e}")
@@ -442,29 +517,16 @@ def get_device_status(self) -> tuple[str | None, str, str, str, str]:
442517
beta_contents = file.read().decode("utf-8")
443518

444519
else:
445-
if os.path.exists("/usr/share/remarkable/update.conf"):
446-
with open("/usr/share/remarkable/update.conf", encoding="utf-8") as file:
447-
xochitl_version = re.search(
448-
"(?<=REMARKABLE_RELEASE_VERSION=).*",
449-
file.read().strip("\n"),
450-
).group()
451-
else:
452-
with open("/etc/os-release", encoding="utf-8") as file:
453-
xochitl_version = (
454-
re.search("(?<=IMG_VERSION=).*", file.read())
455-
.group()
456-
.strip('"')
457-
)
520+
xochitl_version, old_update_engine = self._read_version_from_path()
458521

459-
old_update_engine = False
460522
if os.path.exists("/etc/version"):
461-
with open("/etc/version") as file:
523+
with open("/etc/version", encoding="utf-8") as file:
462524
version_id = file.read().rstrip()
463525
else:
464526
version_id = ""
465527

466528
if os.path.exists("/home/root/.config/remarkable/xochitl.conf"):
467-
with open("/home/root/.config/remarkable/xochitl.conf") as file:
529+
with open("/home/root/.config/remarkable/xochitl.conf", encoding="utf-8") as file:
468530
beta_contents = file.read().rstrip()
469531
else:
470532
beta_contents = ""
@@ -691,7 +753,7 @@ def install_sw_update(self, version_file: str, bootloader_files: dict[str, bytes
691753

692754
print("\nDone! Running swupdate (PLEASE BE PATIENT, ~5 MINUTES)")
693755

694-
command = f"/usr/sbin/swupdate-from-image-file {out_location}"
756+
command = f"bash -c 'source /usr/lib/swupdate/conf.d/09-swupdate-args && swupdate $SWUPDATE_ARGS -i {shlex.quote(out_location)}'"
695757
self.logger.debug(command)
696758
_stdin, stdout, _stderr = self.client.exec_command(command)
697759

@@ -737,7 +799,7 @@ def install_sw_update(self, version_file: str, bootloader_files: dict[str, bytes
737799

738800
else:
739801
print("Running swupdate")
740-
command = ["/usr/sbin/swupdate-from-image-file", version_file]
802+
command = ["bash", "-c", f"source /usr/lib/swupdate/conf.d/09-swupdate-args && swupdate $SWUPDATE_ARGS -i {shlex.quote(version_file)}"]
741803
self.logger.debug(command)
742804

743805
try:

0 commit comments

Comments
 (0)