Skip to content

Conversation

@rmitchellscott
Copy link
Contributor

@rmitchellscott rmitchellscott commented Dec 6, 2025

In testing 3.22+ support, I saw a couple of times when the partition didn't switch after install, but I couldn't reliably replicate it. I believe I've since figured it out: in 3.22+, reMarkable changed how partition switching works:

  1. swupdate sets /sys/devices/platform/lpgpr/swu_status to indicate success/failure
  2. On reboot, rm-apply-ota checks if swu_status == 1 and calls rootdev --switch to change the boot partition
  3. I suspect that if the second swupdate run failed due to it being the current partition, it reset swu_status to 0, causing rm-apply-ota to skip the partition switch

The previous implementation ran swupdate twice (once with -e stable,copy1 and once with -e stable,copy2) to brute-force which partition to install to. One would succeed, one would fail with "over our current root". A code comment calls this a "terrible hack".

This PR changes to using /usr/sbin/swupdate-from-image-file script instead of manually invoking swupdate. This script:

  1. Sources /usr/lib/swupdate/conf.d/09-swupdate-args
  2. Detects the current partition via swupdate -g
  3. Selects the correct copy target automatically
  4. Runs swupdate only once

I've verified that both /usr/sbin/swupdate-from-image-file and /usr/lib/swupdate/conf.d/09-swupdate-args exist in the earliest swu firmware (3.11.3.3) and the latest (3.24.0.149), so this should be a reliable mechanism.

I've successfully tested a codexctl install of 3.24.0.149 to both A and B partitions of my rMPP and confirmed the partition switch happened successfully both times.

In addition to resolving this issue, this change simplifies and cleans up this section of the codebase.

Summary by CodeRabbit

  • Bug Fixes

    • More reliable update failure reporting with consistent error messages and exit-status checks.
    • Fixed cases where update commands could leave unclear state; ensured post-update reboot sequence runs consistently.
  • Improvements

    • Unified remote and local update flows for consistent behavior and faster execution.
    • Added explicit handling for device-specific bootloader downgrades and improved update output logging.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 6, 2025

Walkthrough

Replaced iterative swupdate invocation with a single direct /usr/sbin/swupdate-from-image-file call in device update logic; consolidated remote and local update paths, removed copy1/2 cycling and VERSION_FILE placeholder logic, and standardized error handling to raise SystemError on non‑zero exit status while preserving bootloader update flow.

Changes

Cohort / File(s) Summary
Device Update Simplification
codexctl/device.py
Replaced multi-flag shell swupdate calls and copy1/copy2 cycling with a direct /usr/sbin/swupdate-from-image-file invocation for both remote and local flows; removed VERSION_FILE placeholder logic; unified error handling to check exit status and raise SystemError on failure; preserved and routed Paper Pro bootloader updates to a dedicated _update_paper_pro_bootloader path; adjusted subprocess environment PATH and logging for local execution; maintained reboot/post-update sequence.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Areas needing close review:
    • Rationale and safety implications for removing copy1/copy2 cycling and VERSION_FILE logic.
    • Correctness of unified error handling and whether any prior nuanced failure detection is lost.
    • Bootloader update path _update_paper_pro_bootloader behavior and sequencing after the new swupdate invocation.
    • Subprocess environment changes (PATH) and stdout/stderr handling for local updates.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: simplifying swupdate installation to fix partition switching on firmware 3.22+, which directly aligns with the PR's core objective of replacing the double-invoke approach with a single /usr/sbin/swupdate-from-image-file invocation.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
codexctl/device.py (2)

694-702: Consider logging stdout for consistency and debugging.

The remote execution path doesn't log stdout after successful completion, while the local path does (lines 755-757). For consistency and debugging purposes, consider capturing and logging the stdout.

Apply this diff to add stdout logging:

 command = f"/usr/sbin/swupdate-from-image-file {out_location}"
 self.logger.debug(command)
 _stdin, stdout, _stderr = self.client.exec_command(command)
 
 exit_status = stdout.channel.recv_exit_status()
 
 if exit_status != 0:
     print("".join(_stderr.readlines()))
     raise SystemError("Update failed!")
+
+self.logger.debug(
+    f"Stdout of swupdate: {''.join(stdout.readlines())}"
+)

743-750: Consider avoiding shell=True for better security posture.

While the security risk is mitigated since this runs locally with controlled input, using shell=True is flagged by static analysis. Consider using the array form of subprocess invocation for better security hygiene.

Apply this diff to use array form:

-with subprocess.Popen(
-    command,
-    text=True,
-    shell=True,
-    stdout=subprocess.PIPE,
-    stderr=subprocess.PIPE,
-    env={"PATH": "/bin:/usr/bin:/sbin:/usr/sbin"},
-) as process:
+with subprocess.Popen(
+    ["/usr/sbin/swupdate-from-image-file", version_file],
+    text=True,
+    stdout=subprocess.PIPE,
+    stderr=subprocess.PIPE,
+    env={"PATH": "/bin:/usr/bin:/sbin:/usr/sbin"},
+) as process:
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a8dc990 and 84cc298.

📒 Files selected for processing (1)
  • codexctl/device.py (2 hunks)
🧰 Additional context used
🪛 Ruff (0.14.7)
codexctl/device.py

702-702: Avoid specifying long messages outside the exception class

(TRY003)


743-743: subprocess call with shell=True identified, security issue

(S602)


753-753: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Build for ubuntu-latest
  • GitHub Check: Build for windows-latest
  • GitHub Check: Build for macos-latest
  • GitHub Check: Build for remarkable

@rmitchellscott rmitchellscott requested a review from Eeems December 6, 2025 02:06
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
codexctl/device.py (3)

694-702: Quote remote image path and close SFTP client for robustness

out_location is interpolated directly into a shell command string, and the SFTP client is never closed. Even though out_location is derived from os.path.basename, it can still contain spaces or special characters, and leaving SFTP sessions open is avoidable.

Consider:

  • Using a context manager for SFTP so it always closes.
  • Shell-quoting out_location to handle spaces/special chars safely.
@@
-        if self.client:
-            ftp_client = self.client.open_sftp()
+        if self.client:
+            with self.client.open_sftp() as ftp_client:
@@
-            command = f"/usr/sbin/swupdate-from-image-file {out_location}"
+            import shlex  # add near the top of the file
+            command = f"/usr/sbin/swupdate-from-image-file {shlex.quote(out_location)}"
             self.logger.debug(command)
             _stdin, stdout, _stderr = self.client.exec_command(command)
@@
-            if exit_status != 0:
+            if exit_status != 0:
                 print("".join(_stderr.readlines()))
                 raise SystemError("Update failed!")

739-754: Preserve original error context and avoid clobbering environment

The local subprocess.check_output call looks good and avoids shell=True, but two small tweaks would improve debuggability and compatibility:

  • Chain the SystemError from the original CalledProcessError so callers can inspect the root cause.
  • Merge your custom PATH with the existing environment instead of replacing it entirely, in case swupdate-from-image-file relies on other vars.
@@
-            try:
-                output = subprocess.check_output(
-                    command,
-                    stderr=subprocess.STDOUT,
-                    text=True,
-                    env={"PATH": "/bin:/usr/bin:/sbin:/usr/sbin"},
-                )
+            try:
+                env = os.environ.copy()
+                env["PATH"] = "/bin:/usr/bin:/sbin:/usr/sbin"
+                output = subprocess.check_output(
+                    command,
+                    stderr=subprocess.STDOUT,
+                    text=True,
+                    env=env,
+                )
                 self.logger.debug(f"Stdout of swupdate: {output}")
             except subprocess.CalledProcessError as e:
                 print(e.output)
-                raise SystemError("Update failed")
+                raise SystemError("Update failed") from e

670-681: Align docstring with actual exception type

The docstring says this method raises SystemExit on failure, but the implementation now raises SystemError. That mismatch can surprise callers and any downstream tooling relying on the documented contract.

Either update the docstring to mention SystemError or change the raised exception type to match the documentation.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 84cc298 and a9f986f.

📒 Files selected for processing (1)
  • codexctl/device.py (2 hunks)
🧰 Additional context used
🪛 Ruff (0.14.7)
codexctl/device.py

702-702: Avoid specifying long messages outside the exception class

(TRY003)


744-744: subprocess call: check for execution of untrusted input

(S603)


753-753: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


753-753: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Build for windows-latest
  • GitHub Check: Build for ubuntu-latest
  • GitHub Check: Build for macos-latest
  • GitHub Check: Build for remarkable

@Eeems Eeems merged commit 6204258 into Jayy001:main Dec 6, 2025
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants