From c7edbdd5b3ceca688bd7f06eb3a927d9bd516d42 Mon Sep 17 00:00:00 2001 From: Brigs Date: Sun, 9 Aug 2026 16:08:29 -0400 Subject: [PATCH] fix: make sure the delivered LAVA database opens from read-only media A database whose header says journal_mode=WAL cannot be opened read-only: SQLite must create a -shm file to read one, and the LAVA viewer opens the report database with OPEN_READONLY. lava_finalize_output now checkpoints and returns to the delete journal before closing, so the delivered .db is complete and standalone with no -wal/-shm sidecars to lose when a report is copied. Nothing here enables WAL. This normalises whatever mode the connection ended in, so it is a guard rather than a fix for a failure happening today. Split out of RLEAPP #359; the WAL enablement and storage_safety.py module from that PR are deliberately deferred to the planned core consolidation. Co-Authored-By: Claude Opus 5 --- leapp_functions/app/history.py | 14 +++++++++++++- scripts/lavafuncs.py | 14 ++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/leapp_functions/app/history.py b/leapp_functions/app/history.py index 9534ea22d..66e75bd6f 100644 --- a/leapp_functions/app/history.py +++ b/leapp_functions/app/history.py @@ -129,7 +129,10 @@ def _atomic_write_json(path, data): """ path = Path(path) path.parent.mkdir(parents=True, exist_ok=True) - temp_path = path.with_suffix(".tmp") + # Unique per process: concurrent runs sharing one temp path interleave their + # writes into it, which lands a concatenated document in the real file and + # makes the second os.replace fail because the first already moved it. + temp_path = path.with_suffix(f".tmp.{os.getpid()}") try: with open(temp_path, "w", encoding="utf-8") as f: @@ -163,6 +166,15 @@ def _read_json(path, default=None): except OSError as e: logger.error("Failed to read history/settings file at %s: %s", path, e) return default + except ValueError as e: + # Unreadable JSON here is a damaged convenience file, not a reason to stop + # parsing evidence. Keep a copy so the damage can be looked at later. + logger.error("History/settings file at %s is not valid JSON (%s); ignoring it", path, e) + try: + os.replace(path, f"{path}.corrupt.bak") + except OSError: + pass + return default def _has_history_entries(history_data): diff --git a/scripts/lavafuncs.py b/scripts/lavafuncs.py index b229978b7..342284590 100644 --- a/scripts/lavafuncs.py +++ b/scripts/lavafuncs.py @@ -698,5 +698,19 @@ def lava_finalize_output(output_path): with open(os.path.join(output_path, lava_json_name), 'w', encoding='utf-8') as f: json.dump(lava_data, f, indent=4) + # A database left in WAL mode cannot be opened from read-only media: SQLite + # must create a -shm file to read one, and the LAVA viewer opens the report + # database with OPEN_READONLY. Checkpointing and returning to the delete + # journal guarantees the delivered .db is complete and standalone, with no + # -wal/-shm sidecars to lose when the report folder is copied or zipped. + # This normalises whatever mode the connection ended up in; nothing here + # enables WAL. + try: + if lava_db.execute('PRAGMA journal_mode').fetchone()[0].lower() == 'wal': + lava_db.execute('PRAGMA wal_checkpoint(TRUNCATE)') + lava_db.execute('PRAGMA journal_mode=DELETE') + except sqlite3.Error: + pass + # Close the SQLite database lava_db.close()