Bladeren bron

Fix phantom prints from lingering SD card files (#477)

Three bugs allowed uploaded gcode files to survive on the printer's SD
card, causing firmware restarts to auto-start old prints hours later.

1. The post-print cleanup retry loop always broke after attempt 1
   regardless of delete success — delete_file_async returns False on
   failure instead of raising, so the except-based retry never fired.

2. When start_print() failed after uploading, the file was never cleaned
   up since on_print_complete never fires for prints that never started.

3. Cleanup failures were logged at DEBUG level, invisible in production.
maziggy 3 maanden geleden
bovenliggende
commit
e6236301b4
4 gewijzigde bestanden met toevoegingen van 53 en 16 verwijderingen
  1. 1 0
      CHANGELOG.md
  2. 16 16
      backend/app/main.py
  3. 25 0
      backend/app/services/background_dispatch.py
  4. 11 0
      backend/app/services/print_scheduler.py

+ 1 - 0
CHANGELOG.md

@@ -5,6 +5,7 @@ All notable changes to Bambuddy will be documented in this file.
 ## [0.2.1b2] - Unreleased
 
 ### Fixed
+- **Phantom Prints From Lingering SD Card Files** ([#477](https://github.com/maziggy/bambuddy/issues/477)) — Prints could restart without user input hours after completing, because uploaded gcode files survived on the printer's SD card and were auto-started on firmware restart. Three bugs allowed files to linger. First, the post-print SD card cleanup retry loop always broke after the first attempt regardless of success, because `delete_file_async` catches errors internally and returns `False` instead of raising — the `except` retry branch never executed. Fixed by only breaking on successful delete and retrying with a 2-second delay on failure. Second, when `start_print()` failed after uploading a file (in both the background dispatcher and print scheduler), the uploaded file was never cleaned up since `on_print_complete` never fires for a print that never started. Now deletes the uploaded file on a best-effort basis when `start_print()` returns `False`. Third, cleanup failure logging was at `DEBUG` level, making failures invisible in normal operation — escalated to `WARNING`.
 - **Non-Actionable HMS Errors Triggering Notifications** ([#470](https://github.com/maziggy/bambuddy/issues/470)) — Infrastructure and auth-related HMS error codes (like `0500_0007` "MQTT command verification failed") were triggering printer error notifications even though they don't indicate actual print problems. For example, a device with incorrect bind settings sending unauthorized MQTT commands caused repeated false-alarm nozzle/extruder error notifications with camera snapshots of perfectly fine prints. Now suppresses notifications for known non-actionable error codes: `0500_0007` (MQTT auth failure), `0500_4001` (Bambu Cloud connection failure), and `0500_400E` (print cancelled by user).
 - **Support Bundle Leaking Personal Data** ([#473](https://github.com/maziggy/bambuddy/issues/473)) — The support bundle's log sanitizer only used regex patterns, which can't detect arbitrary user-chosen strings like printer names and usernames. Now queries the database for known sensitive values (printer names, serial numbers, auth usernames, Bambu Cloud email) and does exact-string replacement before the regex pass. Serial number regex no longer leaks the first 3 characters (was using a capture group for partial redaction). Tasmota smart plug credentials embedded in URLs (`http://user:pass@host`) were logged verbatim by httpx; now uses httpx's `auth` parameter for HTTP Basic auth so credentials never appear in the URL. Added `username` and `path` to the settings key filter to redact `smtp_username` and `slicer_binary_path` from the support info JSON. A URL credentials regex provides defense-in-depth for any remaining `user:pass@` patterns in logs. IP addresses are no longer redacted from the bundle as they are needed for connectivity debugging. Updated the frontend privacy disclaimer and wiki documentation to reflect the new behavior.
 - **Spool Usage Lost When Spool Runs Empty Mid-Print** ([#459](https://github.com/maziggy/bambuddy/issues/459)) — When a spool ran empty during a print and the AMS auto-switched to a backup spool, two problems caused incorrect tracking. First, the `on_ams_change` handler eagerly deleted the empty spool's `SpoolAssignment` record (fingerprint mismatch), so `on_print_complete` found nothing and silently dropped usage — fixed by snapshotting all spool assignments at print start into the `PrintSession`. Second, even with the snapshot fix, the entire print's filament weight was attributed to the original spool (100%/0% split) because `_track_from_3mf()` only knew about the tray loaded at print start. Now tracks tray changes during the print via `tray_change_log` on `PrinterState`, recording each tray switch with its layer number. At print completion, the usage tracker splits the 3MF weight across trays using per-layer gcode data for precise segment boundaries, with a linear layer-ratio fallback when gcode data isn't available. The last segment always receives the remainder to prevent rounding drift.

+ 16 - 16
backend/app/main.py

@@ -2154,24 +2154,24 @@ async def on_print_complete(printer_id: int, data: dict):
                             )
                             if delete_result:
                                 logger.info("Deleted %s from printer %s SD card", remote_path, printer.name)
-                            break  # Success or file doesn't exist — no need to retry
+                                break
                         except Exception as e:
-                            if attempt < 3:
-                                logger.debug(
-                                    "SD card cleanup attempt %d/3 failed for %s: %s, retrying in 2s",
-                                    attempt,
-                                    remote_path,
-                                    e,
-                                )
-                                await asyncio.sleep(2)
-                            else:
-                                logger.debug(
-                                    "SD card cleanup failed after 3 attempts for %s: %s (non-critical)",
-                                    remote_path,
-                                    e,
-                                )
+                            delete_result = False
+                            logger.warning(
+                                "SD card cleanup attempt %d/3 raised for %s: %s",
+                                attempt,
+                                remote_path,
+                                e,
+                            )
+                        if not delete_result and attempt < 3:
+                            await asyncio.sleep(2)
+                        elif not delete_result:
+                            logger.warning(
+                                "SD card cleanup failed after 3 attempts for %s (file may linger on SD card)",
+                                remote_path,
+                            )
     except Exception as e:
-        logger.debug("SD card file cleanup failed for printer %s: %s (non-critical)", printer_id, e)
+        logger.warning("SD card file cleanup failed for printer %s: %s", printer_id, e)
 
     log_timing("SD card cleanup")
 

+ 25 - 0
backend/app/services/background_dispatch.py

@@ -664,6 +664,12 @@ class BackgroundDispatchService:
                 )
 
                 if not started:
+                    await self._cleanup_sd_card_file(
+                        printer_ip,
+                        printer_access_code,
+                        remote_path,
+                        printer_model,
+                    )
                     raise RuntimeError("Failed to start print")
 
                 if job.requested_by_user_id and job.requested_by_username:
@@ -821,6 +827,12 @@ class BackgroundDispatchService:
                 )
 
                 if not started:
+                    await self._cleanup_sd_card_file(
+                        printer_ip,
+                        printer_access_code,
+                        remote_path,
+                        printer_model,
+                    )
                     await db.rollback()
                     raise RuntimeError("Failed to start print")
 
@@ -830,6 +842,19 @@ class BackgroundDispatchService:
                 await self._set_active_message(job, f"Cancelled upload on {printer_name}.")
                 raise
 
+    @staticmethod
+    async def _cleanup_sd_card_file(
+        printer_ip: str,
+        access_code: str,
+        remote_path: str,
+        printer_model: str | None,
+    ):
+        """Best-effort delete of uploaded file from printer SD card."""
+        try:
+            await delete_file_async(printer_ip, access_code, remote_path, printer_model=printer_model)
+        except Exception:
+            pass  # Best-effort — don't fail the error handler
+
     @staticmethod
     def _resolve_plate_id(file_path: Path, requested_plate_id: int | None) -> int:
         if requested_plate_id is not None:

+ 11 - 0
backend/app/services/print_scheduler.py

@@ -1112,6 +1112,17 @@ class PrintScheduler:
             except Exception:
                 pass  # Don't fail if MQTT fails
         else:
+            # Clean up uploaded file from SD card to prevent phantom prints
+            try:
+                await delete_file_async(
+                    printer.ip_address,
+                    printer.access_code,
+                    remote_path,
+                    printer_model=printer.model,
+                )
+            except Exception:
+                pass  # Best-effort — don't fail the error handler
+
             # Print command failed - revert status
             item.status = "failed"
             item.error_message = "Failed to send print command to printer"