Files
cursor-free-vip-main/config.py
XnsYT fe445cc298 feat: Add enhanced configuration, error handling and utility systems v1.11.04
## 🚀 Enhanced Features Implementation

This PR introduces significant improvements to the Cursor Free VIP project with enhanced configuration management, error handling, and utility systems.

###  New Features

#### 🔧 Enhanced Configuration Management (`enhanced_config.py`)
- **Multi-format support**: INI, JSON, YAML configuration formats
- **Automatic validation**: Built-in configuration validation with detailed error reporting
- **Backup system**: Automatic configuration backup and restore functionality
- **Platform-specific paths**: Automatic detection and management of paths for Windows, macOS, and Linux
- **Type safety**: Improved type checking and error handling

#### 🛡️ Enhanced Error Handling (`enhanced_error_handler.py`)
- **Automatic categorization**: Intelligent error classification (Network, File System, Process, etc.)
- **Severity-based logging**: Critical, High, Medium, Low severity levels
- **Recovery strategies**: Automatic retry logic with exponential backoff
- **Error history**: Comprehensive error tracking and resolution management
- **Custom callbacks**: Registerable error handlers for specific categories

#### 🛠️ Enhanced Utility System (`enhanced_utils.py`)
- **Advanced path management**: Cross-platform path detection and validation
- **Multi-browser support**: Automatic detection of Chrome, Firefox, Edge, Brave, Opera
- **Process monitoring**: Real-time process tracking and management
- **System information**: Detailed system resource monitoring
- **Network connectivity**: Automated network testing and validation

### 🔧 Technical Improvements

- **Type safety**: Fixed all linter errors and type annotation issues
- **Code robustness**: Improved error handling and exception management
- **Maintainability**: Better code organization and documentation
- **Cross-platform compatibility**: Enhanced support for Windows, macOS, and Linux

### �� Files Changed

- `enhanced_config.py` - New enhanced configuration management system
- `enhanced_utils.py` - New enhanced utility and system management
- `enhanced_error_handler.py` - New enhanced error handling system
- `CHANGELOG.md` - Updated with v1.11.04 release notes

### �� Testing

- All new systems have been tested on Windows, macOS, and Linux
- Type checking passes with no linter errors
- Backward compatibility maintained with existing functionality

### 📝 Documentation

- Comprehensive inline documentation for all new features
- Updated CHANGELOG with detailed feature descriptions
- Follows existing code style and conventions

---

**Note**: These enhancements provide a solid foundation for future development while maintaining full backward compatibility with existing functionality.
2025-07-10 18:46:02 +02:00

601 lines
27 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import os
import sys
import configparser
import logging
import tempfile
import datetime
import platform
from pathlib import Path
from typing import Optional, Dict, Any, Union, List, Tuple
from colorama import Fore, Style, init
# Initialize colorama
init(autoreset=True)
# Configure logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
logger = logging.getLogger(__name__)
# Import utils after logging setup to avoid circular imports
from utils import get_user_documents_path, get_linux_cursor_path, get_default_driver_path, get_default_browser_path
# Define emoji constants
EMOJI = {
"INFO": "",
"WARNING": "⚠️",
"ERROR": "",
"SUCCESS": "",
"ADMIN": "🔒",
"ARROW": "➡️",
"USER": "👤",
"KEY": "🔑",
"SETTINGS": "⚙️",
"CONFIG": "📝"
}
# Global config cache
_config_cache = None
class ConfigManager:
"""Class to manage configuration operations"""
def __init__(self, translator: Any = None):
"""Initialize ConfigManager
Args:
translator: Optional translator for internationalization
"""
self.translator = translator
self.config = configparser.ConfigParser()
self.config_dir = None
self.config_file = None
def _get_message(self, key: str, fallback: str, **kwargs) -> str:
"""Get translated message or fallback
Args:
key: Translation key
fallback: Fallback message if translation not available
**kwargs: Format parameters for the message
Returns:
str: Translated or fallback message
"""
if self.translator:
return self.translator.get(key, fallback=fallback, **kwargs)
return fallback.format(**kwargs) if kwargs else fallback
def setup_config_directory(self) -> Tuple[str, str]:
"""Setup configuration directory
Returns:
Tuple[str, str]: Configuration directory and file paths
"""
# Get documents path
docs_path = get_user_documents_path()
if not docs_path or not os.path.exists(docs_path):
# If documents path not found, use current directory
msg = self._get_message('config.documents_path_not_found',
'Documents path not found, using current directory')
logger.warning(msg)
print(f"{Fore.YELLOW}{EMOJI['WARNING']} {msg}{Style.RESET_ALL}")
docs_path = os.path.abspath('.')
# Normalize path
config_dir = os.path.normpath(os.path.join(docs_path, ".cursor-free-vip"))
config_file = os.path.normpath(os.path.join(config_dir, "config.ini"))
# Create config directory
dir_exists = os.path.exists(config_dir)
try:
os.makedirs(config_dir, exist_ok=True)
if not dir_exists:
msg = self._get_message('config.config_dir_created',
'Config directory created: {path}', path=config_dir)
logger.info(f"Config directory created: {config_dir}")
print(f"{Fore.CYAN}{EMOJI['INFO']} {msg}{Style.RESET_ALL}")
except Exception as e:
# If cannot create directory, use temporary directory
logger.warning(f"Failed to create config directory: {e}")
temp_dir = os.path.normpath(os.path.join(tempfile.gettempdir(), ".cursor-free-vip"))
temp_exists = os.path.exists(temp_dir)
config_dir = temp_dir
config_file = os.path.normpath(os.path.join(config_dir, "config.ini"))
try:
os.makedirs(config_dir, exist_ok=True)
if not temp_exists:
msg = self._get_message('config.using_temp_dir',
'Using temporary directory due to error: {path} (Error: {error})',
path=config_dir, error=str(e))
print(f"{Fore.YELLOW}{EMOJI['WARNING']} {msg}{Style.RESET_ALL}")
except Exception as inner_e:
logger.error(f"Failed to create temporary config directory: {inner_e}")
# Last resort: use current directory
config_dir = os.path.abspath('.')
config_file = os.path.join(config_dir, "config.ini")
self.config_dir = config_dir
self.config_file = config_file
return config_dir, config_file
def get_default_config(self) -> Dict[str, Dict[str, Any]]:
"""Get default configuration
Returns:
Dict[str, Dict[str, Any]]: Default configuration dictionary
"""
config_dir = self.config_dir or os.path.join(get_user_documents_path(), ".cursor-free-vip")
# Default configuration
default_config = {
'Browser': {
'default_browser': 'chrome',
'chrome_path': get_default_browser_path('chrome'),
'chrome_driver_path': get_default_driver_path('chrome'),
'edge_path': get_default_browser_path('edge'),
'edge_driver_path': get_default_driver_path('edge'),
'firefox_path': get_default_browser_path('firefox'),
'firefox_driver_path': get_default_driver_path('firefox'),
'brave_path': get_default_browser_path('brave'),
'brave_driver_path': get_default_driver_path('brave'),
'opera_path': get_default_browser_path('opera'),
'opera_driver_path': get_default_driver_path('opera'),
'operagx_path': get_default_browser_path('operagx'),
'operagx_driver_path': get_default_driver_path('chrome') # Opera GX uses Chrome driver
},
'Turnstile': {
'handle_turnstile_time': '2',
'handle_turnstile_random_time': '1-3'
},
'Timing': {
'min_random_time': '0.1',
'max_random_time': '0.8',
'page_load_wait': '0.1-0.8',
'input_wait': '0.3-0.8',
'submit_wait': '0.5-1.5',
'verification_code_input': '0.1-0.3',
'verification_success_wait': '2-3',
'verification_retry_wait': '2-3',
'email_check_initial_wait': '4-6',
'email_refresh_wait': '2-4',
'settings_page_load_wait': '1-2',
'failed_retry_time': '0.5-1',
'retry_interval': '8-12',
'max_timeout': '160'
},
'Utils': {
'enabled_update_check': 'True',
'enabled_force_update': 'False',
'enabled_account_info': 'True'
},
'OAuth': {
'show_selection_alert': 'False',
'timeout': '120',
'max_attempts': '3'
},
'Token': {
'refresh_server': 'https://token.cursorpro.com.cn',
'enable_refresh': 'True'
},
'Language': {
'current_language': '', # Set by local system detection if empty
'fallback_language': 'en',
'auto_update_languages': 'True',
'language_cache_dir': os.path.join(config_dir, "language_cache")
}
}
# Add system-specific path configuration
self._add_system_paths(default_config)
return default_config
def _add_system_paths(self, default_config: Dict[str, Dict[str, Any]]) -> None:
"""Add system-specific paths to the default configuration
Args:
default_config: Default configuration dictionary to update
"""
if sys.platform == "win32":
self._add_windows_paths(default_config)
elif sys.platform == "darwin":
self._add_macos_paths(default_config)
elif sys.platform == "linux":
self._add_linux_paths(default_config)
def _add_windows_paths(self, default_config: Dict[str, Dict[str, Any]]) -> None:
"""Add Windows-specific paths to the default configuration
Args:
default_config: Default configuration dictionary to update
"""
appdata = os.getenv("APPDATA", "")
localappdata = os.getenv("LOCALAPPDATA", "")
if not appdata or not localappdata:
logger.warning("APPDATA or LOCALAPPDATA environment variables not found")
appdata = os.path.expanduser("~\\AppData\\Roaming")
localappdata = os.path.expanduser("~\\AppData\\Local")
default_config['WindowsPaths'] = {
'storage_path': os.path.join(appdata, "Cursor", "User", "globalStorage", "storage.json"),
'sqlite_path': os.path.join(appdata, "Cursor", "User", "globalStorage", "state.vscdb"),
'machine_id_path': os.path.join(appdata, "Cursor", "machineId"),
'cursor_path': os.path.join(localappdata, "Programs", "Cursor", "resources", "app"),
'updater_path': os.path.join(localappdata, "cursor-updater"),
'update_yml_path': os.path.join(localappdata, "Programs", "Cursor", "resources", "app-update.yml"),
'product_json_path': os.path.join(localappdata, "Programs", "Cursor", "resources", "app", "product.json")
}
# Create storage directory
try:
storage_dir = os.path.dirname(default_config['WindowsPaths']['storage_path'])
os.makedirs(storage_dir, exist_ok=True)
except Exception as e:
logger.warning(f"Failed to create storage directory: {e}")
def _add_macos_paths(self, default_config: Dict[str, Dict[str, Any]]) -> None:
"""Add macOS-specific paths to the default configuration
Args:
default_config: Default configuration dictionary to update
"""
default_config['MacPaths'] = {
'storage_path': os.path.abspath(os.path.expanduser("~/Library/Application Support/Cursor/User/globalStorage/storage.json")),
'sqlite_path': os.path.abspath(os.path.expanduser("~/Library/Application Support/Cursor/User/globalStorage/state.vscdb")),
'machine_id_path': os.path.expanduser("~/Library/Application Support/Cursor/machineId"),
'cursor_path': "/Applications/Cursor.app/Contents/Resources/app",
'updater_path': os.path.expanduser("~/Library/Application Support/cursor-updater"),
'update_yml_path': "/Applications/Cursor.app/Contents/Resources/app-update.yml",
'product_json_path': "/Applications/Cursor.app/Contents/Resources/app/product.json"
}
# Create storage directory
try:
storage_dir = os.path.dirname(default_config['MacPaths']['storage_path'])
os.makedirs(storage_dir, exist_ok=True)
except Exception as e:
logger.warning(f"Failed to create storage directory: {e}")
def _add_linux_paths(self, default_config: Dict[str, Dict[str, Any]]) -> None:
"""Add Linux-specific paths to the default configuration
Args:
default_config: Default configuration dictionary to update
"""
# Get the actual user's home directory, handling both sudo and normal cases
sudo_user = os.environ.get('SUDO_USER')
current_user = sudo_user if sudo_user else (os.getenv('USER') or os.getenv('USERNAME'))
if not current_user:
current_user = os.path.expanduser('~').split('/')[-1]
# Handle sudo case
if sudo_user:
actual_home = f"/home/{sudo_user}"
root_home = "/root"
else:
actual_home = f"/home/{current_user}"
root_home = None
if not os.path.exists(actual_home):
actual_home = os.path.expanduser("~")
# Define base config directory
config_base = os.path.join(actual_home, ".config")
# Try both "Cursor" and "cursor" directory names in both user and root locations
cursor_dir = None
possible_paths = [
os.path.join(config_base, "Cursor"),
os.path.join(config_base, "cursor"),
os.path.join(root_home, ".config", "Cursor") if root_home else None,
os.path.join(root_home, ".config", "cursor") if root_home else None
]
for path in possible_paths:
if path and os.path.exists(path):
cursor_dir = path
break
if not cursor_dir:
msg = self._get_message('config.neither_cursor_nor_cursor_directory_found',
'Neither Cursor nor cursor directory found in {config_base}',
config_base=config_base)
logger.warning(f"Cursor directory not found in {config_base}")
print(f"{Fore.YELLOW}{EMOJI['WARNING']} {msg}{Style.RESET_ALL}")
if root_home:
msg = self._get_message('config.also_checked',
'Also checked {path}',
path=f'{root_home}/.config')
print(f"{Fore.YELLOW}{EMOJI['INFO']} {msg}{Style.RESET_ALL}")
msg = self._get_message('config.please_make_sure_cursor_is_installed_and_has_been_run_at_least_once',
'Please make sure Cursor is installed and has been run at least once')
print(f"{Fore.YELLOW}{EMOJI['INFO']} {msg}{Style.RESET_ALL}")
# Define Linux paths using the found cursor directory
storage_path = os.path.abspath(os.path.join(cursor_dir, "User/globalStorage/storage.json")) if cursor_dir else ""
storage_dir = os.path.dirname(storage_path) if storage_path else ""
# Set default Linux paths
default_config['LinuxPaths'] = {
'storage_path': storage_path,
'sqlite_path': os.path.abspath(os.path.join(cursor_dir, "User/globalStorage/state.vscdb")) if cursor_dir else "",
'machine_id_path': os.path.join(cursor_dir, "machineid") if cursor_dir else "",
'cursor_path': get_linux_cursor_path(),
'updater_path': os.path.join(config_base, "cursor-updater"),
'update_yml_path': os.path.join(cursor_dir, "resources/app-update.yml") if cursor_dir else "",
'product_json_path': os.path.join(cursor_dir, "resources/app/product.json") if cursor_dir else ""
}
# Verify paths and permissions
self._verify_linux_paths(storage_path, storage_dir)
def _verify_linux_paths(self, storage_path: str, storage_dir: str) -> None:
"""Verify Linux paths and permissions
Args:
storage_path: Path to the storage.json file
storage_dir: Directory containing the storage.json file
"""
try:
# Check storage directory
if storage_dir and not os.path.exists(storage_dir):
msg = self._get_message('config.storage_directory_not_found',
'Storage directory not found: {storage_dir}',
storage_dir=storage_dir)
logger.warning(f"Storage directory not found: {storage_dir}")
print(f"{Fore.YELLOW}{EMOJI['WARNING']} {msg}{Style.RESET_ALL}")
msg = self._get_message('config.please_make_sure_cursor_is_installed_and_has_been_run_at_least_once',
'Please make sure Cursor is installed and has been run at least once')
print(f"{Fore.YELLOW}{EMOJI['INFO']} {msg}{Style.RESET_ALL}")
# Check storage.json with more detailed verification
if storage_path and os.path.exists(storage_path):
# Get file stats
try:
stat = os.stat(storage_path)
msg = self._get_message('config.storage_file_found',
'Storage file found: {storage_path}',
storage_path=storage_path)
print(f"{Fore.GREEN}{EMOJI['INFO']} {msg}{Style.RESET_ALL}")
# Log file details
file_details = [
('config.file_size', 'File size: {size} bytes', {'size': stat.st_size}),
('config.file_permissions', 'File permissions: {permissions}', {'permissions': oct(stat.st_mode & 0o777)}),
('config.file_owner', 'File owner: {owner}', {'owner': stat.st_uid}),
('config.file_group', 'File group: {group}', {'group': stat.st_gid})
]
for key, fallback, kwargs in file_details:
msg = self._get_message(key, fallback, **kwargs)
print(f"{Fore.GREEN}{EMOJI['INFO']} {msg}{Style.RESET_ALL}")
except Exception as e:
logger.error(f"Error getting file stats: {e}")
msg = self._get_message('config.error_getting_file_stats',
'Error getting file stats: {error}',
error=str(e))
print(f"{Fore.RED}{EMOJI['ERROR']} {msg}{Style.RESET_ALL}")
# Check if file is readable and writable
if not os.access(storage_path, os.R_OK | os.W_OK):
msg = self._get_message('config.permission_denied',
'Permission denied: {storage_path}',
storage_path=storage_path)
logger.warning(f"Permission denied: {storage_path}")
print(f"{Fore.RED}{EMOJI['ERROR']} {msg}{Style.RESET_ALL}")
sudo_user = os.environ.get('SUDO_USER')
current_user = sudo_user if sudo_user else (os.getenv('USER') or os.getenv('USERNAME'))
if sudo_user:
cmd = f"chown {sudo_user}:{sudo_user} {storage_path}"
else:
cmd = f"chown {current_user}:{current_user} {storage_path}"
msg = self._get_message('config.try_running',
'Try running: {command}',
command=cmd)
print(f"{Fore.YELLOW}{EMOJI['INFO']} {msg}{Style.RESET_ALL}")
msg = self._get_message('config.and', 'And')
print(f"{Fore.YELLOW}{EMOJI['INFO']} {msg}: chmod 644 {storage_path}{Style.RESET_ALL}")
# Try to read the file to verify it's not corrupted
try:
with open(storage_path, 'r') as f:
content = f.read()
if not content.strip():
msg = self._get_message('config.storage_file_is_empty',
'Storage file is empty: {storage_path}',
storage_path=storage_path)
logger.warning(f"Storage file is empty: {storage_path}")
print(f"{Fore.YELLOW}{EMOJI['WARNING']} {msg}{Style.RESET_ALL}")
msg = self._get_message('config.the_file_might_be_corrupted_please_reinstall_cursor',
'The file might be corrupted, please reinstall Cursor')
print(f"{Fore.YELLOW}{EMOJI['INFO']} {msg}{Style.RESET_ALL}")
else:
msg = self._get_message('config.storage_file_is_valid_and_contains_data',
'Storage file is valid and contains data')
logger.info("Storage file is valid and contains data")
print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {msg}{Style.RESET_ALL}")
except Exception as e:
logger.error(f"Error reading storage file: {e}")
msg = self._get_message('config.error_reading_storage_file',
'Error reading storage file: {error}',
error=str(e))
print(f"{Fore.RED}{EMOJI['ERROR']} {msg}{Style.RESET_ALL}")
msg = self._get_message('config.the_file_might_be_corrupted_please_reinstall_cursor',
'The file might be corrupted. Please reinstall Cursor')
print(f"{Fore.YELLOW}{EMOJI['INFO']} {msg}{Style.RESET_ALL}")
elif storage_path:
msg = self._get_message('config.storage_file_not_found',
'Storage file not found: {storage_path}',
storage_path=storage_path)
logger.warning(f"Storage file not found: {storage_path}")
print(f"{Fore.YELLOW}{EMOJI['WARNING']} {msg}{Style.RESET_ALL}")
msg = self._get_message('config.please_make_sure_cursor_is_installed_and_has_been_run_at_least_once',
'Please make sure Cursor is installed and has been run at least once')
print(f"{Fore.YELLOW}{EMOJI['INFO']} {msg}{Style.RESET_ALL}")
except (OSError, IOError) as e:
logger.error(f"Error checking Linux paths: {e}")
msg = self._get_message('config.error_checking_linux_paths',
'Error checking Linux paths: {error}',
error=str(e))
print(f"{Fore.RED}{EMOJI['ERROR']} {msg}{Style.RESET_ALL}")
def setup(self) -> configparser.ConfigParser:
"""Setup configuration
Returns:
configparser.ConfigParser: Configured ConfigParser object
"""
# Setup config directory
self.setup_config_directory()
# Get default configuration
default_config = self.get_default_config()
# Read existing config if it exists
if os.path.exists(self.config_file):
try:
self.config.read(self.config_file)
logger.info(f"Read existing configuration from {self.config_file}")
except Exception as e:
logger.error(f"Error reading config file: {e}")
# Continue with default config
# Update config with default values for missing sections/options
for section, options in default_config.items():
if not self.config.has_section(section):
self.config.add_section(section)
for option, value in options.items():
if not self.config.has_option(section, option):
self.config.set(section, option, str(value))
# Save config
try:
with open(self.config_file, 'w', encoding='utf-8') as f:
self.config.write(f)
logger.info(f"Configuration saved to {self.config_file}")
except Exception as e:
logger.error(f"Error saving config file: {e}")
return self.config
def setup_config(translator: Any = None) -> configparser.ConfigParser:
"""Setup configuration file and return config object
Args:
translator: Optional translator for internationalization
Returns:
configparser.ConfigParser: Configured ConfigParser object
"""
try:
config_manager = ConfigManager(translator)
return config_manager.setup()
except Exception as e:
logger.error(f"Error setting up configuration: {e}")
# Return empty config as fallback
return configparser.ConfigParser()
def print_config(config: configparser.ConfigParser, translator: Any = None) -> None:
"""Print configuration
Args:
config: ConfigParser object
translator: Optional translator for internationalization
"""
if not config:
print(f"{Fore.RED}{EMOJI['ERROR']} Configuration not available{Style.RESET_ALL}")
return
print(f"\n{Fore.CYAN}{EMOJI['CONFIG']} Configuration:{Style.RESET_ALL}")
for section in config.sections():
print(f"\n{Fore.CYAN}[{section}]{Style.RESET_ALL}")
for option in config.options(section):
value = config.get(section, option)
# Mask sensitive information
if 'token' in option.lower() or 'password' in option.lower() or 'key' in option.lower():
value = '*' * 8
print(f" {option} = {value}")
def force_update_config(translator: Any = None) -> configparser.ConfigParser:
"""Force update configuration
Args:
translator: Optional translator for internationalization
Returns:
configparser.ConfigParser: Updated ConfigParser object
"""
global _config_cache
_config_cache = None
# Create backup of existing config
try:
config_dir = os.path.join(get_user_documents_path(), ".cursor-free-vip")
config_file = os.path.join(config_dir, "config.ini")
if os.path.exists(config_file):
timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
backup_file = f"{config_file}.{timestamp}.bak"
import shutil
shutil.copy2(config_file, backup_file)
msg = translator.get('config.backup_created', fallback='Backup created: {path}', path=backup_file) if translator else f"Backup created: {backup_file}"
logger.info(f"Config backup created: {backup_file}")
print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {msg}{Style.RESET_ALL}")
except Exception as e:
logger.error(f"Error creating config backup: {e}")
# Setup new config
return setup_config(translator)
def get_config(translator: Any = None) -> configparser.ConfigParser:
"""Get configuration
Args:
translator: Optional translator for internationalization
Returns:
configparser.ConfigParser: ConfigParser object
"""
global _config_cache
if _config_cache is not None:
return _config_cache
try:
config_dir = os.path.join(get_user_documents_path(), ".cursor-free-vip")
config_file = os.path.join(config_dir, "config.ini")
if os.path.exists(config_file):
config = configparser.ConfigParser()
config.read(config_file)
_config_cache = config
return config
else:
_config_cache = setup_config(translator)
return _config_cache
except Exception as e:
logger.error(f"Error getting configuration: {e}")
return configparser.ConfigParser()