pomera-ai-commander 1.1.1 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +105 -680
- package/bin/pomera-ai-commander.js +62 -62
- package/core/__init__.py +65 -65
- package/core/app_context.py +482 -482
- package/core/async_text_processor.py +421 -421
- package/core/backup_manager.py +655 -655
- package/core/backup_recovery_manager.py +1199 -1033
- package/core/content_hash_cache.py +508 -508
- package/core/context_menu.py +313 -313
- package/core/data_directory.py +549 -0
- package/core/data_validator.py +1066 -1066
- package/core/database_connection_manager.py +744 -744
- package/core/database_curl_settings_manager.py +608 -608
- package/core/database_promera_ai_settings_manager.py +446 -446
- package/core/database_schema.py +411 -411
- package/core/database_schema_manager.py +395 -395
- package/core/database_settings_manager.py +1507 -1507
- package/core/database_settings_manager_interface.py +456 -456
- package/core/dialog_manager.py +734 -734
- package/core/diff_utils.py +239 -0
- package/core/efficient_line_numbers.py +540 -510
- package/core/error_handler.py +746 -746
- package/core/error_service.py +431 -431
- package/core/event_consolidator.py +511 -511
- package/core/mcp/__init__.py +43 -43
- package/core/mcp/find_replace_diff.py +334 -0
- package/core/mcp/protocol.py +288 -288
- package/core/mcp/schema.py +251 -251
- package/core/mcp/server_stdio.py +299 -299
- package/core/mcp/tool_registry.py +2699 -2345
- package/core/memento.py +275 -0
- package/core/memory_efficient_text_widget.py +711 -711
- package/core/migration_manager.py +914 -914
- package/core/migration_test_suite.py +1085 -1085
- package/core/migration_validator.py +1143 -1143
- package/core/optimized_find_replace.py +714 -714
- package/core/optimized_pattern_engine.py +424 -424
- package/core/optimized_search_highlighter.py +552 -552
- package/core/performance_monitor.py +674 -674
- package/core/persistence_manager.py +712 -712
- package/core/progressive_stats_calculator.py +632 -632
- package/core/regex_pattern_cache.py +529 -529
- package/core/regex_pattern_library.py +350 -350
- package/core/search_operation_manager.py +434 -434
- package/core/settings_defaults_registry.py +1087 -1087
- package/core/settings_integrity_validator.py +1111 -1111
- package/core/settings_serializer.py +557 -557
- package/core/settings_validator.py +1823 -1823
- package/core/smart_stats_calculator.py +709 -709
- package/core/statistics_update_manager.py +619 -619
- package/core/stats_config_manager.py +858 -858
- package/core/streaming_text_handler.py +723 -723
- package/core/task_scheduler.py +596 -596
- package/core/update_pattern_library.py +168 -168
- package/core/visibility_monitor.py +596 -596
- package/core/widget_cache.py +498 -498
- package/mcp.json +51 -61
- package/migrate_data.py +127 -0
- package/package.json +64 -57
- package/pomera.py +7883 -7482
- package/pomera_mcp_server.py +183 -144
- package/requirements.txt +33 -0
- package/scripts/Dockerfile.alpine +43 -0
- package/scripts/Dockerfile.gui-test +54 -0
- package/scripts/Dockerfile.linux +43 -0
- package/scripts/Dockerfile.test-linux +80 -0
- package/scripts/Dockerfile.ubuntu +39 -0
- package/scripts/README.md +53 -0
- package/scripts/build-all.bat +113 -0
- package/scripts/build-docker.bat +53 -0
- package/scripts/build-docker.sh +55 -0
- package/scripts/build-optimized.bat +101 -0
- package/scripts/build.sh +78 -0
- package/scripts/docker-compose.test.yml +27 -0
- package/scripts/docker-compose.yml +32 -0
- package/scripts/postinstall.js +62 -0
- package/scripts/requirements-minimal.txt +33 -0
- package/scripts/test-linux-simple.bat +28 -0
- package/scripts/validate-release-workflow.py +450 -0
- package/tools/__init__.py +4 -4
- package/tools/ai_tools.py +2891 -2891
- package/tools/ascii_art_generator.py +352 -352
- package/tools/base64_tools.py +183 -183
- package/tools/base_tool.py +511 -511
- package/tools/case_tool.py +308 -308
- package/tools/column_tools.py +395 -395
- package/tools/cron_tool.py +884 -884
- package/tools/curl_history.py +600 -600
- package/tools/curl_processor.py +1207 -1207
- package/tools/curl_settings.py +502 -502
- package/tools/curl_tool.py +5467 -5467
- package/tools/diff_viewer.py +1817 -1072
- package/tools/email_extraction_tool.py +248 -248
- package/tools/email_header_analyzer.py +425 -425
- package/tools/extraction_tools.py +250 -250
- package/tools/find_replace.py +2289 -1750
- package/tools/folder_file_reporter.py +1463 -1463
- package/tools/folder_file_reporter_adapter.py +480 -480
- package/tools/generator_tools.py +1216 -1216
- package/tools/hash_generator.py +255 -255
- package/tools/html_tool.py +656 -656
- package/tools/jsonxml_tool.py +729 -729
- package/tools/line_tools.py +419 -419
- package/tools/markdown_tools.py +561 -561
- package/tools/mcp_widget.py +1417 -1417
- package/tools/notes_widget.py +978 -973
- package/tools/number_base_converter.py +372 -372
- package/tools/regex_extractor.py +571 -571
- package/tools/slug_generator.py +310 -310
- package/tools/sorter_tools.py +458 -458
- package/tools/string_escape_tool.py +392 -392
- package/tools/text_statistics_tool.py +365 -365
- package/tools/text_wrapper.py +430 -430
- package/tools/timestamp_converter.py +421 -421
- package/tools/tool_loader.py +710 -710
- package/tools/translator_tools.py +522 -522
- package/tools/url_link_extractor.py +261 -261
- package/tools/url_parser.py +204 -204
- package/tools/whitespace_tools.py +355 -355
- package/tools/word_frequency_counter.py +146 -146
- package/core/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/__pycache__/app_context.cpython-313.pyc +0 -0
- package/core/__pycache__/async_text_processor.cpython-313.pyc +0 -0
- package/core/__pycache__/backup_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/backup_recovery_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/content_hash_cache.cpython-313.pyc +0 -0
- package/core/__pycache__/context_menu.cpython-313.pyc +0 -0
- package/core/__pycache__/data_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/database_connection_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_curl_settings_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_promera_ai_settings_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_schema.cpython-313.pyc +0 -0
- package/core/__pycache__/database_schema_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_settings_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_settings_manager_interface.cpython-313.pyc +0 -0
- package/core/__pycache__/dialog_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/efficient_line_numbers.cpython-313.pyc +0 -0
- package/core/__pycache__/error_handler.cpython-313.pyc +0 -0
- package/core/__pycache__/error_service.cpython-313.pyc +0 -0
- package/core/__pycache__/event_consolidator.cpython-313.pyc +0 -0
- package/core/__pycache__/memory_efficient_text_widget.cpython-313.pyc +0 -0
- package/core/__pycache__/migration_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/migration_test_suite.cpython-313.pyc +0 -0
- package/core/__pycache__/migration_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/optimized_find_replace.cpython-313.pyc +0 -0
- package/core/__pycache__/optimized_pattern_engine.cpython-313.pyc +0 -0
- package/core/__pycache__/optimized_search_highlighter.cpython-313.pyc +0 -0
- package/core/__pycache__/performance_monitor.cpython-313.pyc +0 -0
- package/core/__pycache__/persistence_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/progressive_stats_calculator.cpython-313.pyc +0 -0
- package/core/__pycache__/regex_pattern_cache.cpython-313.pyc +0 -0
- package/core/__pycache__/regex_pattern_library.cpython-313.pyc +0 -0
- package/core/__pycache__/search_operation_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_defaults_registry.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_integrity_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_serializer.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/smart_stats_calculator.cpython-313.pyc +0 -0
- package/core/__pycache__/statistics_update_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/stats_config_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/streaming_text_handler.cpython-313.pyc +0 -0
- package/core/__pycache__/task_scheduler.cpython-313.pyc +0 -0
- package/core/__pycache__/visibility_monitor.cpython-313.pyc +0 -0
- package/core/__pycache__/widget_cache.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/protocol.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/schema.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/server_stdio.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/tool_registry.cpython-313.pyc +0 -0
- package/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- package/tools/__pycache__/ai_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/ascii_art_generator.cpython-313.pyc +0 -0
- package/tools/__pycache__/base64_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/base_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/case_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/column_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/cron_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_history.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_processor.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_settings.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/diff_viewer.cpython-313.pyc +0 -0
- package/tools/__pycache__/email_extraction_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/email_header_analyzer.cpython-313.pyc +0 -0
- package/tools/__pycache__/extraction_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/find_replace.cpython-313.pyc +0 -0
- package/tools/__pycache__/folder_file_reporter.cpython-313.pyc +0 -0
- package/tools/__pycache__/folder_file_reporter_adapter.cpython-313.pyc +0 -0
- package/tools/__pycache__/generator_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/hash_generator.cpython-313.pyc +0 -0
- package/tools/__pycache__/html_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/huggingface_helper.cpython-313.pyc +0 -0
- package/tools/__pycache__/jsonxml_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/line_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/list_comparator.cpython-313.pyc +0 -0
- package/tools/__pycache__/markdown_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/mcp_widget.cpython-313.pyc +0 -0
- package/tools/__pycache__/notes_widget.cpython-313.pyc +0 -0
- package/tools/__pycache__/number_base_converter.cpython-313.pyc +0 -0
- package/tools/__pycache__/regex_extractor.cpython-313.pyc +0 -0
- package/tools/__pycache__/slug_generator.cpython-313.pyc +0 -0
- package/tools/__pycache__/sorter_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/string_escape_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/text_statistics_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/text_wrapper.cpython-313.pyc +0 -0
- package/tools/__pycache__/timestamp_converter.cpython-313.pyc +0 -0
- package/tools/__pycache__/tool_loader.cpython-313.pyc +0 -0
- package/tools/__pycache__/translator_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/url_link_extractor.cpython-313.pyc +0 -0
- package/tools/__pycache__/url_parser.cpython-313.pyc +0 -0
- package/tools/__pycache__/whitespace_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/word_frequency_counter.cpython-313.pyc +0 -0
package/core/error_handler.py
CHANGED
|
@@ -1,747 +1,747 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Comprehensive Error Handler for Settings Database Migration
|
|
3
|
-
|
|
4
|
-
This module provides comprehensive error handling, graceful degradation,
|
|
5
|
-
and fallback mechanisms for the database settings system. It ensures
|
|
6
|
-
the application remains stable even when database operations fail.
|
|
7
|
-
|
|
8
|
-
Features:
|
|
9
|
-
- Graceful degradation for database connection failures
|
|
10
|
-
- Fallback to JSON file system if database fails
|
|
11
|
-
- Error logging and notification systems
|
|
12
|
-
- Automatic recovery procedures for common failure modes
|
|
13
|
-
- Data validation and corruption detection
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
import json
|
|
17
|
-
import sqlite3
|
|
18
|
-
import logging
|
|
19
|
-
import os
|
|
20
|
-
import shutil
|
|
21
|
-
import threading
|
|
22
|
-
import time
|
|
23
|
-
from typing import Dict, List, Tuple, Any, Optional, Union, Callable
|
|
24
|
-
from datetime import datetime, timedelta
|
|
25
|
-
from pathlib import Path
|
|
26
|
-
from enum import Enum
|
|
27
|
-
from dataclasses import dataclass
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class ErrorSeverity(Enum):
|
|
31
|
-
"""Error severity levels for categorizing issues."""
|
|
32
|
-
LOW = "low"
|
|
33
|
-
MEDIUM = "medium"
|
|
34
|
-
HIGH = "high"
|
|
35
|
-
CRITICAL = "critical"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
class ErrorCategory(Enum):
|
|
39
|
-
"""Categories of errors that can occur."""
|
|
40
|
-
DATABASE_CONNECTION = "database_connection"
|
|
41
|
-
DATABASE_CORRUPTION = "database_corruption"
|
|
42
|
-
DISK_SPACE = "disk_space"
|
|
43
|
-
PERMISSION = "permission"
|
|
44
|
-
DATA_VALIDATION = "data_validation"
|
|
45
|
-
MIGRATION = "migration"
|
|
46
|
-
BACKUP = "backup"
|
|
47
|
-
RECOVERY = "recovery"
|
|
48
|
-
UNKNOWN = "unknown"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
@dataclass
|
|
52
|
-
class ErrorInfo:
|
|
53
|
-
"""Information about an error that occurred."""
|
|
54
|
-
timestamp: datetime
|
|
55
|
-
category: ErrorCategory
|
|
56
|
-
severity: ErrorSeverity
|
|
57
|
-
message: str
|
|
58
|
-
exception: Optional[Exception] = None
|
|
59
|
-
context: Optional[Dict[str, Any]] = None
|
|
60
|
-
recovery_attempted: bool = False
|
|
61
|
-
recovery_successful: bool = False
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
class SettingsErrorHandler:
|
|
65
|
-
"""
|
|
66
|
-
Comprehensive error handler for the settings database system.
|
|
67
|
-
|
|
68
|
-
Provides graceful degradation, fallback mechanisms, and automatic
|
|
69
|
-
recovery procedures to ensure application stability.
|
|
70
|
-
"""
|
|
71
|
-
|
|
72
|
-
def __init__(self, json_fallback_path: str = "settings.json",
|
|
73
|
-
backup_dir: str = "backups",
|
|
74
|
-
max_error_history: int = 1000):
|
|
75
|
-
"""
|
|
76
|
-
Initialize the error handler.
|
|
77
|
-
|
|
78
|
-
Args:
|
|
79
|
-
json_fallback_path: Path to JSON fallback file
|
|
80
|
-
backup_dir: Directory for backup files
|
|
81
|
-
max_error_history: Maximum number of errors to keep in history
|
|
82
|
-
"""
|
|
83
|
-
self.json_fallback_path = json_fallback_path
|
|
84
|
-
self.backup_dir = Path(backup_dir)
|
|
85
|
-
self.max_error_history = max_error_history
|
|
86
|
-
|
|
87
|
-
# Ensure backup directory exists
|
|
88
|
-
self.backup_dir.mkdir(parents=True, exist_ok=True)
|
|
89
|
-
|
|
90
|
-
# Error tracking
|
|
91
|
-
self._error_history: List[ErrorInfo] = []
|
|
92
|
-
self._error_counts: Dict[ErrorCategory, int] = {}
|
|
93
|
-
self._last_error_time: Dict[ErrorCategory, datetime] = {}
|
|
94
|
-
self._lock = threading.RLock()
|
|
95
|
-
|
|
96
|
-
# Recovery state
|
|
97
|
-
self._fallback_mode = False
|
|
98
|
-
self._fallback_settings: Optional[Dict[str, Any]] = None
|
|
99
|
-
self._recovery_in_progress = False
|
|
100
|
-
|
|
101
|
-
# Configuration
|
|
102
|
-
self._auto_recovery_enabled = True
|
|
103
|
-
self._fallback_enabled = True
|
|
104
|
-
self._notification_enabled = True
|
|
105
|
-
|
|
106
|
-
# Logger
|
|
107
|
-
self.logger = logging.getLogger(__name__)
|
|
108
|
-
|
|
109
|
-
# Error handlers for different categories
|
|
110
|
-
self._error_handlers = {
|
|
111
|
-
ErrorCategory.DATABASE_CONNECTION: self._handle_database_connection_error,
|
|
112
|
-
ErrorCategory.DATABASE_CORRUPTION: self._handle_database_corruption_error,
|
|
113
|
-
ErrorCategory.DISK_SPACE: self._handle_disk_space_error,
|
|
114
|
-
ErrorCategory.PERMISSION: self._handle_permission_error,
|
|
115
|
-
ErrorCategory.DATA_VALIDATION: self._handle_data_validation_error,
|
|
116
|
-
ErrorCategory.MIGRATION: self._handle_migration_error,
|
|
117
|
-
ErrorCategory.BACKUP: self._handle_backup_error,
|
|
118
|
-
ErrorCategory.RECOVERY: self._handle_recovery_error,
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
# Recovery procedures
|
|
122
|
-
self._recovery_procedures = {
|
|
123
|
-
ErrorCategory.DATABASE_CONNECTION: self._recover_database_connection,
|
|
124
|
-
ErrorCategory.DATABASE_CORRUPTION: self._recover_database_corruption,
|
|
125
|
-
ErrorCategory.DISK_SPACE: self._recover_disk_space,
|
|
126
|
-
ErrorCategory.PERMISSION: self._recover_permission,
|
|
127
|
-
ErrorCategory.DATA_VALIDATION: self._recover_data_validation,
|
|
128
|
-
ErrorCategory.MIGRATION: self._recover_migration,
|
|
129
|
-
ErrorCategory.BACKUP: self._recover_backup,
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
def handle_error(self, category: ErrorCategory, message: str,
|
|
133
|
-
exception: Optional[Exception] = None,
|
|
134
|
-
context: Optional[Dict[str, Any]] = None,
|
|
135
|
-
severity: Optional[ErrorSeverity] = None) -> bool:
|
|
136
|
-
"""
|
|
137
|
-
Handle an error with appropriate recovery procedures.
|
|
138
|
-
|
|
139
|
-
Args:
|
|
140
|
-
category: Category of the error
|
|
141
|
-
message: Error message
|
|
142
|
-
exception: Exception that caused the error (if any)
|
|
143
|
-
context: Additional context information
|
|
144
|
-
severity: Error severity (auto-determined if None)
|
|
145
|
-
|
|
146
|
-
Returns:
|
|
147
|
-
True if error was handled successfully, False otherwise
|
|
148
|
-
"""
|
|
149
|
-
try:
|
|
150
|
-
# Determine severity if not provided
|
|
151
|
-
if severity is None:
|
|
152
|
-
severity = self._determine_severity(category, exception)
|
|
153
|
-
|
|
154
|
-
# Create error info
|
|
155
|
-
error_info = ErrorInfo(
|
|
156
|
-
timestamp=datetime.now(),
|
|
157
|
-
category=category,
|
|
158
|
-
severity=severity,
|
|
159
|
-
message=message,
|
|
160
|
-
exception=exception,
|
|
161
|
-
context=context or {}
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
# Record the error
|
|
165
|
-
self._record_error(error_info)
|
|
166
|
-
|
|
167
|
-
# Log the error
|
|
168
|
-
self._log_error(error_info)
|
|
169
|
-
|
|
170
|
-
# Handle the error based on category
|
|
171
|
-
handler = self._error_handlers.get(category, self._handle_generic_error)
|
|
172
|
-
recovery_successful = handler(error_info)
|
|
173
|
-
|
|
174
|
-
# Update recovery status
|
|
175
|
-
error_info.recovery_attempted = True
|
|
176
|
-
error_info.recovery_successful = recovery_successful
|
|
177
|
-
|
|
178
|
-
# Send notification if enabled
|
|
179
|
-
if self._notification_enabled:
|
|
180
|
-
self._send_error_notification(error_info)
|
|
181
|
-
|
|
182
|
-
return recovery_successful
|
|
183
|
-
|
|
184
|
-
except Exception as e:
|
|
185
|
-
self.logger.critical(f"Error handler itself failed: {e}")
|
|
186
|
-
return False
|
|
187
|
-
|
|
188
|
-
def enable_fallback_mode(self, settings_data: Optional[Dict[str, Any]] = None) -> bool:
|
|
189
|
-
"""
|
|
190
|
-
Enable fallback mode using JSON file system.
|
|
191
|
-
|
|
192
|
-
Args:
|
|
193
|
-
settings_data: Settings data to use in fallback mode
|
|
194
|
-
|
|
195
|
-
Returns:
|
|
196
|
-
True if fallback mode enabled successfully
|
|
197
|
-
"""
|
|
198
|
-
try:
|
|
199
|
-
with self._lock:
|
|
200
|
-
self._fallback_mode = True
|
|
201
|
-
|
|
202
|
-
if settings_data:
|
|
203
|
-
self._fallback_settings = settings_data.copy()
|
|
204
|
-
else:
|
|
205
|
-
# Try to load from JSON file
|
|
206
|
-
self._fallback_settings = self._load_json_fallback()
|
|
207
|
-
|
|
208
|
-
if self._fallback_settings is None:
|
|
209
|
-
# Use minimal default settings
|
|
210
|
-
self._fallback_settings = self._get_minimal_default_settings()
|
|
211
|
-
|
|
212
|
-
self.logger.warning("Fallback mode enabled - using JSON file system")
|
|
213
|
-
return True
|
|
214
|
-
|
|
215
|
-
except Exception as e:
|
|
216
|
-
self.logger.error(f"Failed to enable fallback mode: {e}")
|
|
217
|
-
return False
|
|
218
|
-
|
|
219
|
-
def disable_fallback_mode(self) -> bool:
|
|
220
|
-
"""
|
|
221
|
-
Disable fallback mode and return to database system.
|
|
222
|
-
|
|
223
|
-
Returns:
|
|
224
|
-
True if fallback mode disabled successfully
|
|
225
|
-
"""
|
|
226
|
-
try:
|
|
227
|
-
with self._lock:
|
|
228
|
-
self._fallback_mode = False
|
|
229
|
-
self._fallback_settings = None
|
|
230
|
-
|
|
231
|
-
self.logger.info("Fallback mode disabled - returning to database system")
|
|
232
|
-
return True
|
|
233
|
-
|
|
234
|
-
except Exception as e:
|
|
235
|
-
self.logger.error(f"Failed to disable fallback mode: {e}")
|
|
236
|
-
return False
|
|
237
|
-
|
|
238
|
-
def is_fallback_mode(self) -> bool:
|
|
239
|
-
"""Check if currently in fallback mode."""
|
|
240
|
-
return self._fallback_mode
|
|
241
|
-
|
|
242
|
-
def get_fallback_settings(self) -> Optional[Dict[str, Any]]:
|
|
243
|
-
"""Get current fallback settings."""
|
|
244
|
-
return self._fallback_settings.copy() if self._fallback_settings else None
|
|
245
|
-
|
|
246
|
-
def save_fallback_settings(self, settings: Dict[str, Any]) -> bool:
|
|
247
|
-
"""
|
|
248
|
-
Save settings in fallback mode.
|
|
249
|
-
|
|
250
|
-
Args:
|
|
251
|
-
settings: Settings to save
|
|
252
|
-
|
|
253
|
-
Returns:
|
|
254
|
-
True if saved successfully
|
|
255
|
-
"""
|
|
256
|
-
try:
|
|
257
|
-
if not self._fallback_mode:
|
|
258
|
-
return False
|
|
259
|
-
|
|
260
|
-
with self._lock:
|
|
261
|
-
# Update in-memory fallback settings
|
|
262
|
-
self._fallback_settings = settings.copy()
|
|
263
|
-
|
|
264
|
-
# Save to JSON file
|
|
265
|
-
return self._save_json_fallback(settings)
|
|
266
|
-
|
|
267
|
-
except Exception as e:
|
|
268
|
-
self.logger.error(f"Failed to save fallback settings: {e}")
|
|
269
|
-
return False
|
|
270
|
-
|
|
271
|
-
def attempt_recovery(self, category: ErrorCategory) -> bool:
|
|
272
|
-
"""
|
|
273
|
-
Attempt recovery for a specific error category.
|
|
274
|
-
|
|
275
|
-
Args:
|
|
276
|
-
category: Error category to recover from
|
|
277
|
-
|
|
278
|
-
Returns:
|
|
279
|
-
True if recovery successful
|
|
280
|
-
"""
|
|
281
|
-
if self._recovery_in_progress:
|
|
282
|
-
self.logger.warning("Recovery already in progress")
|
|
283
|
-
return False
|
|
284
|
-
|
|
285
|
-
try:
|
|
286
|
-
self._recovery_in_progress = True
|
|
287
|
-
|
|
288
|
-
recovery_proc = self._recovery_procedures.get(category)
|
|
289
|
-
if recovery_proc:
|
|
290
|
-
success = recovery_proc()
|
|
291
|
-
if success:
|
|
292
|
-
self.logger.info(f"Recovery successful for {category.value}")
|
|
293
|
-
else:
|
|
294
|
-
self.logger.error(f"Recovery failed for {category.value}")
|
|
295
|
-
return success
|
|
296
|
-
else:
|
|
297
|
-
self.logger.warning(f"No recovery procedure for {category.value}")
|
|
298
|
-
return False
|
|
299
|
-
|
|
300
|
-
except Exception as e:
|
|
301
|
-
self.logger.error(f"Recovery attempt failed: {e}")
|
|
302
|
-
return False
|
|
303
|
-
finally:
|
|
304
|
-
self._recovery_in_progress = False
|
|
305
|
-
|
|
306
|
-
def get_error_statistics(self) -> Dict[str, Any]:
|
|
307
|
-
"""
|
|
308
|
-
Get error statistics and health information.
|
|
309
|
-
|
|
310
|
-
Returns:
|
|
311
|
-
Dictionary with error statistics
|
|
312
|
-
"""
|
|
313
|
-
with self._lock:
|
|
314
|
-
total_errors = len(self._error_history)
|
|
315
|
-
recent_errors = [e for e in self._error_history
|
|
316
|
-
if e.timestamp > datetime.now() - timedelta(hours=24)]
|
|
317
|
-
|
|
318
|
-
critical_errors = [e for e in self._error_history
|
|
319
|
-
if e.severity == ErrorSeverity.CRITICAL]
|
|
320
|
-
|
|
321
|
-
stats = {
|
|
322
|
-
'total_errors': total_errors,
|
|
323
|
-
'recent_errors_24h': len(recent_errors),
|
|
324
|
-
'critical_errors': len(critical_errors),
|
|
325
|
-
'fallback_mode': self._fallback_mode,
|
|
326
|
-
'recovery_in_progress': self._recovery_in_progress,
|
|
327
|
-
'error_counts_by_category': dict(self._error_counts),
|
|
328
|
-
'last_error_times': {
|
|
329
|
-
cat.value: time.isoformat()
|
|
330
|
-
for cat, time in self._last_error_time.items()
|
|
331
|
-
},
|
|
332
|
-
'most_common_errors': self._get_most_common_errors(10)
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
return stats
|
|
336
|
-
|
|
337
|
-
def clear_error_history(self) -> None:
|
|
338
|
-
"""Clear error history."""
|
|
339
|
-
with self._lock:
|
|
340
|
-
self._error_history.clear()
|
|
341
|
-
self._error_counts.clear()
|
|
342
|
-
self._last_error_time.clear()
|
|
343
|
-
self.logger.info("Error history cleared")
|
|
344
|
-
|
|
345
|
-
def validate_database_integrity(self, connection_manager) -> List[str]:
|
|
346
|
-
"""
|
|
347
|
-
Validate database integrity and detect corruption.
|
|
348
|
-
|
|
349
|
-
Args:
|
|
350
|
-
connection_manager: Database connection manager
|
|
351
|
-
|
|
352
|
-
Returns:
|
|
353
|
-
List of integrity issues found
|
|
354
|
-
"""
|
|
355
|
-
issues = []
|
|
356
|
-
|
|
357
|
-
try:
|
|
358
|
-
conn = connection_manager.get_connection()
|
|
359
|
-
|
|
360
|
-
# Check database integrity
|
|
361
|
-
cursor = conn.execute("PRAGMA integrity_check")
|
|
362
|
-
integrity_result = cursor.fetchone()[0]
|
|
363
|
-
|
|
364
|
-
if integrity_result != "ok":
|
|
365
|
-
issues.append(f"Database integrity check failed: {integrity_result}")
|
|
366
|
-
|
|
367
|
-
# Check foreign key constraints
|
|
368
|
-
cursor = conn.execute("PRAGMA foreign_key_check")
|
|
369
|
-
fk_violations = cursor.fetchall()
|
|
370
|
-
|
|
371
|
-
if fk_violations:
|
|
372
|
-
issues.append(f"Foreign key violations found: {len(fk_violations)}")
|
|
373
|
-
|
|
374
|
-
# Check table existence
|
|
375
|
-
required_tables = [
|
|
376
|
-
'core_settings', 'tool_settings', 'tab_content',
|
|
377
|
-
'performance_settings', 'font_settings', 'dialog_settings',
|
|
378
|
-
'settings_metadata'
|
|
379
|
-
]
|
|
380
|
-
|
|
381
|
-
cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
|
382
|
-
existing_tables = {row[0] for row in cursor.fetchall()}
|
|
383
|
-
|
|
384
|
-
missing_tables = set(required_tables) - existing_tables
|
|
385
|
-
if missing_tables:
|
|
386
|
-
issues.append(f"Missing tables: {', '.join(missing_tables)}")
|
|
387
|
-
|
|
388
|
-
# Check for empty critical tables
|
|
389
|
-
for table in ['core_settings']:
|
|
390
|
-
cursor = conn.execute(f"SELECT COUNT(*) FROM {table}")
|
|
391
|
-
count = cursor.fetchone()[0]
|
|
392
|
-
if count == 0:
|
|
393
|
-
issues.append(f"Critical table {table} is empty")
|
|
394
|
-
|
|
395
|
-
except Exception as e:
|
|
396
|
-
issues.append(f"Database validation failed: {e}")
|
|
397
|
-
|
|
398
|
-
return issues
|
|
399
|
-
|
|
400
|
-
# Private methods for error handling
|
|
401
|
-
|
|
402
|
-
def _record_error(self, error_info: ErrorInfo) -> None:
|
|
403
|
-
"""Record error in history."""
|
|
404
|
-
with self._lock:
|
|
405
|
-
self._error_history.append(error_info)
|
|
406
|
-
|
|
407
|
-
# Update counts
|
|
408
|
-
category = error_info.category
|
|
409
|
-
self._error_counts[category] = self._error_counts.get(category, 0) + 1
|
|
410
|
-
self._last_error_time[category] = error_info.timestamp
|
|
411
|
-
|
|
412
|
-
# Trim history if too large
|
|
413
|
-
if len(self._error_history) > self.max_error_history:
|
|
414
|
-
self._error_history = self._error_history[-self.max_error_history:]
|
|
415
|
-
|
|
416
|
-
def _log_error(self, error_info: ErrorInfo) -> None:
|
|
417
|
-
"""Log error with appropriate level."""
|
|
418
|
-
log_message = f"[{error_info.category.value}] {error_info.message}"
|
|
419
|
-
|
|
420
|
-
if error_info.exception:
|
|
421
|
-
log_message += f" - Exception: {error_info.exception}"
|
|
422
|
-
|
|
423
|
-
if error_info.context:
|
|
424
|
-
log_message += f" - Context: {error_info.context}"
|
|
425
|
-
|
|
426
|
-
if error_info.severity == ErrorSeverity.CRITICAL:
|
|
427
|
-
self.logger.critical(log_message)
|
|
428
|
-
elif error_info.severity == ErrorSeverity.HIGH:
|
|
429
|
-
self.logger.error(log_message)
|
|
430
|
-
elif error_info.severity == ErrorSeverity.MEDIUM:
|
|
431
|
-
self.logger.warning(log_message)
|
|
432
|
-
else:
|
|
433
|
-
self.logger.info(log_message)
|
|
434
|
-
|
|
435
|
-
def _determine_severity(self, category: ErrorCategory,
|
|
436
|
-
exception: Optional[Exception]) -> ErrorSeverity:
|
|
437
|
-
"""Determine error severity based on category and exception."""
|
|
438
|
-
# Critical errors that prevent core functionality
|
|
439
|
-
if category in [ErrorCategory.DATABASE_CORRUPTION, ErrorCategory.MIGRATION]:
|
|
440
|
-
return ErrorSeverity.CRITICAL
|
|
441
|
-
|
|
442
|
-
# High severity errors that significantly impact functionality
|
|
443
|
-
if category in [ErrorCategory.DATABASE_CONNECTION, ErrorCategory.PERMISSION]:
|
|
444
|
-
return ErrorSeverity.HIGH
|
|
445
|
-
|
|
446
|
-
# Medium severity errors that cause inconvenience
|
|
447
|
-
if category in [ErrorCategory.DISK_SPACE, ErrorCategory.BACKUP]:
|
|
448
|
-
return ErrorSeverity.MEDIUM
|
|
449
|
-
|
|
450
|
-
# Check exception type for additional context
|
|
451
|
-
if isinstance(exception, (sqlite3.DatabaseError, sqlite3.CorruptError)):
|
|
452
|
-
return ErrorSeverity.CRITICAL
|
|
453
|
-
elif isinstance(exception, (PermissionError, OSError)):
|
|
454
|
-
return ErrorSeverity.HIGH
|
|
455
|
-
|
|
456
|
-
return ErrorSeverity.LOW
|
|
457
|
-
|
|
458
|
-
def _send_error_notification(self, error_info: ErrorInfo) -> None:
|
|
459
|
-
"""Send error notification (placeholder for future implementation)."""
|
|
460
|
-
# This could be extended to send notifications via:
|
|
461
|
-
# - System notifications
|
|
462
|
-
# - Email alerts
|
|
463
|
-
# - Logging to external systems
|
|
464
|
-
# - UI notifications
|
|
465
|
-
pass
|
|
466
|
-
|
|
467
|
-
# Error handlers for specific categories
|
|
468
|
-
|
|
469
|
-
def _handle_database_connection_error(self, error_info: ErrorInfo) -> bool:
|
|
470
|
-
"""Handle database connection errors."""
|
|
471
|
-
self.logger.warning("Database connection error - attempting fallback")
|
|
472
|
-
|
|
473
|
-
# Enable fallback mode
|
|
474
|
-
if self._fallback_enabled:
|
|
475
|
-
return self.enable_fallback_mode()
|
|
476
|
-
|
|
477
|
-
return False
|
|
478
|
-
|
|
479
|
-
def _handle_database_corruption_error(self, error_info: ErrorInfo) -> bool:
|
|
480
|
-
"""Handle database corruption errors."""
|
|
481
|
-
self.logger.error("Database corruption detected - attempting recovery")
|
|
482
|
-
|
|
483
|
-
# Try to recover from backup
|
|
484
|
-
if self._auto_recovery_enabled:
|
|
485
|
-
return self.attempt_recovery(ErrorCategory.DATABASE_CORRUPTION)
|
|
486
|
-
|
|
487
|
-
# Fall back to JSON if recovery not enabled
|
|
488
|
-
if self._fallback_enabled:
|
|
489
|
-
return self.enable_fallback_mode()
|
|
490
|
-
|
|
491
|
-
return False
|
|
492
|
-
|
|
493
|
-
def _handle_disk_space_error(self, error_info: ErrorInfo) -> bool:
|
|
494
|
-
"""Handle disk space errors."""
|
|
495
|
-
self.logger.warning("Disk space error - continuing with in-memory operations")
|
|
496
|
-
|
|
497
|
-
# Continue with in-memory operations, disable backups temporarily
|
|
498
|
-
return True
|
|
499
|
-
|
|
500
|
-
def _handle_permission_error(self, error_info: ErrorInfo) -> bool:
|
|
501
|
-
"""Handle permission errors."""
|
|
502
|
-
self.logger.error("Permission error - attempting fallback")
|
|
503
|
-
|
|
504
|
-
# Try fallback mode
|
|
505
|
-
if self._fallback_enabled:
|
|
506
|
-
return self.enable_fallback_mode()
|
|
507
|
-
|
|
508
|
-
return False
|
|
509
|
-
|
|
510
|
-
def _handle_data_validation_error(self, error_info: ErrorInfo) -> bool:
|
|
511
|
-
"""Handle data validation errors."""
|
|
512
|
-
self.logger.warning("Data validation error - using default values")
|
|
513
|
-
|
|
514
|
-
# Continue with default values for invalid data
|
|
515
|
-
return True
|
|
516
|
-
|
|
517
|
-
def _handle_migration_error(self, error_info: ErrorInfo) -> bool:
|
|
518
|
-
"""Handle migration errors."""
|
|
519
|
-
self.logger.error("Migration error - attempting rollback")
|
|
520
|
-
|
|
521
|
-
# Try to rollback migration
|
|
522
|
-
if self._auto_recovery_enabled:
|
|
523
|
-
return self.attempt_recovery(ErrorCategory.MIGRATION)
|
|
524
|
-
|
|
525
|
-
return False
|
|
526
|
-
|
|
527
|
-
def _handle_backup_error(self, error_info: ErrorInfo) -> bool:
|
|
528
|
-
"""Handle backup errors."""
|
|
529
|
-
self.logger.warning("Backup error - continuing without backup")
|
|
530
|
-
|
|
531
|
-
# Continue operations without backup
|
|
532
|
-
return True
|
|
533
|
-
|
|
534
|
-
def _handle_recovery_error(self, error_info: ErrorInfo) -> bool:
|
|
535
|
-
"""Handle recovery errors."""
|
|
536
|
-
self.logger.error("Recovery error - enabling fallback mode")
|
|
537
|
-
|
|
538
|
-
# Enable fallback as last resort
|
|
539
|
-
if self._fallback_enabled:
|
|
540
|
-
return self.enable_fallback_mode()
|
|
541
|
-
|
|
542
|
-
return False
|
|
543
|
-
|
|
544
|
-
def _handle_generic_error(self, error_info: ErrorInfo) -> bool:
|
|
545
|
-
"""Handle generic/unknown errors."""
|
|
546
|
-
self.logger.warning(f"Generic error: {error_info.message}")
|
|
547
|
-
|
|
548
|
-
# Try fallback for unknown errors
|
|
549
|
-
if error_info.severity in [ErrorSeverity.HIGH, ErrorSeverity.CRITICAL]:
|
|
550
|
-
if self._fallback_enabled:
|
|
551
|
-
return self.enable_fallback_mode()
|
|
552
|
-
|
|
553
|
-
return True
|
|
554
|
-
|
|
555
|
-
# Recovery procedures
|
|
556
|
-
|
|
557
|
-
def _recover_database_connection(self) -> bool:
|
|
558
|
-
"""Recover from database connection issues."""
|
|
559
|
-
try:
|
|
560
|
-
# Wait a moment and retry connection
|
|
561
|
-
time.sleep(1)
|
|
562
|
-
|
|
563
|
-
# This would be implemented by the connection manager
|
|
564
|
-
# For now, just return success to indicate recovery attempt
|
|
565
|
-
self.logger.info("Database connection recovery attempted")
|
|
566
|
-
return True
|
|
567
|
-
|
|
568
|
-
except Exception as e:
|
|
569
|
-
self.logger.error(f"Database connection recovery failed: {e}")
|
|
570
|
-
return False
|
|
571
|
-
|
|
572
|
-
def _recover_database_corruption(self) -> bool:
|
|
573
|
-
"""Recover from database corruption."""
|
|
574
|
-
try:
|
|
575
|
-
# Try to restore from most recent backup
|
|
576
|
-
backup_files = list(self.backup_dir.glob("settings_backup_*.db"))
|
|
577
|
-
if backup_files:
|
|
578
|
-
# Sort by modification time, get most recent
|
|
579
|
-
latest_backup = max(backup_files, key=lambda p: p.stat().st_mtime)
|
|
580
|
-
|
|
581
|
-
self.logger.info(f"Attempting to restore from backup: {latest_backup}")
|
|
582
|
-
# This would be implemented by the connection manager
|
|
583
|
-
return True
|
|
584
|
-
else:
|
|
585
|
-
self.logger.error("No backup files found for recovery")
|
|
586
|
-
return False
|
|
587
|
-
|
|
588
|
-
except Exception as e:
|
|
589
|
-
self.logger.error(f"Database corruption recovery failed: {e}")
|
|
590
|
-
return False
|
|
591
|
-
|
|
592
|
-
def _recover_disk_space(self) -> bool:
|
|
593
|
-
"""Recover from disk space issues."""
|
|
594
|
-
try:
|
|
595
|
-
# Clean up old backup files
|
|
596
|
-
backup_files = list(self.backup_dir.glob("settings_backup_*.db"))
|
|
597
|
-
if len(backup_files) > 5: # Keep only 5 most recent
|
|
598
|
-
old_backups = sorted(backup_files, key=lambda p: p.stat().st_mtime)[:-5]
|
|
599
|
-
for backup in old_backups:
|
|
600
|
-
backup.unlink()
|
|
601
|
-
self.logger.info(f"Cleaned up old backup: {backup}")
|
|
602
|
-
|
|
603
|
-
return True
|
|
604
|
-
|
|
605
|
-
except Exception as e:
|
|
606
|
-
self.logger.error(f"Disk space recovery failed: {e}")
|
|
607
|
-
return False
|
|
608
|
-
|
|
609
|
-
def _recover_permission(self) -> bool:
|
|
610
|
-
"""Recover from permission issues."""
|
|
611
|
-
try:
|
|
612
|
-
# Try to change file permissions if possible
|
|
613
|
-
# This is a placeholder - actual implementation would depend on OS
|
|
614
|
-
self.logger.info("Permission recovery attempted")
|
|
615
|
-
return False # Usually requires manual intervention
|
|
616
|
-
|
|
617
|
-
except Exception as e:
|
|
618
|
-
self.logger.error(f"Permission recovery failed: {e}")
|
|
619
|
-
return False
|
|
620
|
-
|
|
621
|
-
def _recover_data_validation(self) -> bool:
|
|
622
|
-
"""Recover from data validation issues."""
|
|
623
|
-
try:
|
|
624
|
-
# Reset to default values for invalid data
|
|
625
|
-
self.logger.info("Data validation recovery - using defaults")
|
|
626
|
-
return True
|
|
627
|
-
|
|
628
|
-
except Exception as e:
|
|
629
|
-
self.logger.error(f"Data validation recovery failed: {e}")
|
|
630
|
-
return False
|
|
631
|
-
|
|
632
|
-
def _recover_migration(self) -> bool:
|
|
633
|
-
"""Recover from migration issues."""
|
|
634
|
-
try:
|
|
635
|
-
# Try to rollback to original JSON file
|
|
636
|
-
backup_files = list(Path(".").glob("settings.json.backup_*"))
|
|
637
|
-
if backup_files:
|
|
638
|
-
latest_backup = max(backup_files, key=lambda p: p.stat().st_mtime)
|
|
639
|
-
shutil.copy2(latest_backup, self.json_fallback_path)
|
|
640
|
-
self.logger.info(f"Migration rollback completed: {latest_backup}")
|
|
641
|
-
return True
|
|
642
|
-
else:
|
|
643
|
-
self.logger.error("No JSON backup found for migration rollback")
|
|
644
|
-
return False
|
|
645
|
-
|
|
646
|
-
except Exception as e:
|
|
647
|
-
self.logger.error(f"Migration recovery failed: {e}")
|
|
648
|
-
return False
|
|
649
|
-
|
|
650
|
-
def _recover_backup(self) -> bool:
|
|
651
|
-
"""Recover from backup issues."""
|
|
652
|
-
try:
|
|
653
|
-
# Try alternative backup location or method
|
|
654
|
-
self.logger.info("Backup recovery attempted")
|
|
655
|
-
return True
|
|
656
|
-
|
|
657
|
-
except Exception as e:
|
|
658
|
-
self.logger.error(f"Backup recovery failed: {e}")
|
|
659
|
-
return False
|
|
660
|
-
|
|
661
|
-
# Fallback JSON operations
|
|
662
|
-
|
|
663
|
-
def _load_json_fallback(self) -> Optional[Dict[str, Any]]:
|
|
664
|
-
"""Load settings from JSON fallback file."""
|
|
665
|
-
try:
|
|
666
|
-
if os.path.exists(self.json_fallback_path):
|
|
667
|
-
with open(self.json_fallback_path, 'r', encoding='utf-8') as f:
|
|
668
|
-
return json.load(f)
|
|
669
|
-
else:
|
|
670
|
-
self.logger.warning(f"JSON fallback file not found: {self.json_fallback_path}")
|
|
671
|
-
return None
|
|
672
|
-
|
|
673
|
-
except Exception as e:
|
|
674
|
-
self.logger.error(f"Failed to load JSON fallback: {e}")
|
|
675
|
-
return None
|
|
676
|
-
|
|
677
|
-
def _save_json_fallback(self, settings: Dict[str, Any]) -> bool:
|
|
678
|
-
"""Save settings to JSON fallback file."""
|
|
679
|
-
try:
|
|
680
|
-
with open(self.json_fallback_path, 'w', encoding='utf-8') as f:
|
|
681
|
-
json.dump(settings, f, indent=2, ensure_ascii=False)
|
|
682
|
-
return True
|
|
683
|
-
|
|
684
|
-
except Exception as e:
|
|
685
|
-
self.logger.error(f"Failed to save JSON fallback: {e}")
|
|
686
|
-
return False
|
|
687
|
-
|
|
688
|
-
def _get_minimal_default_settings(self) -> Dict[str, Any]:
|
|
689
|
-
"""Get minimal default settings for emergency fallback."""
|
|
690
|
-
return {
|
|
691
|
-
"export_path": str(Path.home() / "Downloads"),
|
|
692
|
-
"debug_level": "INFO",
|
|
693
|
-
"selected_tool": "Case Tool",
|
|
694
|
-
"active_input_tab": 0,
|
|
695
|
-
"active_output_tab": 0,
|
|
696
|
-
"input_tabs": [""] * 7,
|
|
697
|
-
"output_tabs": [""] * 7,
|
|
698
|
-
"tool_settings": {},
|
|
699
|
-
"performance_settings": {"mode": "automatic"},
|
|
700
|
-
"font_settings": {"text_font": {"family": "Consolas", "size": 11}},
|
|
701
|
-
"dialog_settings": {"error": {"enabled": True, "locked": True}}
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
def _get_most_common_errors(self, limit: int) -> List[Dict[str, Any]]:
|
|
705
|
-
"""Get most common error categories."""
|
|
706
|
-
error_counts = {}
|
|
707
|
-
|
|
708
|
-
for error in self._error_history:
|
|
709
|
-
key = f"{error.category.value}: {error.message[:50]}"
|
|
710
|
-
error_counts[key] = error_counts.get(key, 0) + 1
|
|
711
|
-
|
|
712
|
-
# Sort by count and return top N
|
|
713
|
-
sorted_errors = sorted(error_counts.items(), key=lambda x: x[1], reverse=True)
|
|
714
|
-
|
|
715
|
-
return [
|
|
716
|
-
{"error": error, "count": count}
|
|
717
|
-
for error, count in sorted_errors[:limit]
|
|
718
|
-
]
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
# Global error handler instance
|
|
722
|
-
_global_error_handler: Optional[SettingsErrorHandler] = None
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
def get_error_handler() -> SettingsErrorHandler:
|
|
726
|
-
"""Get the global error handler instance."""
|
|
727
|
-
global _global_error_handler
|
|
728
|
-
if _global_error_handler is None:
|
|
729
|
-
_global_error_handler = SettingsErrorHandler()
|
|
730
|
-
return _global_error_handler
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
def initialize_error_handler(json_fallback_path: str = "settings.json",
|
|
734
|
-
backup_dir: str = "backups") -> SettingsErrorHandler:
|
|
735
|
-
"""
|
|
736
|
-
Initialize the global error handler.
|
|
737
|
-
|
|
738
|
-
Args:
|
|
739
|
-
json_fallback_path: Path to JSON fallback file
|
|
740
|
-
backup_dir: Directory for backup files
|
|
741
|
-
|
|
742
|
-
Returns:
|
|
743
|
-
Initialized error handler instance
|
|
744
|
-
"""
|
|
745
|
-
global _global_error_handler
|
|
746
|
-
_global_error_handler = SettingsErrorHandler(json_fallback_path, backup_dir)
|
|
1
|
+
"""
|
|
2
|
+
Comprehensive Error Handler for Settings Database Migration
|
|
3
|
+
|
|
4
|
+
This module provides comprehensive error handling, graceful degradation,
|
|
5
|
+
and fallback mechanisms for the database settings system. It ensures
|
|
6
|
+
the application remains stable even when database operations fail.
|
|
7
|
+
|
|
8
|
+
Features:
|
|
9
|
+
- Graceful degradation for database connection failures
|
|
10
|
+
- Fallback to JSON file system if database fails
|
|
11
|
+
- Error logging and notification systems
|
|
12
|
+
- Automatic recovery procedures for common failure modes
|
|
13
|
+
- Data validation and corruption detection
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import sqlite3
|
|
18
|
+
import logging
|
|
19
|
+
import os
|
|
20
|
+
import shutil
|
|
21
|
+
import threading
|
|
22
|
+
import time
|
|
23
|
+
from typing import Dict, List, Tuple, Any, Optional, Union, Callable
|
|
24
|
+
from datetime import datetime, timedelta
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from enum import Enum
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ErrorSeverity(Enum):
|
|
31
|
+
"""Error severity levels for categorizing issues."""
|
|
32
|
+
LOW = "low"
|
|
33
|
+
MEDIUM = "medium"
|
|
34
|
+
HIGH = "high"
|
|
35
|
+
CRITICAL = "critical"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ErrorCategory(Enum):
|
|
39
|
+
"""Categories of errors that can occur."""
|
|
40
|
+
DATABASE_CONNECTION = "database_connection"
|
|
41
|
+
DATABASE_CORRUPTION = "database_corruption"
|
|
42
|
+
DISK_SPACE = "disk_space"
|
|
43
|
+
PERMISSION = "permission"
|
|
44
|
+
DATA_VALIDATION = "data_validation"
|
|
45
|
+
MIGRATION = "migration"
|
|
46
|
+
BACKUP = "backup"
|
|
47
|
+
RECOVERY = "recovery"
|
|
48
|
+
UNKNOWN = "unknown"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class ErrorInfo:
|
|
53
|
+
"""Information about an error that occurred."""
|
|
54
|
+
timestamp: datetime
|
|
55
|
+
category: ErrorCategory
|
|
56
|
+
severity: ErrorSeverity
|
|
57
|
+
message: str
|
|
58
|
+
exception: Optional[Exception] = None
|
|
59
|
+
context: Optional[Dict[str, Any]] = None
|
|
60
|
+
recovery_attempted: bool = False
|
|
61
|
+
recovery_successful: bool = False
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class SettingsErrorHandler:
|
|
65
|
+
"""
|
|
66
|
+
Comprehensive error handler for the settings database system.
|
|
67
|
+
|
|
68
|
+
Provides graceful degradation, fallback mechanisms, and automatic
|
|
69
|
+
recovery procedures to ensure application stability.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(self, json_fallback_path: str = "settings.json",
|
|
73
|
+
backup_dir: str = "backups",
|
|
74
|
+
max_error_history: int = 1000):
|
|
75
|
+
"""
|
|
76
|
+
Initialize the error handler.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
json_fallback_path: Path to JSON fallback file
|
|
80
|
+
backup_dir: Directory for backup files
|
|
81
|
+
max_error_history: Maximum number of errors to keep in history
|
|
82
|
+
"""
|
|
83
|
+
self.json_fallback_path = json_fallback_path
|
|
84
|
+
self.backup_dir = Path(backup_dir)
|
|
85
|
+
self.max_error_history = max_error_history
|
|
86
|
+
|
|
87
|
+
# Ensure backup directory exists
|
|
88
|
+
self.backup_dir.mkdir(parents=True, exist_ok=True)
|
|
89
|
+
|
|
90
|
+
# Error tracking
|
|
91
|
+
self._error_history: List[ErrorInfo] = []
|
|
92
|
+
self._error_counts: Dict[ErrorCategory, int] = {}
|
|
93
|
+
self._last_error_time: Dict[ErrorCategory, datetime] = {}
|
|
94
|
+
self._lock = threading.RLock()
|
|
95
|
+
|
|
96
|
+
# Recovery state
|
|
97
|
+
self._fallback_mode = False
|
|
98
|
+
self._fallback_settings: Optional[Dict[str, Any]] = None
|
|
99
|
+
self._recovery_in_progress = False
|
|
100
|
+
|
|
101
|
+
# Configuration
|
|
102
|
+
self._auto_recovery_enabled = True
|
|
103
|
+
self._fallback_enabled = True
|
|
104
|
+
self._notification_enabled = True
|
|
105
|
+
|
|
106
|
+
# Logger
|
|
107
|
+
self.logger = logging.getLogger(__name__)
|
|
108
|
+
|
|
109
|
+
# Error handlers for different categories
|
|
110
|
+
self._error_handlers = {
|
|
111
|
+
ErrorCategory.DATABASE_CONNECTION: self._handle_database_connection_error,
|
|
112
|
+
ErrorCategory.DATABASE_CORRUPTION: self._handle_database_corruption_error,
|
|
113
|
+
ErrorCategory.DISK_SPACE: self._handle_disk_space_error,
|
|
114
|
+
ErrorCategory.PERMISSION: self._handle_permission_error,
|
|
115
|
+
ErrorCategory.DATA_VALIDATION: self._handle_data_validation_error,
|
|
116
|
+
ErrorCategory.MIGRATION: self._handle_migration_error,
|
|
117
|
+
ErrorCategory.BACKUP: self._handle_backup_error,
|
|
118
|
+
ErrorCategory.RECOVERY: self._handle_recovery_error,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# Recovery procedures
|
|
122
|
+
self._recovery_procedures = {
|
|
123
|
+
ErrorCategory.DATABASE_CONNECTION: self._recover_database_connection,
|
|
124
|
+
ErrorCategory.DATABASE_CORRUPTION: self._recover_database_corruption,
|
|
125
|
+
ErrorCategory.DISK_SPACE: self._recover_disk_space,
|
|
126
|
+
ErrorCategory.PERMISSION: self._recover_permission,
|
|
127
|
+
ErrorCategory.DATA_VALIDATION: self._recover_data_validation,
|
|
128
|
+
ErrorCategory.MIGRATION: self._recover_migration,
|
|
129
|
+
ErrorCategory.BACKUP: self._recover_backup,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
def handle_error(self, category: ErrorCategory, message: str,
|
|
133
|
+
exception: Optional[Exception] = None,
|
|
134
|
+
context: Optional[Dict[str, Any]] = None,
|
|
135
|
+
severity: Optional[ErrorSeverity] = None) -> bool:
|
|
136
|
+
"""
|
|
137
|
+
Handle an error with appropriate recovery procedures.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
category: Category of the error
|
|
141
|
+
message: Error message
|
|
142
|
+
exception: Exception that caused the error (if any)
|
|
143
|
+
context: Additional context information
|
|
144
|
+
severity: Error severity (auto-determined if None)
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
True if error was handled successfully, False otherwise
|
|
148
|
+
"""
|
|
149
|
+
try:
|
|
150
|
+
# Determine severity if not provided
|
|
151
|
+
if severity is None:
|
|
152
|
+
severity = self._determine_severity(category, exception)
|
|
153
|
+
|
|
154
|
+
# Create error info
|
|
155
|
+
error_info = ErrorInfo(
|
|
156
|
+
timestamp=datetime.now(),
|
|
157
|
+
category=category,
|
|
158
|
+
severity=severity,
|
|
159
|
+
message=message,
|
|
160
|
+
exception=exception,
|
|
161
|
+
context=context or {}
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Record the error
|
|
165
|
+
self._record_error(error_info)
|
|
166
|
+
|
|
167
|
+
# Log the error
|
|
168
|
+
self._log_error(error_info)
|
|
169
|
+
|
|
170
|
+
# Handle the error based on category
|
|
171
|
+
handler = self._error_handlers.get(category, self._handle_generic_error)
|
|
172
|
+
recovery_successful = handler(error_info)
|
|
173
|
+
|
|
174
|
+
# Update recovery status
|
|
175
|
+
error_info.recovery_attempted = True
|
|
176
|
+
error_info.recovery_successful = recovery_successful
|
|
177
|
+
|
|
178
|
+
# Send notification if enabled
|
|
179
|
+
if self._notification_enabled:
|
|
180
|
+
self._send_error_notification(error_info)
|
|
181
|
+
|
|
182
|
+
return recovery_successful
|
|
183
|
+
|
|
184
|
+
except Exception as e:
|
|
185
|
+
self.logger.critical(f"Error handler itself failed: {e}")
|
|
186
|
+
return False
|
|
187
|
+
|
|
188
|
+
def enable_fallback_mode(self, settings_data: Optional[Dict[str, Any]] = None) -> bool:
|
|
189
|
+
"""
|
|
190
|
+
Enable fallback mode using JSON file system.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
settings_data: Settings data to use in fallback mode
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
True if fallback mode enabled successfully
|
|
197
|
+
"""
|
|
198
|
+
try:
|
|
199
|
+
with self._lock:
|
|
200
|
+
self._fallback_mode = True
|
|
201
|
+
|
|
202
|
+
if settings_data:
|
|
203
|
+
self._fallback_settings = settings_data.copy()
|
|
204
|
+
else:
|
|
205
|
+
# Try to load from JSON file
|
|
206
|
+
self._fallback_settings = self._load_json_fallback()
|
|
207
|
+
|
|
208
|
+
if self._fallback_settings is None:
|
|
209
|
+
# Use minimal default settings
|
|
210
|
+
self._fallback_settings = self._get_minimal_default_settings()
|
|
211
|
+
|
|
212
|
+
self.logger.warning("Fallback mode enabled - using JSON file system")
|
|
213
|
+
return True
|
|
214
|
+
|
|
215
|
+
except Exception as e:
|
|
216
|
+
self.logger.error(f"Failed to enable fallback mode: {e}")
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
def disable_fallback_mode(self) -> bool:
|
|
220
|
+
"""
|
|
221
|
+
Disable fallback mode and return to database system.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
True if fallback mode disabled successfully
|
|
225
|
+
"""
|
|
226
|
+
try:
|
|
227
|
+
with self._lock:
|
|
228
|
+
self._fallback_mode = False
|
|
229
|
+
self._fallback_settings = None
|
|
230
|
+
|
|
231
|
+
self.logger.info("Fallback mode disabled - returning to database system")
|
|
232
|
+
return True
|
|
233
|
+
|
|
234
|
+
except Exception as e:
|
|
235
|
+
self.logger.error(f"Failed to disable fallback mode: {e}")
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
def is_fallback_mode(self) -> bool:
|
|
239
|
+
"""Check if currently in fallback mode."""
|
|
240
|
+
return self._fallback_mode
|
|
241
|
+
|
|
242
|
+
def get_fallback_settings(self) -> Optional[Dict[str, Any]]:
|
|
243
|
+
"""Get current fallback settings."""
|
|
244
|
+
return self._fallback_settings.copy() if self._fallback_settings else None
|
|
245
|
+
|
|
246
|
+
def save_fallback_settings(self, settings: Dict[str, Any]) -> bool:
|
|
247
|
+
"""
|
|
248
|
+
Save settings in fallback mode.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
settings: Settings to save
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
True if saved successfully
|
|
255
|
+
"""
|
|
256
|
+
try:
|
|
257
|
+
if not self._fallback_mode:
|
|
258
|
+
return False
|
|
259
|
+
|
|
260
|
+
with self._lock:
|
|
261
|
+
# Update in-memory fallback settings
|
|
262
|
+
self._fallback_settings = settings.copy()
|
|
263
|
+
|
|
264
|
+
# Save to JSON file
|
|
265
|
+
return self._save_json_fallback(settings)
|
|
266
|
+
|
|
267
|
+
except Exception as e:
|
|
268
|
+
self.logger.error(f"Failed to save fallback settings: {e}")
|
|
269
|
+
return False
|
|
270
|
+
|
|
271
|
+
def attempt_recovery(self, category: ErrorCategory) -> bool:
|
|
272
|
+
"""
|
|
273
|
+
Attempt recovery for a specific error category.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
category: Error category to recover from
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
True if recovery successful
|
|
280
|
+
"""
|
|
281
|
+
if self._recovery_in_progress:
|
|
282
|
+
self.logger.warning("Recovery already in progress")
|
|
283
|
+
return False
|
|
284
|
+
|
|
285
|
+
try:
|
|
286
|
+
self._recovery_in_progress = True
|
|
287
|
+
|
|
288
|
+
recovery_proc = self._recovery_procedures.get(category)
|
|
289
|
+
if recovery_proc:
|
|
290
|
+
success = recovery_proc()
|
|
291
|
+
if success:
|
|
292
|
+
self.logger.info(f"Recovery successful for {category.value}")
|
|
293
|
+
else:
|
|
294
|
+
self.logger.error(f"Recovery failed for {category.value}")
|
|
295
|
+
return success
|
|
296
|
+
else:
|
|
297
|
+
self.logger.warning(f"No recovery procedure for {category.value}")
|
|
298
|
+
return False
|
|
299
|
+
|
|
300
|
+
except Exception as e:
|
|
301
|
+
self.logger.error(f"Recovery attempt failed: {e}")
|
|
302
|
+
return False
|
|
303
|
+
finally:
|
|
304
|
+
self._recovery_in_progress = False
|
|
305
|
+
|
|
306
|
+
def get_error_statistics(self) -> Dict[str, Any]:
|
|
307
|
+
"""
|
|
308
|
+
Get error statistics and health information.
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
Dictionary with error statistics
|
|
312
|
+
"""
|
|
313
|
+
with self._lock:
|
|
314
|
+
total_errors = len(self._error_history)
|
|
315
|
+
recent_errors = [e for e in self._error_history
|
|
316
|
+
if e.timestamp > datetime.now() - timedelta(hours=24)]
|
|
317
|
+
|
|
318
|
+
critical_errors = [e for e in self._error_history
|
|
319
|
+
if e.severity == ErrorSeverity.CRITICAL]
|
|
320
|
+
|
|
321
|
+
stats = {
|
|
322
|
+
'total_errors': total_errors,
|
|
323
|
+
'recent_errors_24h': len(recent_errors),
|
|
324
|
+
'critical_errors': len(critical_errors),
|
|
325
|
+
'fallback_mode': self._fallback_mode,
|
|
326
|
+
'recovery_in_progress': self._recovery_in_progress,
|
|
327
|
+
'error_counts_by_category': dict(self._error_counts),
|
|
328
|
+
'last_error_times': {
|
|
329
|
+
cat.value: time.isoformat()
|
|
330
|
+
for cat, time in self._last_error_time.items()
|
|
331
|
+
},
|
|
332
|
+
'most_common_errors': self._get_most_common_errors(10)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return stats
|
|
336
|
+
|
|
337
|
+
def clear_error_history(self) -> None:
|
|
338
|
+
"""Clear error history."""
|
|
339
|
+
with self._lock:
|
|
340
|
+
self._error_history.clear()
|
|
341
|
+
self._error_counts.clear()
|
|
342
|
+
self._last_error_time.clear()
|
|
343
|
+
self.logger.info("Error history cleared")
|
|
344
|
+
|
|
345
|
+
def validate_database_integrity(self, connection_manager) -> List[str]:
|
|
346
|
+
"""
|
|
347
|
+
Validate database integrity and detect corruption.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
connection_manager: Database connection manager
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
List of integrity issues found
|
|
354
|
+
"""
|
|
355
|
+
issues = []
|
|
356
|
+
|
|
357
|
+
try:
|
|
358
|
+
conn = connection_manager.get_connection()
|
|
359
|
+
|
|
360
|
+
# Check database integrity
|
|
361
|
+
cursor = conn.execute("PRAGMA integrity_check")
|
|
362
|
+
integrity_result = cursor.fetchone()[0]
|
|
363
|
+
|
|
364
|
+
if integrity_result != "ok":
|
|
365
|
+
issues.append(f"Database integrity check failed: {integrity_result}")
|
|
366
|
+
|
|
367
|
+
# Check foreign key constraints
|
|
368
|
+
cursor = conn.execute("PRAGMA foreign_key_check")
|
|
369
|
+
fk_violations = cursor.fetchall()
|
|
370
|
+
|
|
371
|
+
if fk_violations:
|
|
372
|
+
issues.append(f"Foreign key violations found: {len(fk_violations)}")
|
|
373
|
+
|
|
374
|
+
# Check table existence
|
|
375
|
+
required_tables = [
|
|
376
|
+
'core_settings', 'tool_settings', 'tab_content',
|
|
377
|
+
'performance_settings', 'font_settings', 'dialog_settings',
|
|
378
|
+
'settings_metadata'
|
|
379
|
+
]
|
|
380
|
+
|
|
381
|
+
cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
|
382
|
+
existing_tables = {row[0] for row in cursor.fetchall()}
|
|
383
|
+
|
|
384
|
+
missing_tables = set(required_tables) - existing_tables
|
|
385
|
+
if missing_tables:
|
|
386
|
+
issues.append(f"Missing tables: {', '.join(missing_tables)}")
|
|
387
|
+
|
|
388
|
+
# Check for empty critical tables
|
|
389
|
+
for table in ['core_settings']:
|
|
390
|
+
cursor = conn.execute(f"SELECT COUNT(*) FROM {table}")
|
|
391
|
+
count = cursor.fetchone()[0]
|
|
392
|
+
if count == 0:
|
|
393
|
+
issues.append(f"Critical table {table} is empty")
|
|
394
|
+
|
|
395
|
+
except Exception as e:
|
|
396
|
+
issues.append(f"Database validation failed: {e}")
|
|
397
|
+
|
|
398
|
+
return issues
|
|
399
|
+
|
|
400
|
+
# Private methods for error handling
|
|
401
|
+
|
|
402
|
+
def _record_error(self, error_info: ErrorInfo) -> None:
|
|
403
|
+
"""Record error in history."""
|
|
404
|
+
with self._lock:
|
|
405
|
+
self._error_history.append(error_info)
|
|
406
|
+
|
|
407
|
+
# Update counts
|
|
408
|
+
category = error_info.category
|
|
409
|
+
self._error_counts[category] = self._error_counts.get(category, 0) + 1
|
|
410
|
+
self._last_error_time[category] = error_info.timestamp
|
|
411
|
+
|
|
412
|
+
# Trim history if too large
|
|
413
|
+
if len(self._error_history) > self.max_error_history:
|
|
414
|
+
self._error_history = self._error_history[-self.max_error_history:]
|
|
415
|
+
|
|
416
|
+
def _log_error(self, error_info: ErrorInfo) -> None:
|
|
417
|
+
"""Log error with appropriate level."""
|
|
418
|
+
log_message = f"[{error_info.category.value}] {error_info.message}"
|
|
419
|
+
|
|
420
|
+
if error_info.exception:
|
|
421
|
+
log_message += f" - Exception: {error_info.exception}"
|
|
422
|
+
|
|
423
|
+
if error_info.context:
|
|
424
|
+
log_message += f" - Context: {error_info.context}"
|
|
425
|
+
|
|
426
|
+
if error_info.severity == ErrorSeverity.CRITICAL:
|
|
427
|
+
self.logger.critical(log_message)
|
|
428
|
+
elif error_info.severity == ErrorSeverity.HIGH:
|
|
429
|
+
self.logger.error(log_message)
|
|
430
|
+
elif error_info.severity == ErrorSeverity.MEDIUM:
|
|
431
|
+
self.logger.warning(log_message)
|
|
432
|
+
else:
|
|
433
|
+
self.logger.info(log_message)
|
|
434
|
+
|
|
435
|
+
def _determine_severity(self, category: ErrorCategory,
|
|
436
|
+
exception: Optional[Exception]) -> ErrorSeverity:
|
|
437
|
+
"""Determine error severity based on category and exception."""
|
|
438
|
+
# Critical errors that prevent core functionality
|
|
439
|
+
if category in [ErrorCategory.DATABASE_CORRUPTION, ErrorCategory.MIGRATION]:
|
|
440
|
+
return ErrorSeverity.CRITICAL
|
|
441
|
+
|
|
442
|
+
# High severity errors that significantly impact functionality
|
|
443
|
+
if category in [ErrorCategory.DATABASE_CONNECTION, ErrorCategory.PERMISSION]:
|
|
444
|
+
return ErrorSeverity.HIGH
|
|
445
|
+
|
|
446
|
+
# Medium severity errors that cause inconvenience
|
|
447
|
+
if category in [ErrorCategory.DISK_SPACE, ErrorCategory.BACKUP]:
|
|
448
|
+
return ErrorSeverity.MEDIUM
|
|
449
|
+
|
|
450
|
+
# Check exception type for additional context
|
|
451
|
+
if isinstance(exception, (sqlite3.DatabaseError, sqlite3.CorruptError)):
|
|
452
|
+
return ErrorSeverity.CRITICAL
|
|
453
|
+
elif isinstance(exception, (PermissionError, OSError)):
|
|
454
|
+
return ErrorSeverity.HIGH
|
|
455
|
+
|
|
456
|
+
return ErrorSeverity.LOW
|
|
457
|
+
|
|
458
|
+
def _send_error_notification(self, error_info: ErrorInfo) -> None:
|
|
459
|
+
"""Send error notification (placeholder for future implementation)."""
|
|
460
|
+
# This could be extended to send notifications via:
|
|
461
|
+
# - System notifications
|
|
462
|
+
# - Email alerts
|
|
463
|
+
# - Logging to external systems
|
|
464
|
+
# - UI notifications
|
|
465
|
+
pass
|
|
466
|
+
|
|
467
|
+
# Error handlers for specific categories
|
|
468
|
+
|
|
469
|
+
def _handle_database_connection_error(self, error_info: ErrorInfo) -> bool:
|
|
470
|
+
"""Handle database connection errors."""
|
|
471
|
+
self.logger.warning("Database connection error - attempting fallback")
|
|
472
|
+
|
|
473
|
+
# Enable fallback mode
|
|
474
|
+
if self._fallback_enabled:
|
|
475
|
+
return self.enable_fallback_mode()
|
|
476
|
+
|
|
477
|
+
return False
|
|
478
|
+
|
|
479
|
+
def _handle_database_corruption_error(self, error_info: ErrorInfo) -> bool:
|
|
480
|
+
"""Handle database corruption errors."""
|
|
481
|
+
self.logger.error("Database corruption detected - attempting recovery")
|
|
482
|
+
|
|
483
|
+
# Try to recover from backup
|
|
484
|
+
if self._auto_recovery_enabled:
|
|
485
|
+
return self.attempt_recovery(ErrorCategory.DATABASE_CORRUPTION)
|
|
486
|
+
|
|
487
|
+
# Fall back to JSON if recovery not enabled
|
|
488
|
+
if self._fallback_enabled:
|
|
489
|
+
return self.enable_fallback_mode()
|
|
490
|
+
|
|
491
|
+
return False
|
|
492
|
+
|
|
493
|
+
def _handle_disk_space_error(self, error_info: ErrorInfo) -> bool:
|
|
494
|
+
"""Handle disk space errors."""
|
|
495
|
+
self.logger.warning("Disk space error - continuing with in-memory operations")
|
|
496
|
+
|
|
497
|
+
# Continue with in-memory operations, disable backups temporarily
|
|
498
|
+
return True
|
|
499
|
+
|
|
500
|
+
def _handle_permission_error(self, error_info: ErrorInfo) -> bool:
|
|
501
|
+
"""Handle permission errors."""
|
|
502
|
+
self.logger.error("Permission error - attempting fallback")
|
|
503
|
+
|
|
504
|
+
# Try fallback mode
|
|
505
|
+
if self._fallback_enabled:
|
|
506
|
+
return self.enable_fallback_mode()
|
|
507
|
+
|
|
508
|
+
return False
|
|
509
|
+
|
|
510
|
+
def _handle_data_validation_error(self, error_info: ErrorInfo) -> bool:
|
|
511
|
+
"""Handle data validation errors."""
|
|
512
|
+
self.logger.warning("Data validation error - using default values")
|
|
513
|
+
|
|
514
|
+
# Continue with default values for invalid data
|
|
515
|
+
return True
|
|
516
|
+
|
|
517
|
+
def _handle_migration_error(self, error_info: ErrorInfo) -> bool:
|
|
518
|
+
"""Handle migration errors."""
|
|
519
|
+
self.logger.error("Migration error - attempting rollback")
|
|
520
|
+
|
|
521
|
+
# Try to rollback migration
|
|
522
|
+
if self._auto_recovery_enabled:
|
|
523
|
+
return self.attempt_recovery(ErrorCategory.MIGRATION)
|
|
524
|
+
|
|
525
|
+
return False
|
|
526
|
+
|
|
527
|
+
def _handle_backup_error(self, error_info: ErrorInfo) -> bool:
|
|
528
|
+
"""Handle backup errors."""
|
|
529
|
+
self.logger.warning("Backup error - continuing without backup")
|
|
530
|
+
|
|
531
|
+
# Continue operations without backup
|
|
532
|
+
return True
|
|
533
|
+
|
|
534
|
+
def _handle_recovery_error(self, error_info: ErrorInfo) -> bool:
|
|
535
|
+
"""Handle recovery errors."""
|
|
536
|
+
self.logger.error("Recovery error - enabling fallback mode")
|
|
537
|
+
|
|
538
|
+
# Enable fallback as last resort
|
|
539
|
+
if self._fallback_enabled:
|
|
540
|
+
return self.enable_fallback_mode()
|
|
541
|
+
|
|
542
|
+
return False
|
|
543
|
+
|
|
544
|
+
def _handle_generic_error(self, error_info: ErrorInfo) -> bool:
|
|
545
|
+
"""Handle generic/unknown errors."""
|
|
546
|
+
self.logger.warning(f"Generic error: {error_info.message}")
|
|
547
|
+
|
|
548
|
+
# Try fallback for unknown errors
|
|
549
|
+
if error_info.severity in [ErrorSeverity.HIGH, ErrorSeverity.CRITICAL]:
|
|
550
|
+
if self._fallback_enabled:
|
|
551
|
+
return self.enable_fallback_mode()
|
|
552
|
+
|
|
553
|
+
return True
|
|
554
|
+
|
|
555
|
+
# Recovery procedures
|
|
556
|
+
|
|
557
|
+
def _recover_database_connection(self) -> bool:
|
|
558
|
+
"""Recover from database connection issues."""
|
|
559
|
+
try:
|
|
560
|
+
# Wait a moment and retry connection
|
|
561
|
+
time.sleep(1)
|
|
562
|
+
|
|
563
|
+
# This would be implemented by the connection manager
|
|
564
|
+
# For now, just return success to indicate recovery attempt
|
|
565
|
+
self.logger.info("Database connection recovery attempted")
|
|
566
|
+
return True
|
|
567
|
+
|
|
568
|
+
except Exception as e:
|
|
569
|
+
self.logger.error(f"Database connection recovery failed: {e}")
|
|
570
|
+
return False
|
|
571
|
+
|
|
572
|
+
def _recover_database_corruption(self) -> bool:
|
|
573
|
+
"""Recover from database corruption."""
|
|
574
|
+
try:
|
|
575
|
+
# Try to restore from most recent backup
|
|
576
|
+
backup_files = list(self.backup_dir.glob("settings_backup_*.db"))
|
|
577
|
+
if backup_files:
|
|
578
|
+
# Sort by modification time, get most recent
|
|
579
|
+
latest_backup = max(backup_files, key=lambda p: p.stat().st_mtime)
|
|
580
|
+
|
|
581
|
+
self.logger.info(f"Attempting to restore from backup: {latest_backup}")
|
|
582
|
+
# This would be implemented by the connection manager
|
|
583
|
+
return True
|
|
584
|
+
else:
|
|
585
|
+
self.logger.error("No backup files found for recovery")
|
|
586
|
+
return False
|
|
587
|
+
|
|
588
|
+
except Exception as e:
|
|
589
|
+
self.logger.error(f"Database corruption recovery failed: {e}")
|
|
590
|
+
return False
|
|
591
|
+
|
|
592
|
+
def _recover_disk_space(self) -> bool:
|
|
593
|
+
"""Recover from disk space issues."""
|
|
594
|
+
try:
|
|
595
|
+
# Clean up old backup files
|
|
596
|
+
backup_files = list(self.backup_dir.glob("settings_backup_*.db"))
|
|
597
|
+
if len(backup_files) > 5: # Keep only 5 most recent
|
|
598
|
+
old_backups = sorted(backup_files, key=lambda p: p.stat().st_mtime)[:-5]
|
|
599
|
+
for backup in old_backups:
|
|
600
|
+
backup.unlink()
|
|
601
|
+
self.logger.info(f"Cleaned up old backup: {backup}")
|
|
602
|
+
|
|
603
|
+
return True
|
|
604
|
+
|
|
605
|
+
except Exception as e:
|
|
606
|
+
self.logger.error(f"Disk space recovery failed: {e}")
|
|
607
|
+
return False
|
|
608
|
+
|
|
609
|
+
def _recover_permission(self) -> bool:
|
|
610
|
+
"""Recover from permission issues."""
|
|
611
|
+
try:
|
|
612
|
+
# Try to change file permissions if possible
|
|
613
|
+
# This is a placeholder - actual implementation would depend on OS
|
|
614
|
+
self.logger.info("Permission recovery attempted")
|
|
615
|
+
return False # Usually requires manual intervention
|
|
616
|
+
|
|
617
|
+
except Exception as e:
|
|
618
|
+
self.logger.error(f"Permission recovery failed: {e}")
|
|
619
|
+
return False
|
|
620
|
+
|
|
621
|
+
def _recover_data_validation(self) -> bool:
|
|
622
|
+
"""Recover from data validation issues."""
|
|
623
|
+
try:
|
|
624
|
+
# Reset to default values for invalid data
|
|
625
|
+
self.logger.info("Data validation recovery - using defaults")
|
|
626
|
+
return True
|
|
627
|
+
|
|
628
|
+
except Exception as e:
|
|
629
|
+
self.logger.error(f"Data validation recovery failed: {e}")
|
|
630
|
+
return False
|
|
631
|
+
|
|
632
|
+
def _recover_migration(self) -> bool:
|
|
633
|
+
"""Recover from migration issues."""
|
|
634
|
+
try:
|
|
635
|
+
# Try to rollback to original JSON file
|
|
636
|
+
backup_files = list(Path(".").glob("settings.json.backup_*"))
|
|
637
|
+
if backup_files:
|
|
638
|
+
latest_backup = max(backup_files, key=lambda p: p.stat().st_mtime)
|
|
639
|
+
shutil.copy2(latest_backup, self.json_fallback_path)
|
|
640
|
+
self.logger.info(f"Migration rollback completed: {latest_backup}")
|
|
641
|
+
return True
|
|
642
|
+
else:
|
|
643
|
+
self.logger.error("No JSON backup found for migration rollback")
|
|
644
|
+
return False
|
|
645
|
+
|
|
646
|
+
except Exception as e:
|
|
647
|
+
self.logger.error(f"Migration recovery failed: {e}")
|
|
648
|
+
return False
|
|
649
|
+
|
|
650
|
+
def _recover_backup(self) -> bool:
|
|
651
|
+
"""Recover from backup issues."""
|
|
652
|
+
try:
|
|
653
|
+
# Try alternative backup location or method
|
|
654
|
+
self.logger.info("Backup recovery attempted")
|
|
655
|
+
return True
|
|
656
|
+
|
|
657
|
+
except Exception as e:
|
|
658
|
+
self.logger.error(f"Backup recovery failed: {e}")
|
|
659
|
+
return False
|
|
660
|
+
|
|
661
|
+
# Fallback JSON operations
|
|
662
|
+
|
|
663
|
+
def _load_json_fallback(self) -> Optional[Dict[str, Any]]:
|
|
664
|
+
"""Load settings from JSON fallback file."""
|
|
665
|
+
try:
|
|
666
|
+
if os.path.exists(self.json_fallback_path):
|
|
667
|
+
with open(self.json_fallback_path, 'r', encoding='utf-8') as f:
|
|
668
|
+
return json.load(f)
|
|
669
|
+
else:
|
|
670
|
+
self.logger.warning(f"JSON fallback file not found: {self.json_fallback_path}")
|
|
671
|
+
return None
|
|
672
|
+
|
|
673
|
+
except Exception as e:
|
|
674
|
+
self.logger.error(f"Failed to load JSON fallback: {e}")
|
|
675
|
+
return None
|
|
676
|
+
|
|
677
|
+
def _save_json_fallback(self, settings: Dict[str, Any]) -> bool:
|
|
678
|
+
"""Save settings to JSON fallback file."""
|
|
679
|
+
try:
|
|
680
|
+
with open(self.json_fallback_path, 'w', encoding='utf-8') as f:
|
|
681
|
+
json.dump(settings, f, indent=2, ensure_ascii=False)
|
|
682
|
+
return True
|
|
683
|
+
|
|
684
|
+
except Exception as e:
|
|
685
|
+
self.logger.error(f"Failed to save JSON fallback: {e}")
|
|
686
|
+
return False
|
|
687
|
+
|
|
688
|
+
def _get_minimal_default_settings(self) -> Dict[str, Any]:
|
|
689
|
+
"""Get minimal default settings for emergency fallback."""
|
|
690
|
+
return {
|
|
691
|
+
"export_path": str(Path.home() / "Downloads"),
|
|
692
|
+
"debug_level": "INFO",
|
|
693
|
+
"selected_tool": "Case Tool",
|
|
694
|
+
"active_input_tab": 0,
|
|
695
|
+
"active_output_tab": 0,
|
|
696
|
+
"input_tabs": [""] * 7,
|
|
697
|
+
"output_tabs": [""] * 7,
|
|
698
|
+
"tool_settings": {},
|
|
699
|
+
"performance_settings": {"mode": "automatic"},
|
|
700
|
+
"font_settings": {"text_font": {"family": "Consolas", "size": 11}},
|
|
701
|
+
"dialog_settings": {"error": {"enabled": True, "locked": True}}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
def _get_most_common_errors(self, limit: int) -> List[Dict[str, Any]]:
|
|
705
|
+
"""Get most common error categories."""
|
|
706
|
+
error_counts = {}
|
|
707
|
+
|
|
708
|
+
for error in self._error_history:
|
|
709
|
+
key = f"{error.category.value}: {error.message[:50]}"
|
|
710
|
+
error_counts[key] = error_counts.get(key, 0) + 1
|
|
711
|
+
|
|
712
|
+
# Sort by count and return top N
|
|
713
|
+
sorted_errors = sorted(error_counts.items(), key=lambda x: x[1], reverse=True)
|
|
714
|
+
|
|
715
|
+
return [
|
|
716
|
+
{"error": error, "count": count}
|
|
717
|
+
for error, count in sorted_errors[:limit]
|
|
718
|
+
]
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
# Global error handler instance
|
|
722
|
+
_global_error_handler: Optional[SettingsErrorHandler] = None
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def get_error_handler() -> SettingsErrorHandler:
|
|
726
|
+
"""Get the global error handler instance."""
|
|
727
|
+
global _global_error_handler
|
|
728
|
+
if _global_error_handler is None:
|
|
729
|
+
_global_error_handler = SettingsErrorHandler()
|
|
730
|
+
return _global_error_handler
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def initialize_error_handler(json_fallback_path: str = "settings.json",
|
|
734
|
+
backup_dir: str = "backups") -> SettingsErrorHandler:
|
|
735
|
+
"""
|
|
736
|
+
Initialize the global error handler.
|
|
737
|
+
|
|
738
|
+
Args:
|
|
739
|
+
json_fallback_path: Path to JSON fallback file
|
|
740
|
+
backup_dir: Directory for backup files
|
|
741
|
+
|
|
742
|
+
Returns:
|
|
743
|
+
Initialized error handler instance
|
|
744
|
+
"""
|
|
745
|
+
global _global_error_handler
|
|
746
|
+
_global_error_handler = SettingsErrorHandler(json_fallback_path, backup_dir)
|
|
747
747
|
return _global_error_handler
|