From 7bfdcb494c6a7985ecbfdc8a798a5b04df5ec689 Mon Sep 17 00:00:00 2001 From: Cam Gorrie Date: Fri, 14 Aug 2026 15:52:21 -0400 Subject: [PATCH 1/2] Fix pedalboard refetch / last.json restore race condition --- modalapi/modhandler.py | 18 +++++++++ tests/v3/test_pedalboards.py | 73 ++++++++++++++++++++++++++++++++++-- 2 files changed, 88 insertions(+), 3 deletions(-) mode change 100755 => 100644 modalapi/modhandler.py diff --git a/modalapi/modhandler.py b/modalapi/modhandler.py old mode 100755 new mode 100644 index 979ba0bac..b9bdbee88 --- a/modalapi/modhandler.py +++ b/modalapi/modhandler.py @@ -198,6 +198,7 @@ def __init__(self, audiocard: Audiocard, homedir, data_dir="/home/pistomp/data") # Backup self.backup_file = "pistomp_backup.zip" self.data_dir = data_dir + self._restoring = False # Banks self.banks_file = os.path.join(self.data_dir, "banks.json") @@ -970,6 +971,11 @@ def poll_modui_changes(self): # reads next_pedalboard_preset_index this tick. No-op if already drained. self.poll_ws_messages() + # unzip rewrites last.json/banks.json/snapshots.json + # don't poll again until we restart the service + if self._restoring: + return + # Check for pedalboard change via last.json if self.last_json_monitor.check_for_change(): self._is_pedalboard_loading = True @@ -980,6 +986,15 @@ def poll_modui_changes(self): if mod_bundle not in self.pedalboards: self.load_pedalboards() + if mod_bundle not in self.pedalboards: + # MOD-UI owns this relationship; if its own list still lacks + # the bundle we have nothing to load and no business picking + # a substitute mid-session. Keep the board we have. + logging.warning("last.json names a pedalboard MOD-UI does not list: %s", mod_bundle) + self._is_pedalboard_loading = False + self.lcd.link_data(self.pedalboard_list, self.current, self.hardware.footswitches) + self.lcd.draw_main_panel() + return pb = self.reload_pedalboard(mod_bundle) self.set_current_pedalboard(pb) @@ -1053,6 +1068,8 @@ def load_pedalboards(self): sys.exit() pbs = json.loads(resp.text) + self.pedalboards = {} + self.pedalboard_list = [] for pb in pbs: bundle = pb[Token.BUNDLE] title = pb[Token.TITLE] @@ -1683,6 +1700,7 @@ def _do_restore_data(self, backup_dir: str, on_success=None): logging.info("Restoring data backup...") cmd = os.path.join(self.homedir, "util", "data-restore.sh") job = ArchiveJob.restore(cmd, self.username, os.path.join(backup_dir, self.backup_file), self.data_dir) + self._restoring = True self.lcd.pstack.push_panel( ArchiveProgressPanel( title="Restoring", diff --git a/tests/v3/test_pedalboards.py b/tests/v3/test_pedalboards.py index 55ebb2435..afca6a4e0 100644 --- a/tests/v3/test_pedalboards.py +++ b/tests/v3/test_pedalboards.py @@ -86,9 +86,7 @@ def get_side_effect(url, **kwargs): snapshot() -def test_v3_pedalboard_selection_menu_shows_all_boards( - v3_system: SystemFixture, nav_handler, make_plugin, snapshot -): +def test_v3_pedalboard_selection_menu_shows_all_boards(v3_system: SystemFixture, nav_handler, make_plugin, snapshot): """Starting from a loaded pedalboard, open the pedalboard selection menu showing 6 boards with distinct titles.""" handler = v3_system.handler @@ -96,6 +94,7 @@ def test_v3_pedalboard_selection_menu_shows_all_boards( titles = ["Blues Rig", "Doom Bass", "Shoegaze", "Ambient Pad", "Metal", "Jazz Clean"] from unittest.mock import MagicMock + for i, title in enumerate(titles): pb = MagicMock() pb.title = title @@ -116,6 +115,74 @@ def test_v3_pedalboard_selection_menu_shows_all_boards( snapshot() +def _list_side_effect(boards: list[tuple[str, str]]): + """mock_get side effect where pedalboard/list returns exactly `boards` as (title, bundle).""" + + def side_effect(url, **_kwargs): + resp = MagicMock() + resp.status_code = 200 + if "pedalboard/list" in url: + resp.text = json.dumps([{"title": t, "bundle": b} for t, b in boards]) + elif "snapshot/list" in url: + resp.text = json.dumps({"0": "Default"}) + elif "snapshot/name" in url: + resp.text = json.dumps({"name": "Default"}) + else: + resp.text = "{}" + return resp + + return side_effect + + +def test_v3_refetch_for_unknown_bundle_does_not_duplicate_list(v3_system: SystemFixture): + """last.json naming a board we haven't cached refetches the list. The refetch + rebuilds it — a board known before must not appear twice afterwards.""" + handler = v3_system.handler + + before = [pb.bundle for pb in handler.pedalboard_list] + assert before == ["/path/to/rig.pedalboard", "/path/to/new.pedalboard"] + + v3_system.mock_get.side_effect = _list_side_effect( + [ + ("Integration Rig", "/path/to/rig.pedalboard"), + ("New Rig", "/path/to/new.pedalboard"), + ("Restored Rig", "/path/to/restored.pedalboard"), + ] + ) + + last_json = Path(handler.data_dir) / "last.json" + last_json.write_text(json.dumps({"pedalboard": "/path/to/restored.pedalboard"})) + os.utime(last_json, (9999, 9999)) + + handler.poll_modui_changes() + + assert handler.current + assert handler.current.pedalboard.title == "Restored Rig" + + bundles = [pb.bundle for pb in handler.pedalboard_list] + assert bundles == [ + "/path/to/rig.pedalboard", + "/path/to/new.pedalboard", + "/path/to/restored.pedalboard", + ] + assert len(handler.pedalboards) == 3 + + +def test_v3_refetch_drops_boards_modui_no_longer_lists(v3_system: SystemFixture): + """A board deleted in MOD-UI leaves both the dict and the nav list on refetch.""" + handler = v3_system.handler + + v3_system.mock_get.side_effect = _list_side_effect( + [ + ("Integration Rig", "/path/to/rig.pedalboard"), + ] + ) + + handler.load_pedalboards() + + assert "/path/to/new.pedalboard" not in handler.pedalboards + assert [pb.bundle for pb in handler.pedalboard_list] == ["/path/to/rig.pedalboard"] + def test_v3_outbound_ws_suppressed_during_pedalboard_change(v3_system: SystemFixture, make_plugin): """While a pedalboard change is in flight, outbound param_set messages are dropped.""" From f72c5cc78a2845929483f47a449f8fc5fedeaf15 Mon Sep 17 00:00:00 2001 From: Cam Gorrie Date: Fri, 14 Aug 2026 15:56:43 -0400 Subject: [PATCH 2/2] v3.3.1 --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6af9ef9d6..b7a6b2bab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ Notable user visible changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [v3.3.1] - 2026-08-14 +### Fixed +- Pedalboards list no longer shows deleted pedalboards after they are removed from MOD-UI +- Fixed a race condition where Restore would re-write `last.json`, triggering a crash when the pedalboard it refers to does not yet exist when it is re-scanned + ## [v3.3.0] - 2026-08-14 ### Added - Welcome screen on startup