From 59094ee4690c05c01134304d4eff5c9246c6d1c6 Mon Sep 17 00:00:00 2001 From: LemonNexus Date: Sun, 8 Feb 2026 22:33:08 +0000 Subject: [PATCH] fix(gui): SQLite thread safety - use queue for cross-thread database access - LogWatcher callbacks now queue events instead of direct DB access - Main thread processes queue every 100ms - Fixes 'SQLite objects created in a thread can only be used in that same thread' error - Loot will now be properly recorded to database --- ui/main_window.py | 63 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 51 insertions(+), 12 deletions(-) diff --git a/ui/main_window.py b/ui/main_window.py index bb28194..63f67c9 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -236,10 +236,20 @@ class MainWindow(QMainWindow): self.log_watcher: Optional[LogWatcher] = None self._log_watcher_task = None + # Thread-safe queue for cross-thread communication + from queue import Queue + self._event_queue = Queue() + + # Timer to process queued events in main thread + self._queue_timer = QTimer(self) + self._queue_timer.timeout.connect(self._process_queued_events) + self._queue_timer.start(100) # Check every 100ms + # State self.current_project: Optional[Project] = None self.session_state = SessionState.IDLE self.current_session_id: Optional[int] = None + self._current_db_session_id: Optional[int] = None # Setup UI self.setup_ui() @@ -904,27 +914,28 @@ class MainWindow(QMainWindow): """Handle loot events.""" item_name = event.data.get('item_name', 'Unknown') value_ped = event.data.get('value_ped', Decimal('0.0')) + quantity = event.data.get('quantity', 1) # Skip Universal Ammo if item_name == 'Universal Ammo': return - # Record to database + # Queue database write for main thread (SQLite thread safety) if self._current_db_session_id: - loot = LootEvent( - item_name=item_name, - quantity=event.data.get('quantity', 1), - value_ped=value_ped, - event_type='regular', - raw_log_line=event.raw_line - ) - self.project_manager.record_loot(loot) + self._event_queue.put({ + 'type': 'loot', + 'session_id': self._current_db_session_id, + 'item_name': item_name, + 'quantity': quantity, + 'value_ped': value_ped, + 'raw_line': event.raw_line + }) - # Update HUD + # Update HUD (thread-safe) self.hud.on_loot_event(item_name, value_ped) - # Log to UI - self.log_info("Loot", f"{item_name} x{event.data.get('quantity', 1)} ({value_ped} PED)") + # Log to UI (main thread only - use signal/slot or queue) + # We'll log this in _process_queued_events instead def on_global(event): """Handle global events.""" @@ -1017,6 +1028,34 @@ class MainWindow(QMainWindow): self._log_watcher_thread = None self.log_info("LogWatcher", "Stopped") + def _process_queued_events(self): + """Process events from the queue in the main thread (SQLite thread safety).""" + from core.project_manager import LootEvent + from decimal import Decimal + + processed = 0 + while not self._event_queue.empty() and processed < 10: # Process max 10 per tick + try: + event = self._event_queue.get_nowait() + + if event['type'] == 'loot': + # Record to database (now in main thread - safe) + loot = LootEvent( + item_name=event['item_name'], + quantity=event['quantity'], + value_ped=event['value_ped'], + event_type='regular', + raw_log_line=event['raw_line'] + ) + self.project_manager.record_loot(loot) + + # Log to UI + self.log_info("Loot", f"{event['item_name']} x{event['quantity']} ({event['value_ped']} PED)") + + processed += 1 + except Exception as e: + self.log_error("EventQueue", f"Error processing event: {e}") + def on_start_session(self): """Handle start session button.""" if self.current_project and self.session_state == SessionState.IDLE: