22import logging
33import os
44import re
5+ import shlex
56import socket
67import subprocess
78import 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 ("\n Done! 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