mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-21 10:15:04 +02:00
fix(virtual-printer): #1429 net.info[*].ip cache leak + mode wire-value rename
#1429 (reported by @TrickShotMLG02, confirmed by @Mape6 on a flat single-LAN that rules out subnet / mDNS-reflector theories): with the physical printer off the slicer's "Send" landed in Bambuddy's archive; once the printer powered on every subsequent "Send" went straight to the printer's SD card and bypassed Bambuddy. Bundle analysis: mape6-before showed clean FTP receive + archive lines, mape6-after had zero FTP attempts to Bambuddy once the printer was online. Cause: mqtt_bridge.py::_resolve_client encoded _target_ip_uint32_le / _vp_ip_uint32_le ONLY on client-identity change and early-returned on every refresh tick when the same client object was still bound. If target_client.ip_address was empty at first bind (DB row stale, or client constructed before SSDP refresh filled it in), the encoding stayed None, the net.info[*].ip rewrite block was skipped, the cache filled with the real printer IP, sticky-key preservation kept the poisoned net value alive across every subsequent incremental push, and the slicer followed the leaked IP. Only Bambuddy-restart-with-printer-off cleared it — the workaround both reporters independently arrived at. Same shape on multi-NIC printers (X1C, H2D Pro): the rewrite only matched entries whose ip equalled _target_ip_uint32_le, so a secondary interface IP Bambuddy never saw would leak through unchanged. Bridge fix: - _resolve_client calls a new _refresh_ip_encoding() on every refresh tick, even when client identity is unchanged; self-heals once ip_address becomes valid. - _refresh_ip_encoding() sweeps the existing _latest_print_state when encoding becomes valid for the first time. Without the sweep, sticky-key preservation keeps the pre-arm poisoned cache alive forever — incremental pushes that don't include net carry the bad value forward. - _rewrite_net_info_ips() rewrites EVERY non-zero net.info[].ip entry that doesn't already equal the VP IP, not just entries matching _target_ip_uint32_le. Multi-NIC printers stop leaking secondary interfaces. Zero-IP placeholders are left alone so "active interface" detection still works. - INFO logging on encoding arm/update and on cache sweep so future bundles directly answer "did the rewrite fire?". Mode wire-value rename (#1429 follow-up, separate confusion source): - Both reporters' support bundles showed mode: immediate while the UI said "Archive"; @TrickShotMLG02 quoted: "I have no idea why it says immediate in the support-info.json file. In the webui the printer is set to archive". UI button "Archive" had always saved immediate, and "Queue" had always saved print_queue. Canonical wire values are now archive / review / queue / proxy, matching the button labels 1:1. - New normalize_vp_mode() + VP_MODE_* constants in models/virtual_printer.py; manager.py normalises on construction so a legacy row read pre-migration still dispatches correctly. - core/database.py::run_migrations rewrites existing virtual_printers and settings rows; idempotent (re-runs are no-ops); identical SQL under SQLite and Postgres. - API routes accept both legacy and canonical on input, normalise before storage. GET /settings/virtual-printer normalises on read so the frontend's mode-button highlight works for stale legacy values. - Three frontend VP components (VirtualPrinterSettings, VirtualPrinterCard, VirtualPrinterAddDialog) switched click handlers and type aliases to canonical; each got its own normalizeMode() helper so a stale-cached settings payload still highlights the right button. Two pre-existing `printer.mode === 'queue' ? 'review'` legacy mappings in VirtualPrinterCard were the source of a test failure caught mid-implementation where the new canonical 'queue' was being mis-aliased back to 'review' and hiding the auto-dispatch + force-color-match toggles. mode handler is NOT the dispatch bug: manager.py::_archive_file (the handler for archive mode) doesn't dispatch to the physical printer. The "files end up on the printer's SD card" symptom was the IP-leak from the bridge cache. The mode rename is purely clarity / support- bundle accuracy.
This commit is contained in:
@@ -61,33 +61,46 @@ class TestVirtualPrinterSettingsAPI:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_mode_to_print_queue(self, async_client: AsyncClient):
|
||||
"""Verify mode can be set to print_queue."""
|
||||
response = await async_client.put("/api/v1/settings/virtual-printer?mode=print_queue")
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["mode"] == "print_queue"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_mode_legacy_queue_maps_to_review(self, async_client: AsyncClient):
|
||||
"""Verify legacy 'queue' mode is normalized to 'review'."""
|
||||
async def test_update_mode_to_queue(self, async_client: AsyncClient):
|
||||
"""Verify mode can be set to the canonical 'queue' value."""
|
||||
response = await async_client.put("/api/v1/settings/virtual-printer?mode=queue")
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["mode"] == "review" # Legacy queue maps to review
|
||||
assert result["mode"] == "queue"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_mode_to_immediate(self, async_client: AsyncClient):
|
||||
"""Verify mode can be set to immediate."""
|
||||
async def test_update_mode_legacy_print_queue_normalises_to_queue(self, async_client: AsyncClient):
|
||||
"""Legacy `print_queue` is accepted on input and translated to `queue` on
|
||||
storage so the UI button label and the support-bundle field agree
|
||||
(#1429 mode-label discrepancy)."""
|
||||
response = await async_client.put("/api/v1/settings/virtual-printer?mode=print_queue")
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["mode"] == "queue"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_mode_legacy_immediate_normalises_to_archive(self, async_client: AsyncClient):
|
||||
"""Legacy `immediate` is accepted on input and translated to `archive`
|
||||
on storage (#1429 mode-label discrepancy)."""
|
||||
response = await async_client.put("/api/v1/settings/virtual-printer?mode=immediate")
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["mode"] == "immediate"
|
||||
assert result["mode"] == "archive"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_mode_to_archive(self, async_client: AsyncClient):
|
||||
"""Verify mode can be set to the canonical 'archive' value."""
|
||||
response = await async_client.put("/api/v1/settings/virtual-printer?mode=archive")
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["mode"] == "archive"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
@@ -136,7 +149,7 @@ class TestVirtualPrinterSettingsAPI:
|
||||
return_value={
|
||||
"enabled": True,
|
||||
"running": True,
|
||||
"mode": "immediate",
|
||||
"mode": "archive",
|
||||
"name": "Bambuddy",
|
||||
"serial": "00M09A391800001",
|
||||
"pending_files": 0,
|
||||
@@ -157,7 +170,7 @@ class TestVirtualPrinterSettingsAPI:
|
||||
return_value={
|
||||
"enabled": False,
|
||||
"running": False,
|
||||
"mode": "immediate",
|
||||
"mode": "archive",
|
||||
"name": "Bambuddy",
|
||||
"serial": "00M09A391800001",
|
||||
"pending_files": 0,
|
||||
@@ -283,7 +296,7 @@ class TestVirtualPrinterAutoDispatchAPI:
|
||||
"/api/v1/virtual-printers",
|
||||
json={
|
||||
"name": "TestDefaultDispatch",
|
||||
"mode": "print_queue",
|
||||
"mode": "queue",
|
||||
"access_code": "12345678",
|
||||
},
|
||||
)
|
||||
@@ -300,7 +313,7 @@ class TestVirtualPrinterAutoDispatchAPI:
|
||||
"/api/v1/virtual-printers",
|
||||
json={
|
||||
"name": "TestManualDispatch",
|
||||
"mode": "print_queue",
|
||||
"mode": "queue",
|
||||
"access_code": "12345678",
|
||||
"auto_dispatch": False,
|
||||
},
|
||||
@@ -319,7 +332,7 @@ class TestVirtualPrinterAutoDispatchAPI:
|
||||
"/api/v1/virtual-printers",
|
||||
json={
|
||||
"name": "TestToggleDispatch",
|
||||
"mode": "print_queue",
|
||||
"mode": "queue",
|
||||
"access_code": "12345678",
|
||||
},
|
||||
)
|
||||
@@ -359,7 +372,7 @@ class TestVirtualPrinterTailscaleToggleAPI:
|
||||
"/api/v1/virtual-printers",
|
||||
json={
|
||||
"name": "TestTailscaleToggle",
|
||||
"mode": "immediate",
|
||||
"mode": "archive",
|
||||
"access_code": "12345678",
|
||||
},
|
||||
)
|
||||
@@ -426,7 +439,7 @@ class TestVirtualPrinterDiagnosticAPI:
|
||||
"""A freshly created (disabled) VP fails the 'enabled' check."""
|
||||
create_resp = await async_client.post(
|
||||
"/api/v1/virtual-printers",
|
||||
json={"name": "TestDiagVP", "mode": "immediate", "access_code": "12345678"},
|
||||
json={"name": "TestDiagVP", "mode": "archive", "access_code": "12345678"},
|
||||
)
|
||||
assert create_resp.status_code == 200
|
||||
vp_id = create_resp.json()["id"]
|
||||
|
||||
Reference in New Issue
Block a user