Browse Source

Fix SD card cleanup silently failing after print completion (#374)

The post-print SD card cleanup used printer_manager.get_printer(),
which returns a PrinterInfo object with only name and serial_number.
The code then accessed .ip_address, .access_code, and .model, which
don't exist on PrinterInfo — raising AttributeError on every print
completion. The outer except Exception handler silently swallowed it
at debug level.

Result: uploaded gcode files were never deleted from the printer's
SD card root. On some printers (e.g. P1S), files left in root
auto-start on power cycle, causing phantom prints.

Replaced with a DB query for the Printer model, matching the pattern
used everywhere else in on_print_complete().
maziggy 3 months ago
parent
commit
b22f091dd0
2 changed files with 39 additions and 32 deletions
  1. 1 0
      CHANGELOG.md
  2. 38 32
      backend/app/main.py

+ 1 - 0
CHANGELOG.md

@@ -9,6 +9,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **K-Profile Response Race Condition Crash** ([#462](https://github.com/maziggy/bambuddy/issues/462)) — An unsolicited or late K-profile MQTT response could crash the MQTT handler with `AttributeError: 'NoneType' object has no attribute 'set'`. The MQTT callback thread checked `self._pending_kprofile_response` (not None) at line 2698, but between that check and the `.set()` call, the asyncio thread's `finally` block in `get_kprofiles()` could clear the attribute to `None` after a timeout — a classic TOCTOU race. Fixed by capturing the event reference in a local variable before the check.
 - **Queue Stuck on "Busy" for "Any Model" Jobs** ([#435](https://github.com/maziggy/bambuddy/issues/435)) — When a print was queued with "Any [Model]" (e.g., "Any P1S"), it was created with `printer_id=NULL` and `target_model="P1S"`. After the assigned printer finished, the queue widget queried only for items matching `printer_id=X`, missing the next pending model-based item (`printer_id IS NULL`). With no next item found, the "Clear Plate & Start Next" button never appeared, leaving the scheduler stuck reporting "Busy". The queue API now accepts an optional `target_model` parameter; when combined with `printer_id`, it uses OR logic to also return unassigned items whose `target_model` matches the printer's model. The frontend passes the printer's model through to this query. Additionally, the backend now resolves the printer's model server-side from the database when the frontend doesn't provide `target_model` (e.g., when the printer was added without selecting a model), ensuring the OR logic works regardless of whether the client knows the printer's model.
 - **Queue "Any Model" Jobs Stuck in "Waiting" After Plate Clear** ([#435](https://github.com/maziggy/bambuddy/issues/435)) — After the queue visibility fix above, "Any Model" jobs were correctly assigned to an idle printer but immediately crashed with `'>=' not supported between instances of 'str' and 'int'` when computing AMS filament mapping. MQTT raw data returns AMS unit and tray IDs as strings, but `_build_loaded_filaments()` compared them to integers without casting. The crash prevented the assignment from committing, so the scheduler retried every 30 seconds in an infinite loop. Cast `ams_id` and `tray_id` to `int()` to match the pattern already used for external spool IDs.
+- **SD Card Cleanup After Print Never Runs** ([#374](https://github.com/maziggy/bambuddy/issues/374)) — The post-print SD card cleanup (which deletes uploaded gcode from the printer root to prevent phantom prints on power cycle) used `printer_manager.get_printer()`, which returns a `PrinterInfo` with only `name` and `serial_number`. Accessing `.ip_address`, `.access_code`, and `.model` raised `AttributeError`, silently caught by the outer exception handler. Replaced with a DB query for the `Printer` model, matching the pattern used everywhere else in `on_print_complete()`.
 
 ### New Features
 - **Background Print Dispatch** ([#408](https://github.com/maziggy/bambuddy/pull/408), [#112](https://github.com/maziggy/bambuddy/issues/112)) — Printing from archives and the file manager now runs in the background via an async dispatch service. FTP uploads and print-start commands are decoupled from API request latency, so the UI responds immediately. Real-time progress is streamed to all clients via WebSocket, rendered as a persistent toast with per-job upload progress bars, status badges (dispatched/processing/completed/failed/cancelled), and a cancel button. The dispatcher supports concurrent uploads to different printers with per-printer queuing to prevent conflicts. Cancellation is cooperative — uploads abort at the next chunk boundary and clean up partial files on the printer. Batch progress tracking shows overall completion across multi-printer dispatches. Translations added for all 6 locales (en, de, fr, it, ja, pt-BR).

+ 38 - 32
backend/app/main.py

@@ -2119,40 +2119,46 @@ async def on_print_complete(printer_id: int, data: dict):
     # auto-start files found in root on power cycle, causing ghost prints.
     # Must run before the archive_id early-return so it executes even when archiving is disabled.
     try:
-        printer_info = printer_manager.get_printer(printer_id)
-        if printer_info and subtask_name:
-            from backend.app.services.bambu_ftp import delete_file_async
-
-            # Try both .3mf and .gcode extensions — the printer may have either
-            for ext in (".3mf", ".gcode"):
-                remote_path = f"/{subtask_name}{ext}"
-                # Retry up to 3 times — the printer may still lock the filesystem briefly after a print ends
-                for attempt in range(1, 4):
-                    try:
-                        delete_result = await delete_file_async(
-                            printer_info.ip_address,
-                            printer_info.access_code,
-                            remote_path,
-                            printer_model=printer_info.model,
-                        )
-                        if delete_result:
-                            logger.info("Deleted %s from printer %s SD card", remote_path, printer_info.name)
-                        break  # Success or file doesn't exist — no need to retry
-                    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)",
+        if subtask_name:
+            async with async_session() as db:
+                from backend.app.models.printer import Printer
+
+                result = await db.execute(select(Printer).where(Printer.id == printer_id))
+                printer = result.scalar_one_or_none()
+
+            if printer:
+                from backend.app.services.bambu_ftp import delete_file_async
+
+                # Try both .3mf and .gcode extensions — the printer may have either
+                for ext in (".3mf", ".gcode"):
+                    remote_path = f"/{subtask_name}{ext}"
+                    # Retry up to 3 times — the printer may still lock the filesystem briefly after a print ends
+                    for attempt in range(1, 4):
+                        try:
+                            delete_result = await delete_file_async(
+                                printer.ip_address,
+                                printer.access_code,
                                 remote_path,
-                                e,
+                                printer_model=printer.model,
                             )
+                            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
+                        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,
+                                )
     except Exception as e:
         logger.debug("SD card file cleanup failed for printer %s: %s (non-critical)", printer_id, e)