mirror of
https://github.com/psipher/cursor-free-vip-main.git
synced 2026-02-10 01:21:58 +00:00
Merge pull request #1 from XnsYT/main
feat: Add enhanced configuration, error handling and utility systems …
This commit is contained in:
21
CHANGELOG.md
21
CHANGELOG.md
@@ -1,5 +1,26 @@
|
||||
# Change Log
|
||||
|
||||
## v1.11.04
|
||||
1. Add: Enhanced Configuration Management System | 新增增强配置管理系统
|
||||
- Multi-format support (INI, JSON, YAML) | 多格式支持 (INI, JSON, YAML)
|
||||
- Automatic configuration validation | 自动配置验证
|
||||
- Backup and restore functionality | 备份和恢复功能
|
||||
- Platform-specific path management | 平台特定路径管理
|
||||
2. Add: Enhanced Error Handling System | 新增增强错误处理系统
|
||||
- Automatic error categorization | 自动错误分类
|
||||
- Severity-based logging | 基于严重性的日志记录
|
||||
- Recovery strategies with retry logic | 带重试逻辑的恢复策略
|
||||
- Error history tracking | 错误历史跟踪
|
||||
3. Add: Enhanced Utility System | 新增增强工具系统
|
||||
- Advanced path management | 高级路径管理
|
||||
- Multi-browser support with detection | 多浏览器支持与检测
|
||||
- Process monitoring and management | 进程监控和管理
|
||||
- System information gathering | 系统信息收集
|
||||
- Network connectivity testing | 网络连接测试
|
||||
4. Improve: Code robustness and maintainability | 改进代码健壮性和可维护性
|
||||
5. Fix: Type safety issues and linter errors | 修复类型安全问题和linter错误
|
||||
6. Fix: Some Issues | 修复一些问题
|
||||
|
||||
## v1.11.03
|
||||
1. Update: TempMailPlus Cursor Email Detection Logic | 更新 TempMailPlus Cursor 邮件识别逻辑
|
||||
2. Fix: Windows User Directory Path | 修正 windows 环境下用户目录的获取方式
|
||||
|
||||
14
README.md
14
README.md
@@ -70,6 +70,20 @@ For optimal performance, run with privileges and always stay up to date.
|
||||
|
||||
* Multi-language support (English, 简体中文, 繁體中文, Vietnamese)<br>多語言支持(英文、简体中文、繁體中文、越南語)<br>
|
||||
|
||||
## 🚀 Recent Improvements | 最近改進
|
||||
|
||||
* **Enhanced Error Handling**: Robust error handling with detailed logging for better troubleshooting<br>增強的錯誤處理:具有詳細日誌記錄的強大錯誤處理,以便更好地進行故障排除<br>
|
||||
|
||||
* **Improved Configuration Management**: Centralized configuration with type validation and better defaults<br>改進的配置管理:集中配置,具有類型驗證和更好的默認值<br>
|
||||
|
||||
* **Code Refactoring**: Better code organization with proper typing and documentation<br>代碼重構:通過適當的類型和文檔實現更好的代碼組織<br>
|
||||
|
||||
* **Enhanced Process Management**: Better detection and management of Cursor processes across all platforms<br>增強的進程管理:在所有平台上更好地檢測和管理 Cursor 進程<br>
|
||||
|
||||
* **Token Management**: Improved token validation, refresh, and extraction logic<br>令牌管理:改進的令牌驗證、刷新和提取邏輯<br>
|
||||
|
||||
* **Cross-Platform Compatibility**: Better handling of platform-specific paths and behaviors<br>跨平台兼容性:更好地處理特定於平台的路徑和行為<br>
|
||||
|
||||
## 💻 System Support | 系統支持
|
||||
|
||||
| Operating System | Architecture | Supported |
|
||||
|
||||
@@ -3,14 +3,26 @@ import shutil
|
||||
import platform
|
||||
import tempfile
|
||||
import glob
|
||||
import logging
|
||||
from colorama import Fore, Style, init
|
||||
import configparser
|
||||
import sys
|
||||
from config import get_config
|
||||
from datetime import datetime
|
||||
from typing import Optional, Dict, List, Union, Tuple, Any
|
||||
from pathlib import Path
|
||||
|
||||
from config import get_config
|
||||
|
||||
# Initialize colorama
|
||||
init()
|
||||
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__)
|
||||
|
||||
# Define emoji constants
|
||||
EMOJI = {
|
||||
@@ -23,39 +35,45 @@ EMOJI = {
|
||||
"WARNING": "⚠️",
|
||||
}
|
||||
|
||||
def get_user_documents_path():
|
||||
"""Get user Documents folder path"""
|
||||
if sys.platform == "win32":
|
||||
try:
|
||||
import winreg
|
||||
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders") as key:
|
||||
documents_path, _ = winreg.QueryValueEx(key, "Personal")
|
||||
return documents_path
|
||||
except Exception as e:
|
||||
# fallback
|
||||
return os.path.join(os.path.expanduser("~"), "Documents")
|
||||
elif sys.platform == "darwin":
|
||||
return os.path.join(os.path.expanduser("~"), "Documents")
|
||||
else: # Linux
|
||||
# Get actual user's home directory
|
||||
sudo_user = os.environ.get('SUDO_USER')
|
||||
if sudo_user:
|
||||
return os.path.join("/home", sudo_user, "Documents")
|
||||
return os.path.join(os.path.expanduser("~"), "Documents")
|
||||
|
||||
def get_user_documents_path() -> str:
|
||||
"""Get user Documents folder path"""
|
||||
if sys.platform == "win32":
|
||||
try:
|
||||
import winreg
|
||||
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders") as key:
|
||||
documents_path, _ = winreg.QueryValueEx(key, "Personal")
|
||||
return documents_path
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to get Documents path from registry: {e}")
|
||||
return os.path.join(os.path.expanduser("~"), "Documents")
|
||||
elif sys.platform == "darwin":
|
||||
return os.path.join(os.path.expanduser("~"), "Documents")
|
||||
else: # Linux
|
||||
# Get actual user's home directory
|
||||
sudo_user = os.environ.get('SUDO_USER')
|
||||
if sudo_user:
|
||||
return os.path.join("/home", sudo_user, "Documents")
|
||||
return os.path.join(os.path.expanduser("~"), "Documents")
|
||||
|
||||
def get_workbench_cursor_path(translator=None) -> str:
|
||||
"""Get Cursor workbench.desktop.main.js path"""
|
||||
|
||||
def get_workbench_cursor_path(translator: Any = None) -> str:
|
||||
"""Get Cursor workbench.desktop.main.js path
|
||||
|
||||
Args:
|
||||
translator: Optional translator for internationalization
|
||||
|
||||
Returns:
|
||||
str: Path to the workbench.desktop.main.js file
|
||||
|
||||
Raises:
|
||||
OSError: If the file is not found or the OS is not supported
|
||||
"""
|
||||
system = platform.system()
|
||||
|
||||
# Read configuration
|
||||
config_dir = os.path.join(get_user_documents_path(), ".cursor-free-vip")
|
||||
config_file = os.path.join(config_dir, "config.ini")
|
||||
config = configparser.ConfigParser()
|
||||
|
||||
if os.path.exists(config_file):
|
||||
config.read(config_file)
|
||||
config = get_config(translator)
|
||||
|
||||
# Define paths for different operating systems
|
||||
paths_map = {
|
||||
"Darwin": { # macOS
|
||||
"base": "/Applications/Cursor.app/Contents/Resources/app",
|
||||
@@ -65,53 +83,81 @@ def get_workbench_cursor_path(translator=None) -> str:
|
||||
"main": "out\\vs\\workbench\\workbench.desktop.main.js"
|
||||
},
|
||||
"Linux": {
|
||||
"bases": ["/opt/Cursor/resources/app", "/usr/share/cursor/resources/app", "/usr/lib/cursor/app/"],
|
||||
"bases": [
|
||||
"/opt/Cursor/resources/app",
|
||||
"/usr/share/cursor/resources/app",
|
||||
"/usr/lib/cursor/app/"
|
||||
],
|
||||
"main": "out/vs/workbench/workbench.desktop.main.js"
|
||||
}
|
||||
}
|
||||
|
||||
# Add extracted AppImage paths for Linux
|
||||
if system == "Linux":
|
||||
# Add extracted AppImage with correct usr structure
|
||||
extracted_usr_paths = glob.glob(os.path.expanduser("~/squashfs-root/usr/share/cursor/resources/app"))
|
||||
|
||||
paths_map["Linux"]["bases"].extend(extracted_usr_paths)
|
||||
|
||||
# Check if the system is supported
|
||||
if system not in paths_map:
|
||||
raise OSError(translator.get('reset.unsupported_os', system=system) if translator else f"不支持的操作系统: {system}")
|
||||
error_msg = f"Unsupported operating system: {system}"
|
||||
logger.error(error_msg)
|
||||
raise OSError(translator.get('reset.unsupported_os', system=system) if translator else error_msg)
|
||||
|
||||
# For Linux, check all possible base paths
|
||||
if system == "Linux":
|
||||
for base in paths_map["Linux"]["bases"]:
|
||||
main_path = os.path.join(base, paths_map["Linux"]["main"])
|
||||
print(f"{Fore.CYAN}{EMOJI['INFO']} Checking path: {main_path}{Style.RESET_ALL}")
|
||||
logger.info(f"Checking path: {main_path}")
|
||||
if os.path.exists(main_path):
|
||||
return main_path
|
||||
|
||||
# For Windows and macOS, get the base path from config
|
||||
if system == "Windows":
|
||||
base_path = config.get('WindowsPaths', 'cursor_path')
|
||||
if config and config.has_section('WindowsPaths') and config.has_option('WindowsPaths', 'cursor_path'):
|
||||
base_path = config.get('WindowsPaths', 'cursor_path')
|
||||
else:
|
||||
logger.warning("WindowsPaths section or cursor_path option not found in config")
|
||||
base_path = os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Programs', 'Cursor', 'resources', 'app')
|
||||
elif system == "Darwin":
|
||||
base_path = paths_map[system]["base"]
|
||||
if config.has_section('MacPaths') and config.has_option('MacPaths', 'cursor_path'):
|
||||
if config and config.has_section('MacPaths') and config.has_option('MacPaths', 'cursor_path'):
|
||||
base_path = config.get('MacPaths', 'cursor_path')
|
||||
else: # Linux
|
||||
# For Linux, we've already checked all bases in the loop above
|
||||
# If we're here, it means none of the bases worked, so we'll use the first one
|
||||
base_path = paths_map[system]["bases"][0]
|
||||
if config.has_section('LinuxPaths') and config.has_option('LinuxPaths', 'cursor_path'):
|
||||
else:
|
||||
base_path = paths_map[system]["base"]
|
||||
else: # Linux (fallback if none of the bases worked)
|
||||
if config and config.has_section('LinuxPaths') and config.has_option('LinuxPaths', 'cursor_path'):
|
||||
base_path = config.get('LinuxPaths', 'cursor_path')
|
||||
else:
|
||||
base_path = paths_map[system]["bases"][0]
|
||||
|
||||
# Construct the full path to the main.js file
|
||||
main_path = os.path.join(base_path, paths_map[system]["main"])
|
||||
|
||||
# Check if the file exists
|
||||
if not os.path.exists(main_path):
|
||||
raise OSError(translator.get('reset.file_not_found', path=main_path) if translator else f"未找到 Cursor main.js 文件: {main_path}")
|
||||
error_msg = f"Cursor main.js file not found: {main_path}"
|
||||
logger.error(error_msg)
|
||||
raise OSError(translator.get('reset.file_not_found', path=main_path) if translator else error_msg)
|
||||
|
||||
return main_path
|
||||
|
||||
|
||||
def modify_workbench_js(file_path: str, translator=None) -> bool:
|
||||
def modify_workbench_js(file_path: str, translator: Any = None) -> bool:
|
||||
"""
|
||||
Modify file content
|
||||
Modify workbench.desktop.main.js file to bypass token limit
|
||||
|
||||
Args:
|
||||
file_path: Path to the workbench.desktop.main.js file
|
||||
translator: Optional translator for internationalization
|
||||
|
||||
Returns:
|
||||
bool: True if the modification was successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
# Check if file exists
|
||||
if not os.path.exists(file_path):
|
||||
logger.error(f"File not found: {file_path}")
|
||||
return False
|
||||
|
||||
# Save original file permissions
|
||||
original_stat = os.stat(file_path)
|
||||
original_mode = original_stat.st_mode
|
||||
@@ -121,33 +167,46 @@ def modify_workbench_js(file_path: str, translator=None) -> bool:
|
||||
# Create temporary file
|
||||
with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", errors="ignore", delete=False) as tmp_file:
|
||||
# Read original content
|
||||
with open(file_path, "r", encoding="utf-8", errors="ignore") as main_file:
|
||||
content = main_file.read()
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8", errors="ignore") as main_file:
|
||||
content = main_file.read()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to read file: {e}")
|
||||
os.unlink(tmp_file.name)
|
||||
return False
|
||||
|
||||
# Define patterns to replace
|
||||
patterns = {
|
||||
# 通用按钮替换模式
|
||||
r'B(k,D(Ln,{title:"Upgrade to Pro",size:"small",get codicon(){return A.rocket},get onClick(){return t.pay}}),null)': r'B(k,D(Ln,{title:"yeongpin GitHub",size:"small",get codicon(){return A.github},get onClick(){return function(){window.open("https://github.com/yeongpin/cursor-free-vip","_blank")}}}),null)',
|
||||
# Button replacement patterns
|
||||
r'B(k,D(Ln,{title:"Upgrade to Pro",size:"small",get codicon(){return A.rocket},get onClick(){return t.pay}}),null)':
|
||||
r'B(k,D(Ln,{title:"yeongpin GitHub",size:"small",get codicon(){return A.github},get onClick(){return function(){window.open("https://github.com/yeongpin/cursor-free-vip","_blank")}}}),null)',
|
||||
|
||||
# Windows/Linux
|
||||
r'M(x,I(as,{title:"Upgrade to Pro",size:"small",get codicon(){return $.rocket},get onClick(){return t.pay}}),null)': r'M(x,I(as,{title:"yeongpin GitHub",size:"small",get codicon(){return $.github},get onClick(){return function(){window.open("https://github.com/yeongpin/cursor-free-vip","_blank")}}}),null)',
|
||||
r'M(x,I(as,{title:"Upgrade to Pro",size:"small",get codicon(){return $.rocket},get onClick(){return t.pay}}),null)':
|
||||
r'M(x,I(as,{title:"yeongpin GitHub",size:"small",get codicon(){return $.github},get onClick(){return function(){window.open("https://github.com/yeongpin/cursor-free-vip","_blank")}}}),null)',
|
||||
|
||||
# Mac 通用按钮替换模式
|
||||
r'$(k,E(Ks,{title:"Upgrade to Pro",size:"small",get codicon(){return F.rocket},get onClick(){return t.pay}}),null)': r'$(k,E(Ks,{title:"yeongpin GitHub",size:"small",get codicon(){return F.rocket},get onClick(){return function(){window.open("https://github.com/yeongpin/cursor-free-vip","_blank")}}}),null)',
|
||||
# Badge 替换
|
||||
# Mac button replacement pattern
|
||||
r'$(k,E(Ks,{title:"Upgrade to Pro",size:"small",get codicon(){return F.rocket},get onClick(){return t.pay}}),null)':
|
||||
r'$(k,E(Ks,{title:"yeongpin GitHub",size:"small",get codicon(){return F.rocket},get onClick(){return function(){window.open("https://github.com/yeongpin/cursor-free-vip","_blank")}}}),null)',
|
||||
|
||||
# Badge replacement
|
||||
r'<div>Pro Trial': r'<div>Pro',
|
||||
|
||||
r'py-1">Auto-select': r'py-1">Bypass-Version-Pin',
|
||||
|
||||
#
|
||||
r'async getEffectiveTokenLimit(e){const n=e.modelName;if(!n)return 2e5;':r'async getEffectiveTokenLimit(e){return 9000000;const n=e.modelName;if(!n)return 9e5;',
|
||||
# Pro
|
||||
r'var DWr=ne("<div class=settings__item_description>You are currently signed in with <strong></strong>.");': r'var DWr=ne("<div class=settings__item_description>You are currently signed in with <strong></strong>. <h1>Pro</h1>");',
|
||||
# Token limit bypass
|
||||
r'async getEffectiveTokenLimit(e){const n=e.modelName;if(!n)return 2e5;':
|
||||
r'async getEffectiveTokenLimit(e){return 9000000;const n=e.modelName;if(!n)return 9e5;',
|
||||
|
||||
# Toast 替换
|
||||
# Pro status
|
||||
r'var DWr=ne("<div class=settings__item_description>You are currently signed in with <strong></strong>.");':
|
||||
r'var DWr=ne("<div class=settings__item_description>You are currently signed in with <strong></strong>. <h1>Pro</h1>");',
|
||||
|
||||
# Toast replacement
|
||||
r'notifications-toasts': r'notifications-toasts hidden'
|
||||
}
|
||||
|
||||
# 使用patterns进行替换
|
||||
# Apply replacements
|
||||
for old_pattern, new_pattern in patterns.items():
|
||||
content = content.replace(old_pattern, new_pattern)
|
||||
|
||||
@@ -158,24 +217,40 @@ def modify_workbench_js(file_path: str, translator=None) -> bool:
|
||||
# Backup original file with timestamp
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
backup_path = f"{file_path}.backup.{timestamp}"
|
||||
shutil.copy2(file_path, backup_path)
|
||||
print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {translator.get('reset.backup_created', path=backup_path)}{Style.RESET_ALL}")
|
||||
try:
|
||||
shutil.copy2(file_path, backup_path)
|
||||
logger.info(f"Backup created: {backup_path}")
|
||||
print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {translator.get('reset.backup_created', path=backup_path) if translator else f'Backup created: {backup_path}'}{Style.RESET_ALL}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create backup: {e}")
|
||||
os.unlink(tmp_path)
|
||||
return False
|
||||
|
||||
# Move temporary file to original position
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
shutil.move(tmp_path, file_path)
|
||||
try:
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
shutil.move(tmp_path, file_path)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to replace original file: {e}")
|
||||
return False
|
||||
|
||||
# Restore original permissions
|
||||
os.chmod(file_path, original_mode)
|
||||
if os.name != "nt": # Not Windows
|
||||
os.chown(file_path, original_uid, original_gid)
|
||||
try:
|
||||
os.chmod(file_path, original_mode)
|
||||
if os.name != "nt": # Not Windows
|
||||
os.chown(file_path, original_uid, original_gid)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to restore original permissions: {e}")
|
||||
# Continue anyway as this is not critical
|
||||
|
||||
print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {translator.get('reset.file_modified')}{Style.RESET_ALL}")
|
||||
logger.info("File modified successfully")
|
||||
print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {translator.get('reset.file_modified') if translator else 'File modified successfully'}{Style.RESET_ALL}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('reset.modify_file_failed', error=str(e))}{Style.RESET_ALL}")
|
||||
logger.error(f"Failed to modify file: {e}")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('reset.modify_file_failed', error=str(e)) if translator else f'Failed to modify file: {str(e)}'}{Style.RESET_ALL}")
|
||||
if "tmp_path" in locals():
|
||||
try:
|
||||
os.unlink(tmp_path)
|
||||
@@ -183,19 +258,53 @@ def modify_workbench_js(file_path: str, translator=None) -> bool:
|
||||
pass
|
||||
return False
|
||||
|
||||
def run(translator=None):
|
||||
config = get_config(translator)
|
||||
if not config:
|
||||
def run(translator: Any = None) -> bool:
|
||||
"""Run the token limit bypass
|
||||
|
||||
Args:
|
||||
translator: Optional translator for internationalization
|
||||
|
||||
Returns:
|
||||
bool: True if successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
config = get_config(translator)
|
||||
if not config:
|
||||
logger.error("Failed to get configuration")
|
||||
return False
|
||||
|
||||
print(f"\n{Fore.CYAN}{'='*50}{Style.RESET_ALL}")
|
||||
print(f"{Fore.CYAN}{EMOJI['RESET']} {translator.get('bypass_token_limit.title') if translator else 'Bypass Token Limit'}{Style.RESET_ALL}")
|
||||
print(f"{Fore.CYAN}{'='*50}{Style.RESET_ALL}")
|
||||
|
||||
# Get workbench.desktop.main.js path
|
||||
try:
|
||||
workbench_path = get_workbench_cursor_path(translator)
|
||||
logger.info(f"Found workbench.desktop.main.js at: {workbench_path}")
|
||||
except OSError as e:
|
||||
logger.error(f"Failed to get workbench path: {e}")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {str(e)}{Style.RESET_ALL}")
|
||||
return False
|
||||
|
||||
# Modify the file
|
||||
success = modify_workbench_js(workbench_path, translator)
|
||||
|
||||
print(f"\n{Fore.CYAN}{'='*50}{Style.RESET_ALL}")
|
||||
input(f"{EMOJI['INFO']} {translator.get('bypass_token_limit.press_enter') if translator else 'Press Enter to continue...'}")
|
||||
|
||||
return success
|
||||
except Exception as e:
|
||||
logger.error(f"Error in run function: {e}")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} An unexpected error occurred: {str(e)}{Style.RESET_ALL}")
|
||||
return False
|
||||
print(f"\n{Fore.CYAN}{'='*50}{Style.RESET_ALL}")
|
||||
print(f"{Fore.CYAN}{EMOJI['RESET']} {translator.get('bypass_token_limit.title')}{Style.RESET_ALL}")
|
||||
print(f"{Fore.CYAN}{'='*50}{Style.RESET_ALL}")
|
||||
|
||||
modify_workbench_js(get_workbench_cursor_path(translator), translator)
|
||||
|
||||
print(f"\n{Fore.CYAN}{'='*50}{Style.RESET_ALL}")
|
||||
input(f"{EMOJI['INFO']} {translator.get('bypass_token_limit.press_enter')}...")
|
||||
|
||||
if __name__ == "__main__":
|
||||
from main import translator as main_translator
|
||||
run(main_translator)
|
||||
try:
|
||||
from main import translator as main_translator
|
||||
run(main_translator)
|
||||
except ImportError:
|
||||
logger.warning("Failed to import translator from main.py, running without translation")
|
||||
run(None)
|
||||
except Exception as e:
|
||||
logger.error(f"Error running bypass_token_limit.py: {e}")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} An unexpected error occurred: {str(e)}{Style.RESET_ALL}")
|
||||
@@ -4,13 +4,24 @@ import shutil
|
||||
import platform
|
||||
import configparser
|
||||
import time
|
||||
import logging
|
||||
from colorama import Fore, Style, init
|
||||
import sys
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, List, Union, Tuple, Any
|
||||
from utils import get_user_documents_path
|
||||
|
||||
# Initialize colorama
|
||||
init()
|
||||
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__)
|
||||
|
||||
# Define emoji constants
|
||||
EMOJI = {
|
||||
@@ -24,8 +35,18 @@ EMOJI = {
|
||||
'VERSION': '🏷️'
|
||||
}
|
||||
|
||||
def get_product_json_path(translator=None):
|
||||
"""Get Cursor product.json path"""
|
||||
def get_product_json_path(translator: Any = None) -> str:
|
||||
"""Get Cursor product.json path based on the operating system.
|
||||
|
||||
Args:
|
||||
translator: Optional translator for internationalization
|
||||
|
||||
Returns:
|
||||
str: Path to the product.json file
|
||||
|
||||
Raises:
|
||||
OSError: If the file is not found or the OS is not supported
|
||||
"""
|
||||
system = platform.system()
|
||||
|
||||
# Read configuration
|
||||
@@ -36,10 +57,13 @@ def get_product_json_path(translator=None):
|
||||
if os.path.exists(config_file):
|
||||
config.read(config_file)
|
||||
|
||||
# Define paths for different operating systems
|
||||
if system == "Windows":
|
||||
localappdata = os.environ.get("LOCALAPPDATA")
|
||||
if not localappdata:
|
||||
raise OSError(translator.get('bypass.localappdata_not_found') if translator else "LOCALAPPDATA environment variable not found")
|
||||
error_msg = "LOCALAPPDATA environment variable not found"
|
||||
logger.error(error_msg)
|
||||
raise OSError(translator.get('bypass.localappdata_not_found') if translator else error_msg)
|
||||
|
||||
product_json_path = os.path.join(localappdata, "Programs", "Cursor", "resources", "app", "product.json")
|
||||
|
||||
@@ -66,38 +90,72 @@ def get_product_json_path(translator=None):
|
||||
if os.path.exists(extracted_usr_paths):
|
||||
possible_paths.append(extracted_usr_paths)
|
||||
|
||||
# Find first existing path
|
||||
for path in possible_paths:
|
||||
if os.path.exists(path):
|
||||
product_json_path = path
|
||||
break
|
||||
else:
|
||||
raise OSError(translator.get('bypass.product_json_not_found') if translator else "product.json not found in common Linux paths")
|
||||
error_msg = "product.json not found in common Linux paths"
|
||||
logger.error(error_msg)
|
||||
raise OSError(translator.get('bypass.product_json_not_found') if translator else error_msg)
|
||||
|
||||
else:
|
||||
raise OSError(translator.get('bypass.unsupported_os', system=system) if translator else f"Unsupported operating system: {system}")
|
||||
error_msg = f"Unsupported operating system: {system}"
|
||||
logger.error(error_msg)
|
||||
raise OSError(translator.get('bypass.unsupported_os', system=system) if translator else error_msg)
|
||||
|
||||
# Verify that the file exists
|
||||
if not os.path.exists(product_json_path):
|
||||
raise OSError(translator.get('bypass.file_not_found', path=product_json_path) if translator else f"File not found: {product_json_path}")
|
||||
error_msg = f"File not found: {product_json_path}"
|
||||
logger.error(error_msg)
|
||||
raise OSError(translator.get('bypass.file_not_found', path=product_json_path) if translator else error_msg)
|
||||
|
||||
logger.info(f"Found product.json at: {product_json_path}")
|
||||
return product_json_path
|
||||
|
||||
def compare_versions(version1, version2):
|
||||
"""Compare two version strings"""
|
||||
v1_parts = [int(x) for x in version1.split('.')]
|
||||
v2_parts = [int(x) for x in version2.split('.')]
|
||||
def compare_versions(version1: str, version2: str) -> int:
|
||||
"""Compare two version strings.
|
||||
|
||||
for i in range(max(len(v1_parts), len(v2_parts))):
|
||||
v1 = v1_parts[i] if i < len(v1_parts) else 0
|
||||
v2 = v2_parts[i] if i < len(v2_parts) else 0
|
||||
if v1 < v2:
|
||||
Args:
|
||||
version1: First version string (e.g., "0.48.7")
|
||||
version2: Second version string (e.g., "0.46.0")
|
||||
|
||||
Returns:
|
||||
int: -1 if version1 < version2, 0 if version1 == version2, 1 if version1 > version2
|
||||
"""
|
||||
try:
|
||||
v1_parts = [int(x) for x in version1.split('.')]
|
||||
v2_parts = [int(x) for x in version2.split('.')]
|
||||
|
||||
for i in range(max(len(v1_parts), len(v2_parts))):
|
||||
v1 = v1_parts[i] if i < len(v1_parts) else 0
|
||||
v2 = v2_parts[i] if i < len(v2_parts) else 0
|
||||
if v1 < v2:
|
||||
return -1
|
||||
elif v1 > v2:
|
||||
return 1
|
||||
|
||||
return 0
|
||||
except (ValueError, TypeError) as e:
|
||||
logger.warning(f"Error comparing versions {version1} and {version2}: {e}")
|
||||
# Fall back to string comparison if numeric comparison fails
|
||||
if version1 < version2:
|
||||
return -1
|
||||
elif v1 > v2:
|
||||
elif version1 > version2:
|
||||
return 1
|
||||
|
||||
return 0
|
||||
else:
|
||||
return 0
|
||||
|
||||
def bypass_version(translator=None):
|
||||
"""Bypass Cursor version check by modifying product.json"""
|
||||
def bypass_version(translator: Any = None) -> bool:
|
||||
"""Bypass Cursor version check by modifying product.json.
|
||||
|
||||
Args:
|
||||
translator: Optional translator for internationalization
|
||||
|
||||
Returns:
|
||||
bool: True if the version was successfully bypassed, False otherwise
|
||||
"""
|
||||
try:
|
||||
print(f"\n{Fore.CYAN}{EMOJI['INFO']} {translator.get('bypass.starting') if translator else 'Starting Cursor version bypass...'}{Style.RESET_ALL}")
|
||||
|
||||
@@ -114,7 +172,12 @@ def bypass_version(translator=None):
|
||||
try:
|
||||
with open(product_json_path, "r", encoding="utf-8") as f:
|
||||
product_data = json.load(f)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Invalid JSON in product.json: {e}")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('bypass.invalid_json', error=str(e)) if translator else f'Invalid JSON in product.json: {str(e)}'}{Style.RESET_ALL}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to read product.json: {e}")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('bypass.read_failed', error=str(e)) if translator else f'Failed to read product.json: {str(e)}'}{Style.RESET_ALL}")
|
||||
return False
|
||||
|
||||
@@ -122,39 +185,98 @@ def bypass_version(translator=None):
|
||||
current_version = product_data.get("version", "0.0.0")
|
||||
print(f"{Fore.CYAN}{EMOJI['VERSION']} {translator.get('bypass.current_version', version=current_version) if translator else f'Current version: {current_version}'}{Style.RESET_ALL}")
|
||||
|
||||
# Target version to set
|
||||
new_version = "0.48.7"
|
||||
|
||||
# Check if version needs to be modified
|
||||
if compare_versions(current_version, "0.46.0") < 0:
|
||||
# Create backup
|
||||
timestamp = time.strftime("%Y%m%d%H%M%S")
|
||||
backup_path = f"{product_json_path}.{timestamp}"
|
||||
shutil.copy2(product_json_path, backup_path)
|
||||
print(f"{Fore.GREEN}{EMOJI['BACKUP']} {translator.get('bypass.backup_created', path=backup_path) if translator else f'Backup created: {backup_path}'}{Style.RESET_ALL}")
|
||||
try:
|
||||
shutil.copy2(product_json_path, backup_path)
|
||||
print(f"{Fore.GREEN}{EMOJI['BACKUP']} {translator.get('bypass.backup_created', path=backup_path) if translator else f'Backup created: {backup_path}'}{Style.RESET_ALL}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create backup: {e}")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('bypass.backup_failed', error=str(e)) if translator else f'Failed to create backup: {str(e)}'}{Style.RESET_ALL}")
|
||||
return False
|
||||
|
||||
# Modify version
|
||||
new_version = "0.48.7"
|
||||
product_data["version"] = new_version
|
||||
|
||||
# Save modified product.json
|
||||
try:
|
||||
with open(product_json_path, "w", encoding="utf-8") as f:
|
||||
json.dump(product_data, f, indent=2)
|
||||
logger.info(f"Version updated from {current_version} to {new_version}")
|
||||
print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {translator.get('bypass.version_updated', old=current_version, new=new_version) if translator else f'Version updated from {current_version} to {new_version}'}{Style.RESET_ALL}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to write product.json: {e}")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('bypass.write_failed', error=str(e)) if translator else f'Failed to write product.json: {str(e)}'}{Style.RESET_ALL}")
|
||||
|
||||
# Try to restore from backup if write fails
|
||||
try:
|
||||
if os.path.exists(backup_path):
|
||||
shutil.copy2(backup_path, product_json_path)
|
||||
print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('bypass.restored_from_backup') if translator else 'Restored from backup'}{Style.RESET_ALL}")
|
||||
except Exception as restore_error:
|
||||
logger.error(f"Failed to restore from backup: {restore_error}")
|
||||
|
||||
return False
|
||||
else:
|
||||
print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('bypass.no_update_needed', version=current_version) if translator else f'No update needed. Current version {current_version} is already >= 0.46.0'}{Style.RESET_ALL}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Version bypass failed: {e}")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('bypass.bypass_failed', error=str(e)) if translator else f'Version bypass failed: {str(e)}'}{Style.RESET_ALL}")
|
||||
print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('bypass.stack_trace') if translator else 'Stack trace'}: {traceback.format_exc()}{Style.RESET_ALL}")
|
||||
return False
|
||||
|
||||
def main(translator=None):
|
||||
"""Main function"""
|
||||
return bypass_version(translator)
|
||||
def run(translator: Any = None) -> bool:
|
||||
"""Main function to run the version bypass.
|
||||
|
||||
Args:
|
||||
translator: Optional translator for internationalization
|
||||
|
||||
Returns:
|
||||
bool: True if successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
print(f"\n{Fore.CYAN}{'='*50}{Style.RESET_ALL}")
|
||||
print(f"{Fore.CYAN}{EMOJI['RESET']} {translator.get('bypass_version.title') if translator else 'Bypass Version Check'}{Style.RESET_ALL}")
|
||||
print(f"{Fore.CYAN}{'='*50}{Style.RESET_ALL}")
|
||||
|
||||
success = bypass_version(translator)
|
||||
|
||||
print(f"\n{Fore.CYAN}{'='*50}{Style.RESET_ALL}")
|
||||
input(f"{EMOJI['INFO']} {translator.get('bypass_version.press_enter') if translator else 'Press Enter to continue...'}")
|
||||
|
||||
return success
|
||||
except Exception as e:
|
||||
logger.error(f"Error in run function: {e}")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} An unexpected error occurred: {str(e)}{Style.RESET_ALL}")
|
||||
return False
|
||||
|
||||
def main(translator: Any = None) -> bool:
|
||||
"""Entry point when called directly.
|
||||
|
||||
Args:
|
||||
translator: Optional translator for internationalization
|
||||
|
||||
Returns:
|
||||
bool: True if successful, False otherwise
|
||||
"""
|
||||
return run(translator)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
try:
|
||||
from main import translator as main_translator
|
||||
main(main_translator)
|
||||
except ImportError:
|
||||
logger.warning("Failed to import translator from main.py, running without translation")
|
||||
main(None)
|
||||
except Exception as e:
|
||||
logger.error(f"Error running bypass_version.py: {e}")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} An unexpected error occurred: {str(e)}{Style.RESET_ALL}")
|
||||
@@ -4,10 +4,20 @@ import time
|
||||
import hashlib
|
||||
import base64
|
||||
import struct
|
||||
import logging
|
||||
from colorama import Fore, Style, init
|
||||
from typing import Optional, Dict, Union, Any, Tuple
|
||||
|
||||
# Initialize colorama
|
||||
init()
|
||||
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__)
|
||||
|
||||
# Define emoji constants
|
||||
EMOJI = {
|
||||
@@ -20,22 +30,57 @@ EMOJI = {
|
||||
}
|
||||
|
||||
def generate_hashed64_hex(input_str: str, salt: str = '') -> str:
|
||||
"""Generate a SHA-256 hash of input + salt and return as hex"""
|
||||
"""Generate a SHA-256 hash of input + salt and return as hex.
|
||||
|
||||
Args:
|
||||
input_str: The input string to hash
|
||||
salt: Optional salt to add to the input string
|
||||
|
||||
Returns:
|
||||
str: Hexadecimal representation of the hash
|
||||
"""
|
||||
if not input_str:
|
||||
logger.warning("Empty input string provided for hashing")
|
||||
return ""
|
||||
|
||||
hash_obj = hashlib.sha256()
|
||||
hash_obj.update((input_str + salt).encode('utf-8'))
|
||||
return hash_obj.hexdigest()
|
||||
|
||||
def obfuscate_bytes(byte_array: bytearray) -> bytearray:
|
||||
"""Obfuscate bytes using the algorithm from utils.js"""
|
||||
"""Obfuscate bytes using the algorithm from utils.js.
|
||||
|
||||
Args:
|
||||
byte_array: The byte array to obfuscate
|
||||
|
||||
Returns:
|
||||
bytearray: The obfuscated byte array
|
||||
"""
|
||||
if not byte_array:
|
||||
return bytearray()
|
||||
|
||||
t = 165
|
||||
for r in range(len(byte_array)):
|
||||
byte_array[r] = ((byte_array[r] ^ t) + (r % 256)) & 0xFF
|
||||
t = byte_array[r]
|
||||
return byte_array
|
||||
|
||||
def generate_cursor_checksum(token: str, translator=None) -> str:
|
||||
"""Generate Cursor checksum from token using the algorithm"""
|
||||
def generate_cursor_checksum(token: str, translator: Any = None) -> str:
|
||||
"""Generate Cursor checksum from token using the algorithm.
|
||||
|
||||
Args:
|
||||
token: The authentication token
|
||||
translator: Optional translator for internationalization
|
||||
|
||||
Returns:
|
||||
str: The generated checksum
|
||||
"""
|
||||
try:
|
||||
# Validate input
|
||||
if not token or not isinstance(token, str):
|
||||
logger.error("Invalid token provided")
|
||||
return ""
|
||||
|
||||
# Clean the token
|
||||
clean_token = token.strip()
|
||||
|
||||
@@ -54,15 +99,16 @@ def generate_cursor_checksum(token: str, translator=None) -> str:
|
||||
# Combine final checksum
|
||||
return f"{encoded_checksum}{machine_id}/{mac_machine_id}"
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating checksum: {e}")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('auth_check.error_generating_checksum', error=str(e)) if translator else f'Error generating checksum: {str(e)}'}{Style.RESET_ALL}")
|
||||
return ""
|
||||
|
||||
def check_user_authorized(token: str, translator=None) -> bool:
|
||||
def check_user_authorized(token: str, translator: Any = None) -> bool:
|
||||
"""
|
||||
Check if the user is authorized with the given token
|
||||
Check if the user is authorized with the given token.
|
||||
|
||||
Args:
|
||||
token (str): The authorization token
|
||||
token: The authorization token
|
||||
translator: Optional translator for internationalization
|
||||
|
||||
Returns:
|
||||
@@ -71,6 +117,12 @@ def check_user_authorized(token: str, translator=None) -> bool:
|
||||
try:
|
||||
print(f"{Fore.CYAN}{EMOJI['CHECK']} {translator.get('auth_check.checking_authorization') if translator else 'Checking authorization...'}{Style.RESET_ALL}")
|
||||
|
||||
# Validate input
|
||||
if not token or not isinstance(token, str):
|
||||
logger.error("Invalid token provided")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('auth_check.invalid_token') if translator else 'Invalid token'}{Style.RESET_ALL}")
|
||||
return False
|
||||
|
||||
# Clean the token
|
||||
if token and '%3A%3A' in token:
|
||||
token = token.split('%3A%3A')[1]
|
||||
@@ -81,6 +133,7 @@ def check_user_authorized(token: str, translator=None) -> bool:
|
||||
token = token.strip()
|
||||
|
||||
if not token or len(token) < 10: # Add a basic validation for token length
|
||||
logger.error("Token too short or empty after cleaning")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('auth_check.invalid_token') if translator else 'Invalid token'}{Style.RESET_ALL}")
|
||||
return False
|
||||
|
||||
@@ -90,7 +143,10 @@ def check_user_authorized(token: str, translator=None) -> bool:
|
||||
try:
|
||||
# Generate checksum
|
||||
checksum = generate_cursor_checksum(token, translator)
|
||||
|
||||
if not checksum:
|
||||
logger.error("Failed to generate checksum")
|
||||
return False
|
||||
|
||||
# Create request headers
|
||||
headers = {
|
||||
'accept-encoding': 'gzip',
|
||||
@@ -107,53 +163,137 @@ def check_user_authorized(token: str, translator=None) -> bool:
|
||||
|
||||
print(f"{Fore.CYAN}{EMOJI['INFO']} {translator.get('auth_check.checking_usage_information') if translator else 'Checking usage information...'}{Style.RESET_ALL}")
|
||||
|
||||
# Make the request - this endpoint doesn't need a request body
|
||||
usage_response = requests.post(
|
||||
'https://api2.cursor.sh/aiserver.v1.DashboardService/GetUsageBasedPremiumRequests',
|
||||
headers=headers,
|
||||
data=b'', # Empty body
|
||||
timeout=10
|
||||
)
|
||||
# Make the request with timeout and retry
|
||||
max_retries = 3
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
usage_response = requests.post(
|
||||
'https://api2.cursor.sh/aiserver.v1.DashboardService/GetUsageBasedPremiumRequests',
|
||||
headers=headers,
|
||||
data=b'', # Empty body
|
||||
timeout=10
|
||||
)
|
||||
break
|
||||
except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
|
||||
if attempt < max_retries - 1:
|
||||
logger.warning(f"Request attempt {attempt + 1} failed: {e}. Retrying...")
|
||||
time.sleep(2)
|
||||
else:
|
||||
raise
|
||||
|
||||
print(f"{Fore.CYAN}{EMOJI['INFO']} {translator.get('auth_check.usage_response', response=usage_response.status_code) if translator else f'Usage response status: {usage_response.status_code}'}{Style.RESET_ALL}")
|
||||
|
||||
if usage_response.status_code == 200:
|
||||
logger.info("User is authorized")
|
||||
print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {translator.get('auth_check.user_authorized') if translator else 'User is authorized'}{Style.RESET_ALL}")
|
||||
return True
|
||||
elif usage_response.status_code == 401 or usage_response.status_code == 403:
|
||||
logger.warning("User is unauthorized")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('auth_check.user_unauthorized') if translator else 'User is unauthorized'}{Style.RESET_ALL}")
|
||||
return False
|
||||
else:
|
||||
logger.warning(f"Unexpected status code: {usage_response.status_code}")
|
||||
print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('auth_check.unexpected_status_code', code=usage_response.status_code) if translator else f'Unexpected status code: {usage_response.status_code}'}{Style.RESET_ALL}")
|
||||
|
||||
# If the token at least looks like a valid JWT, consider it valid
|
||||
if token.startswith('eyJ') and '.' in token and len(token) > 100:
|
||||
logger.info("Token appears to be in JWT format, but API check returned an unexpected status code")
|
||||
print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('auth_check.jwt_token_warning') if translator else 'Token appears to be in JWT format, but API check returned an unexpected status code. The token might be valid but API access is restricted.'}{Style.RESET_ALL}")
|
||||
return True
|
||||
|
||||
return False
|
||||
except requests.exceptions.Timeout:
|
||||
logger.error("Request timed out")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('auth_check.request_timeout') if translator else 'Request timed out'}{Style.RESET_ALL}")
|
||||
|
||||
# If the token at least looks like a valid JWT, consider it valid even if the API check fails
|
||||
if token.startswith('eyJ') and '.' in token and len(token) > 100:
|
||||
logger.info("Token appears to be in JWT format, but request timed out")
|
||||
print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('auth_check.jwt_token_warning') if translator else 'Token appears to be in JWT format, but API check timed out. The token might be valid but API access is restricted.'}{Style.RESET_ALL}")
|
||||
return True
|
||||
|
||||
return False
|
||||
except requests.exceptions.ConnectionError:
|
||||
logger.error("Connection error")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('auth_check.connection_error') if translator else 'Connection error'}{Style.RESET_ALL}")
|
||||
|
||||
# If the token at least looks like a valid JWT, consider it valid even if the API check fails
|
||||
if token.startswith('eyJ') and '.' in token and len(token) > 100:
|
||||
logger.info("Token appears to be in JWT format, but connection failed")
|
||||
print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('auth_check.jwt_token_warning') if translator else 'Token appears to be in JWT format, but API connection failed. The token might be valid but API access is restricted.'}{Style.RESET_ALL}")
|
||||
return True
|
||||
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking usage: {e}")
|
||||
print(f"{Fore.YELLOW}{EMOJI['WARNING']} Error checking usage: {str(e)}{Style.RESET_ALL}")
|
||||
|
||||
# If the token at least looks like a valid JWT, consider it valid even if the API check fails
|
||||
if token.startswith('eyJ') and '.' in token and len(token) > 100:
|
||||
logger.info("Token appears to be in JWT format, but API check failed")
|
||||
print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('auth_check.jwt_token_warning') if translator else 'Token appears to be in JWT format, but API check failed. The token might be valid but API access is restricted.'}{Style.RESET_ALL}")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
logger.error("Request timed out")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('auth_check.request_timeout') if translator else 'Request timed out'}{Style.RESET_ALL}")
|
||||
return False
|
||||
except requests.exceptions.ConnectionError:
|
||||
logger.error("Connection error")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('auth_check.connection_error') if translator else 'Connection error'}{Style.RESET_ALL}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking authorization: {e}")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('auth_check.check_error', error=str(e)) if translator else f'Error checking authorization: {str(e)}'}{Style.RESET_ALL}")
|
||||
return False
|
||||
|
||||
def run(translator=None):
|
||||
"""Run function to be called from main.py"""
|
||||
def get_token_from_database(translator: Any = None) -> str:
|
||||
"""
|
||||
Get token from database using cursor_acc_info.py.
|
||||
|
||||
Args:
|
||||
translator: Optional translator for internationalization
|
||||
|
||||
Returns:
|
||||
str: The token if found, empty string otherwise
|
||||
"""
|
||||
try:
|
||||
print(f"{Fore.CYAN}{EMOJI['INFO']} {translator.get('auth_check.getting_token_from_db') if translator else 'Getting token from database...'}{Style.RESET_ALL}")
|
||||
|
||||
# Import functions from cursor_acc_info.py
|
||||
from cursor_acc_info import get_token
|
||||
|
||||
# Get token using the get_token function
|
||||
token = get_token()
|
||||
|
||||
if token:
|
||||
logger.info("Token found in database")
|
||||
print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {translator.get('auth_check.token_found_in_db') if translator else 'Token found in database'}{Style.RESET_ALL}")
|
||||
return token
|
||||
else:
|
||||
logger.warning("Token not found in database")
|
||||
print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('auth_check.token_not_found_in_db') if translator else 'Token not found in database'}{Style.RESET_ALL}")
|
||||
return ""
|
||||
except ImportError:
|
||||
logger.error("cursor_acc_info.py not found")
|
||||
print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('auth_check.cursor_acc_info_not_found') if translator else 'cursor_acc_info.py not found'}{Style.RESET_ALL}")
|
||||
return ""
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting token from database: {e}")
|
||||
print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('auth_check.error_getting_token_from_db', error=str(e)) if translator else f'Error getting token from database: {str(e)}'}{Style.RESET_ALL}")
|
||||
return ""
|
||||
|
||||
def run(translator: Any = None) -> bool:
|
||||
"""Run function to be called from main.py.
|
||||
|
||||
Args:
|
||||
translator: Optional translator for internationalization
|
||||
|
||||
Returns:
|
||||
bool: True if authorization successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
# Ask user if they want to get token from database or input manually
|
||||
choice = input(f"{Fore.CYAN}{EMOJI['INFO']} {translator.get('auth_check.token_source') if translator else 'Get token from database or input manually? (d/m, default: d): '}{Style.RESET_ALL}").strip().lower()
|
||||
@@ -162,23 +302,7 @@ def run(translator=None):
|
||||
|
||||
# If user chooses database or default
|
||||
if not choice or choice == 'd':
|
||||
print(f"{Fore.CYAN}{EMOJI['INFO']} {translator.get('auth_check.getting_token_from_db') if translator else 'Getting token from database...'}{Style.RESET_ALL}")
|
||||
|
||||
try:
|
||||
# Import functions from cursor_acc_info.py
|
||||
from cursor_acc_info import get_token
|
||||
|
||||
# Get token using the get_token function
|
||||
token = get_token()
|
||||
|
||||
if token:
|
||||
print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {translator.get('auth_check.token_found_in_db') if translator else 'Token found in database'}{Style.RESET_ALL}")
|
||||
else:
|
||||
print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('auth_check.token_not_found_in_db') if translator else 'Token not found in database'}{Style.RESET_ALL}")
|
||||
except ImportError:
|
||||
print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('auth_check.cursor_acc_info_not_found') if translator else 'cursor_acc_info.py not found'}{Style.RESET_ALL}")
|
||||
except Exception as e:
|
||||
print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('auth_check.error_getting_token_from_db', error=str(e)) if translator else f'Error getting token from database: {str(e)}'}{Style.RESET_ALL}")
|
||||
token = get_token_from_database(translator)
|
||||
|
||||
# If token not found in database or user chooses manual input
|
||||
if not token:
|
||||
@@ -193,22 +317,50 @@ def run(translator=None):
|
||||
is_authorized = check_user_authorized(token, translator)
|
||||
|
||||
if is_authorized:
|
||||
logger.info("Authorization successful")
|
||||
print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {translator.get('auth_check.authorization_successful') if translator else 'Authorization successful!'}{Style.RESET_ALL}")
|
||||
else:
|
||||
logger.warning("Authorization failed")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('auth_check.authorization_failed') if translator else 'Authorization failed!'}{Style.RESET_ALL}")
|
||||
|
||||
return is_authorized
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print(f"\n{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('auth_check.operation_cancelled') if translator else 'Operation cancelled by user'}{Style.RESET_ALL}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('auth_check.unexpected_error', error=str(e)) if translator else f'Unexpected error: {str(e)}'}{Style.RESET_ALL}")
|
||||
logger.error(f"Error in run function: {e}")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} An unexpected error occurred: {str(e)}{Style.RESET_ALL}")
|
||||
return False
|
||||
|
||||
def main(translator=None):
|
||||
"""Main function to check user authorization"""
|
||||
return run(translator)
|
||||
def main(translator: Any = None) -> bool:
|
||||
"""Main function to be called when script is run directly.
|
||||
|
||||
Args:
|
||||
translator: Optional translator for internationalization
|
||||
|
||||
Returns:
|
||||
bool: True if authorization successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
print(f"\n{Fore.CYAN}{'='*50}{Style.RESET_ALL}")
|
||||
print(f"{Fore.CYAN}{EMOJI['CHECK']} {translator.get('auth_check.title') if translator else 'Check User Authorization'}{Style.RESET_ALL}")
|
||||
print(f"{Fore.CYAN}{'='*50}{Style.RESET_ALL}")
|
||||
|
||||
result = run(translator)
|
||||
|
||||
print(f"\n{Fore.CYAN}{'='*50}{Style.RESET_ALL}")
|
||||
input(f"{EMOJI['INFO']} {translator.get('auth_check.press_enter') if translator else 'Press Enter to continue...'}")
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Error in main function: {e}")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} An unexpected error occurred: {str(e)}{Style.RESET_ALL}")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
try:
|
||||
from main import translator as main_translator
|
||||
main(main_translator)
|
||||
except ImportError:
|
||||
logger.warning("Failed to import translator from main.py, running without translation")
|
||||
main(None)
|
||||
except Exception as e:
|
||||
logger.error(f"Error running check_user_authorized.py: {e}")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} An unexpected error occurred: {str(e)}{Style.RESET_ALL}")
|
||||
763
config.py
763
config.py
@@ -1,11 +1,29 @@
|
||||
import os
|
||||
import sys
|
||||
import configparser
|
||||
from colorama import Fore, Style
|
||||
from utils import get_user_documents_path, get_linux_cursor_path, get_default_driver_path, get_default_browser_path
|
||||
import shutil
|
||||
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": "⚠️",
|
||||
@@ -15,45 +33,103 @@ EMOJI = {
|
||||
"ARROW": "➡️",
|
||||
"USER": "👤",
|
||||
"KEY": "🔑",
|
||||
"SETTINGS": "⚙️"
|
||||
"SETTINGS": "⚙️",
|
||||
"CONFIG": "📝"
|
||||
}
|
||||
|
||||
# global config cache
|
||||
# Global config cache
|
||||
_config_cache = None
|
||||
|
||||
def setup_config(translator=None):
|
||||
"""Setup configuration file and return config object"""
|
||||
try:
|
||||
# get documents path
|
||||
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
|
||||
print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('config.documents_path_not_found', fallback='Documents path not found, using current directory') if translator else 'Documents path not found, using current directory'}{Style.RESET_ALL}")
|
||||
# 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
|
||||
# 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, only print message when directory not exists
|
||||
# Create config directory
|
||||
dir_exists = os.path.exists(config_dir)
|
||||
try:
|
||||
os.makedirs(config_dir, exist_ok=True)
|
||||
if not dir_exists: # only print message when directory not exists
|
||||
print(f"{Fore.CYAN}{EMOJI['INFO']} {translator.get('config.config_dir_created', path=config_dir) if translator else f'Config directory created: {config_dir}'}{Style.RESET_ALL}")
|
||||
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
|
||||
import tempfile
|
||||
# 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"))
|
||||
os.makedirs(config_dir, exist_ok=True)
|
||||
if not temp_exists: # only print message when temporary directory not exists
|
||||
print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('config.using_temp_dir', path=config_dir, error=str(e)) if translator else f'Using temporary directory due to error: {config_dir} (Error: {str(e)})'}{Style.RESET_ALL}")
|
||||
|
||||
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
|
||||
|
||||
# create config object
|
||||
config = configparser.ConfigParser()
|
||||
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 = {
|
||||
@@ -70,7 +146,7 @@ def setup_config(translator=None):
|
||||
'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 使用 Chrome 驱动
|
||||
'operagx_driver_path': get_default_driver_path('chrome') # Opera GX uses Chrome driver
|
||||
},
|
||||
'Turnstile': {
|
||||
'handle_turnstile_time': '2',
|
||||
@@ -98,13 +174,13 @@ def setup_config(translator=None):
|
||||
'enabled_account_info': 'True'
|
||||
},
|
||||
'OAuth': {
|
||||
'show_selection_alert': False, # 默认不显示选择提示弹窗
|
||||
'timeout': 120,
|
||||
'max_attempts': 3
|
||||
'show_selection_alert': 'False',
|
||||
'timeout': '120',
|
||||
'max_attempts': '3'
|
||||
},
|
||||
'Token': {
|
||||
'refresh_server': 'https://token.cursorpro.com.cn',
|
||||
'enable_refresh': True
|
||||
'enable_refresh': 'True'
|
||||
},
|
||||
'Language': {
|
||||
'current_language': '', # Set by local system detection if empty
|
||||
@@ -115,266 +191,411 @@ def setup_config(translator=None):
|
||||
}
|
||||
|
||||
# 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":
|
||||
appdata = os.getenv("APPDATA")
|
||||
localappdata = os.getenv("LOCALAPPDATA", "")
|
||||
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
|
||||
os.makedirs(os.path.dirname(default_config['WindowsPaths']['storage_path']), exist_ok=True)
|
||||
|
||||
self._add_windows_paths(default_config)
|
||||
elif sys.platform == "darwin":
|
||||
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
|
||||
os.makedirs(os.path.dirname(default_config['MacPaths']['storage_path']), exist_ok=True)
|
||||
|
||||
self._add_macos_paths(default_config)
|
||||
elif sys.platform == "linux":
|
||||
# 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:
|
||||
print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('config.neither_cursor_nor_cursor_directory_found', config_base=config_base) if translator else f'Neither Cursor nor cursor directory found in {config_base}'}{Style.RESET_ALL}")
|
||||
if root_home:
|
||||
print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('config.also_checked', path=f'{root_home}/.config') if translator else f'Also checked {root_home}/.config'}{Style.RESET_ALL}")
|
||||
print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('config.please_make_sure_cursor_is_installed_and_has_been_run_at_least_once') if translator else 'Please make sure Cursor is installed and has been run at least once'}{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 ""
|
||||
|
||||
# Verify paths and permissions
|
||||
try:
|
||||
# Check storage directory
|
||||
if storage_dir and not os.path.exists(storage_dir):
|
||||
print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('config.storage_directory_not_found', storage_dir=storage_dir) if translator else f'Storage directory not found: {storage_dir}'}{Style.RESET_ALL}")
|
||||
print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('config.please_make_sure_cursor_is_installed_and_has_been_run_at_least_once') if translator else 'Please make sure Cursor is installed and has been run at least once'}{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)
|
||||
print(f"{Fore.GREEN}{EMOJI['INFO']} {translator.get('config.storage_file_found', storage_path=storage_path) if translator else f'Storage file found: {storage_path}'}{Style.RESET_ALL}")
|
||||
print(f"{Fore.GREEN}{EMOJI['INFO']} {translator.get('config.file_size', size=stat.st_size) if translator else f'File size: {stat.st_size} bytes'}{Style.RESET_ALL}")
|
||||
print(f"{Fore.GREEN}{EMOJI['INFO']} {translator.get('config.file_permissions', permissions=oct(stat.st_mode & 0o777)) if translator else f'File permissions: {oct(stat.st_mode & 0o777)}'}{Style.RESET_ALL}")
|
||||
print(f"{Fore.GREEN}{EMOJI['INFO']} {translator.get('config.file_owner', owner=stat.st_uid) if translator else f'File owner: {stat.st_uid}'}{Style.RESET_ALL}")
|
||||
print(f"{Fore.GREEN}{EMOJI['INFO']} {translator.get('config.file_group', group=stat.st_gid) if translator else f'File group: {stat.st_gid}'}{Style.RESET_ALL}")
|
||||
except Exception as e:
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('config.error_getting_file_stats', error=str(e)) if translator else f'Error getting file stats: {str(e)}'}{Style.RESET_ALL}")
|
||||
|
||||
# Check if file is readable and writable
|
||||
if not os.access(storage_path, os.R_OK | os.W_OK):
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('config.permission_denied', storage_path=storage_path) if translator else f'Permission denied: {storage_path}'}{Style.RESET_ALL}")
|
||||
if sudo_user:
|
||||
print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('config.try_running', command=f'chown {sudo_user}:{sudo_user} {storage_path}') if translator else f'Try running: chown {sudo_user}:{sudo_user} {storage_path}'}{Style.RESET_ALL}")
|
||||
print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('config.and') if translator else 'And'}: chmod 644 {storage_path}{Style.RESET_ALL}")
|
||||
else:
|
||||
print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('config.try_running', command=f'chown {current_user}:{current_user} {storage_path}') if translator else f'Try running: chown {current_user}:{current_user} {storage_path}'}{Style.RESET_ALL}")
|
||||
print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('config.and') if translator else 'And'}: 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():
|
||||
print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('config.storage_file_is_empty', storage_path=storage_path) if translator else f'Storage file is empty: {storage_path}'}{Style.RESET_ALL}")
|
||||
print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('config.the_file_might_be_corrupted_please_reinstall_cursor') if translator else 'The file might be corrupted, please reinstall Cursor'}{Style.RESET_ALL}")
|
||||
else:
|
||||
print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {translator.get('config.storage_file_is_valid_and_contains_data') if translator else 'Storage file is valid and contains data'}{Style.RESET_ALL}")
|
||||
except Exception as e:
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('config.error_reading_storage_file', error=str(e)) if translator else f'Error reading storage file: {str(e)}'}{Style.RESET_ALL}")
|
||||
print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('config.the_file_might_be_corrupted_please_reinstall_cursor') if translator else 'The file might be corrupted. Please reinstall Cursor'}{Style.RESET_ALL}")
|
||||
elif storage_path:
|
||||
print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('config.storage_file_not_found', storage_path=storage_path) if translator else f'Storage file not found: {storage_path}'}{Style.RESET_ALL}")
|
||||
print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('config.please_make_sure_cursor_is_installed_and_has_been_run_at_least_once') if translator else 'Please make sure Cursor is installed and has been run at least once'}{Style.RESET_ALL}")
|
||||
|
||||
except (OSError, IOError) as e:
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('config.error_checking_linux_paths', error=str(e)) if translator else f'Error checking Linux paths: {str(e)}'}{Style.RESET_ALL}")
|
||||
|
||||
# Define all paths using the found cursor directory
|
||||
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 ""
|
||||
}
|
||||
|
||||
# Add tempmail_plus configuration
|
||||
default_config['TempMailPlus'] = {
|
||||
'enabled': 'false',
|
||||
'email': '',
|
||||
'epin': ''
|
||||
}
|
||||
|
||||
# Read existing configuration and merge
|
||||
if os.path.exists(config_file):
|
||||
config.read(config_file, encoding='utf-8')
|
||||
config_modified = False
|
||||
|
||||
for section, options in default_config.items():
|
||||
if not config.has_section(section):
|
||||
config.add_section(section)
|
||||
config_modified = True
|
||||
for option, value in options.items():
|
||||
if not config.has_option(section, option):
|
||||
config.set(section, option, str(value))
|
||||
config_modified = True
|
||||
if translator:
|
||||
print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('config.config_option_added', option=f'{section}.{option}') if translator else f'Config option added: {section}.{option}'}{Style.RESET_ALL}")
|
||||
|
||||
if config_modified:
|
||||
with open(config_file, 'w', encoding='utf-8') as f:
|
||||
config.write(f)
|
||||
if translator:
|
||||
print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {translator.get('config.config_updated') if translator else 'Config updated'}{Style.RESET_ALL}")
|
||||
else:
|
||||
for section, options in default_config.items():
|
||||
config.add_section(section)
|
||||
for option, value in options.items():
|
||||
config.set(section, option, str(value))
|
||||
|
||||
with open(config_file, 'w', encoding='utf-8') as f:
|
||||
config.write(f)
|
||||
if translator:
|
||||
print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {translator.get('config.config_created', config_file=config_file) if translator else f'Config created: {config_file}'}{Style.RESET_ALL}")
|
||||
|
||||
return config
|
||||
|
||||
except Exception as e:
|
||||
if translator:
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('config.config_setup_error', error=str(e)) if translator else f'Error setting up config: {str(e)}'}{Style.RESET_ALL}")
|
||||
return None
|
||||
self._add_linux_paths(default_config)
|
||||
|
||||
def print_config(config, translator=None):
|
||||
"""Print configuration in a readable format"""
|
||||
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.YELLOW}{EMOJI['WARNING']} {translator.get('config.config_not_available') if translator else 'Configuration not available'}{Style.RESET_ALL}")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} Configuration not available{Style.RESET_ALL}")
|
||||
return
|
||||
|
||||
print(f"\n{Fore.CYAN}{EMOJI['INFO']} {translator.get('config.configuration') if translator else 'Configuration'}:{Style.RESET_ALL}")
|
||||
print(f"\n{Fore.CYAN}{'─' * 70}{Style.RESET_ALL}")
|
||||
for section in config.sections():
|
||||
print(f"{Fore.GREEN}[{section}]{Style.RESET_ALL}")
|
||||
for key, value in config.items(section):
|
||||
# 对布尔值进行特殊处理,使其显示为彩色
|
||||
if value.lower() in ('true', 'yes', 'on', '1'):
|
||||
value_display = f"{Fore.GREEN}{translator.get('config.enabled') if translator else 'Enabled'}{Style.RESET_ALL}"
|
||||
elif value.lower() in ('false', 'no', 'off', '0'):
|
||||
value_display = f"{Fore.RED}{translator.get('config.disabled') if translator else 'Disabled'}{Style.RESET_ALL}"
|
||||
else:
|
||||
value_display = value
|
||||
|
||||
print(f" {key} = {value_display}")
|
||||
print(f"\n{Fore.CYAN}{EMOJI['CONFIG']} Configuration:{Style.RESET_ALL}")
|
||||
|
||||
print(f"\n{Fore.CYAN}{'─' * 70}{Style.RESET_ALL}")
|
||||
config_dir = os.path.join(get_user_documents_path(), ".cursor-free-vip", "config.ini")
|
||||
print(f"{Fore.CYAN}{EMOJI['INFO']} {translator.get('config.config_directory') if translator else 'Config Directory'}: {config_dir}{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}")
|
||||
|
||||
print()
|
||||
|
||||
def force_update_config(translator=None):
|
||||
"""
|
||||
Force update configuration file with latest defaults if update check is enabled.
|
||||
def force_update_config(translator: Any = None) -> configparser.ConfigParser:
|
||||
"""Force update configuration
|
||||
|
||||
Args:
|
||||
translator: Translator instance
|
||||
translator: Optional translator for internationalization
|
||||
|
||||
Returns:
|
||||
ConfigParser instance or None if failed
|
||||
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")
|
||||
current_time = datetime.datetime.now()
|
||||
|
||||
# If the config file exists, check if forced update is enabled
|
||||
|
||||
if os.path.exists(config_file):
|
||||
# First, read the existing configuration
|
||||
existing_config = configparser.ConfigParser()
|
||||
existing_config.read(config_file, encoding='utf-8')
|
||||
# Check if "enabled_update_check" is True
|
||||
update_enabled = True # Default to True if not set
|
||||
if existing_config.has_section('Utils') and existing_config.has_option('Utils', 'enabled_force_update'):
|
||||
update_enabled = existing_config.get('Utils', 'enabled_force_update').strip().lower() in ('true', 'yes', '1', 'on')
|
||||
|
||||
if update_enabled:
|
||||
try:
|
||||
# Create a backup
|
||||
backup_file = f"{config_file}.bak.{current_time.strftime('%Y%m%d_%H%M%S')}"
|
||||
shutil.copy2(config_file, backup_file)
|
||||
if translator:
|
||||
print(f"\n{Fore.CYAN}{EMOJI['INFO']} {translator.get('config.backup_created', path=backup_file) if translator else f'Backup created: {backup_file}'}{Style.RESET_ALL}")
|
||||
print(f"\n{Fore.CYAN}{EMOJI['INFO']} {translator.get('config.config_force_update_enabled') if translator else 'Config file force update enabled'}{Style.RESET_ALL}")
|
||||
# Delete the original config file (forced update)
|
||||
os.remove(config_file)
|
||||
if translator:
|
||||
print(f"{Fore.CYAN}{EMOJI['INFO']} {translator.get('config.config_removed') if translator else 'Config file removed for forced update'}{Style.RESET_ALL}")
|
||||
except Exception as e:
|
||||
if translator:
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('config.backup_failed', error=str(e)) if translator else f'Failed to backup config: {str(e)}'}{Style.RESET_ALL}")
|
||||
else:
|
||||
if translator:
|
||||
print(f"\n{Fore.CYAN}{EMOJI['INFO']} {translator.get('config.config_force_update_disabled', fallback='Config file force update disabled by configuration. Keeping existing config file.') if translator else 'Config file force update disabled by configuration. Keeping existing config file.'}{Style.RESET_ALL}")
|
||||
|
||||
# Generate a new (or updated) configuration if needed
|
||||
return setup_config(translator)
|
||||
|
||||
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:
|
||||
if translator:
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('config.force_update_failed', error=str(e)) if translator else f'Force update config failed: {str(e)}'}{Style.RESET_ALL}")
|
||||
return None
|
||||
logger.error(f"Error creating config backup: {e}")
|
||||
|
||||
# Setup new config
|
||||
return setup_config(translator)
|
||||
|
||||
def get_config(translator=None):
|
||||
"""Get existing config or create new one"""
|
||||
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 None:
|
||||
_config_cache = setup_config(translator)
|
||||
return _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()
|
||||
475
enhanced_config.py
Normal file
475
enhanced_config.py
Normal file
@@ -0,0 +1,475 @@
|
||||
|
||||
|
||||
import os
|
||||
import sys
|
||||
import configparser
|
||||
import logging
|
||||
import tempfile
|
||||
import datetime
|
||||
import platform
|
||||
import json
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any, Union, List, Tuple, TypeVar, Generic
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from colorama import Fore, Style, init
|
||||
import yaml
|
||||
|
||||
# Initialize colorama
|
||||
init(autoreset=True)
|
||||
|
||||
# Configure enhanced logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
handlers=[
|
||||
logging.FileHandler("cursor_free_vip.log"),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Enhanced emoji constants
|
||||
EMOJI = {
|
||||
"INFO": "ℹ️",
|
||||
"WARNING": "⚠️",
|
||||
"ERROR": "❌",
|
||||
"SUCCESS": "✅",
|
||||
"ADMIN": "🔒",
|
||||
"ARROW": "➡️",
|
||||
"USER": "👤",
|
||||
"KEY": "🔑",
|
||||
"SETTINGS": "⚙️",
|
||||
"CONFIG": "📝",
|
||||
"VALIDATION": "🔍",
|
||||
"BACKUP": "💾",
|
||||
"RESTORE": "🔄",
|
||||
"SECURITY": "🔐",
|
||||
"PERFORMANCE": "⚡"
|
||||
}
|
||||
|
||||
class ConfigFormat(Enum):
|
||||
INI = "ini"
|
||||
JSON = "json"
|
||||
YAML = "yaml"
|
||||
|
||||
class ValidationError(Exception):
|
||||
pass
|
||||
|
||||
@dataclass
|
||||
class BrowserConfig:
|
||||
default_browser: str = "chrome"
|
||||
chrome_path: str = ""
|
||||
chrome_driver_path: str = ""
|
||||
edge_path: str = ""
|
||||
edge_driver_path: str = ""
|
||||
firefox_path: str = ""
|
||||
firefox_driver_path: str = ""
|
||||
brave_path: str = ""
|
||||
brave_driver_path: str = ""
|
||||
opera_path: str = ""
|
||||
opera_driver_path: str = ""
|
||||
operagx_path: str = ""
|
||||
operagx_driver_path: str = ""
|
||||
|
||||
@dataclass
|
||||
class TimingConfig:
|
||||
min_random_time: float = 0.1
|
||||
max_random_time: float = 0.8
|
||||
page_load_wait: str = "0.1-0.8"
|
||||
input_wait: str = "0.3-0.8"
|
||||
submit_wait: str = "0.5-1.5"
|
||||
verification_code_input: str = "0.1-0.3"
|
||||
verification_success_wait: str = "2-3"
|
||||
verification_retry_wait: str = "2-3"
|
||||
email_check_initial_wait: str = "4-6"
|
||||
email_refresh_wait: str = "2-4"
|
||||
settings_page_load_wait: str = "1-2"
|
||||
failed_retry_time: str = "0.5-1"
|
||||
retry_interval: str = "8-12"
|
||||
max_timeout: int = 160
|
||||
|
||||
@dataclass
|
||||
class SecurityConfig:
|
||||
enable_encryption: bool = True
|
||||
encryption_key: str = ""
|
||||
enable_backup: bool = True
|
||||
backup_retention_days: int = 30
|
||||
enable_audit_log: bool = True
|
||||
sensitive_fields: List[str] = field(default_factory=lambda: ["password", "token", "key"])
|
||||
|
||||
class EnhancedConfigManager:
|
||||
|
||||
def __init__(self, translator: Any = None, config_format: ConfigFormat = ConfigFormat.INI):
|
||||
self.translator = translator
|
||||
self.config_format = config_format
|
||||
self.config_dir = None
|
||||
self.config_file = None
|
||||
self.backup_dir = None
|
||||
self.audit_log_file = None
|
||||
self._config_cache = {}
|
||||
self._validation_schema = self._load_validation_schema()
|
||||
|
||||
def _get_message(self, key: str, fallback: str, **kwargs) -> str:
|
||||
"""Get translated message or fallback with enhanced error handling"""
|
||||
try:
|
||||
if self.translator:
|
||||
return self.translator.get(key, fallback=fallback, **kwargs)
|
||||
except Exception as e:
|
||||
logger.warning(f"Translation error for key '{key}': {e}")
|
||||
return fallback.format(**kwargs) if kwargs else fallback
|
||||
|
||||
def _load_validation_schema(self) -> Dict[str, Any]:
|
||||
"""Load configuration validation schema"""
|
||||
return {
|
||||
"Browser": {
|
||||
"default_browser": {"type": "str", "allowed": ["chrome", "edge", "firefox", "brave", "opera", "operagx"]},
|
||||
"chrome_path": {"type": "str", "required": False},
|
||||
"chrome_driver_path": {"type": "str", "required": False}
|
||||
},
|
||||
"Timing": {
|
||||
"min_random_time": {"type": "float", "min": 0.0, "max": 10.0},
|
||||
"max_random_time": {"type": "float", "min": 0.0, "max": 10.0},
|
||||
"max_timeout": {"type": "int", "min": 10, "max": 600}
|
||||
},
|
||||
"Security": {
|
||||
"enable_encryption": {"type": "bool"},
|
||||
"backup_retention_days": {"type": "int", "min": 1, "max": 365}
|
||||
}
|
||||
}
|
||||
|
||||
def setup_config_directory(self) -> Tuple[str, str]:
|
||||
"""Setup configuration directory with enhanced error handling"""
|
||||
try:
|
||||
# Get documents path with fallback
|
||||
docs_path = self._get_documents_path()
|
||||
config_dir = os.path.normpath(os.path.join(docs_path, ".cursor-free-vip"))
|
||||
config_file = os.path.normpath(os.path.join(config_dir, self._get_config_filename()))
|
||||
|
||||
# Create directory structure
|
||||
self._create_directory_structure(config_dir)
|
||||
|
||||
self.config_dir = config_dir
|
||||
self.config_file = config_file
|
||||
self.backup_dir = os.path.join(config_dir, "backups")
|
||||
self.audit_log_file = os.path.join(config_dir, "audit.log")
|
||||
|
||||
return config_dir, config_file
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to setup config directory: {e}")
|
||||
raise ValidationError(f"Configuration setup failed: {e}")
|
||||
|
||||
def _get_documents_path(self) -> str:
|
||||
"""Get documents path with enhanced platform detection"""
|
||||
try:
|
||||
if platform.system() == "Windows":
|
||||
import winreg
|
||||
with winreg.OpenKey(winreg.HKEY_CURRENT_USER,
|
||||
"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders") as key:
|
||||
documents_path, _ = winreg.QueryValueEx(key, "Personal")
|
||||
return documents_path
|
||||
elif platform.system() == "Darwin": # macOS
|
||||
return os.path.expanduser("~/Documents")
|
||||
else: # Linux
|
||||
# Try XDG user directories
|
||||
xdg_config = os.path.expanduser("~/.config/user-dirs.dirs")
|
||||
if os.path.exists(xdg_config):
|
||||
with open(xdg_config, "r") as f:
|
||||
for line in f:
|
||||
if line.startswith("XDG_DOCUMENTS_DIR"):
|
||||
path = line.split("=")[1].strip().strip('"').replace("$HOME", os.path.expanduser("~"))
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
return os.path.expanduser("~/Documents")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to get documents path: {e}")
|
||||
return os.path.abspath('.')
|
||||
|
||||
def _get_config_filename(self) -> str:
|
||||
"""Get configuration filename based on format"""
|
||||
format_extensions = {
|
||||
ConfigFormat.INI: "config.ini",
|
||||
ConfigFormat.JSON: "config.json",
|
||||
ConfigFormat.YAML: "config.yaml"
|
||||
}
|
||||
return format_extensions.get(self.config_format, "config.ini")
|
||||
|
||||
def _create_directory_structure(self, config_dir: str) -> None:
|
||||
"""Create directory structure with proper permissions"""
|
||||
try:
|
||||
os.makedirs(config_dir, exist_ok=True)
|
||||
|
||||
# Create subdirectories
|
||||
subdirs = ["backups", "logs", "cache", "temp"]
|
||||
for subdir in subdirs:
|
||||
subdir_path = os.path.join(config_dir, subdir)
|
||||
os.makedirs(subdir_path, exist_ok=True)
|
||||
|
||||
# Set proper permissions on Unix systems
|
||||
if platform.system() != "Windows":
|
||||
os.chmod(config_dir, 0o700)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create directory structure: {e}")
|
||||
raise
|
||||
|
||||
def validate_config(self, config_data: Dict[str, Any]) -> List[str]:
|
||||
"""Validate configuration data against schema"""
|
||||
errors = []
|
||||
|
||||
for section, section_schema in self._validation_schema.items():
|
||||
if section not in config_data:
|
||||
continue
|
||||
|
||||
section_data = config_data[section]
|
||||
for key, validation in section_schema.items():
|
||||
if key not in section_data:
|
||||
if validation.get("required", False):
|
||||
errors.append(f"Missing required field: {section}.{key}")
|
||||
continue
|
||||
|
||||
value = section_data[key]
|
||||
value_type = validation.get("type")
|
||||
|
||||
# Type validation
|
||||
if value_type == "str" and not isinstance(value, str):
|
||||
errors.append(f"Invalid type for {section}.{key}: expected str, got {type(value)}")
|
||||
elif value_type == "int" and not isinstance(value, int):
|
||||
errors.append(f"Invalid type for {section}.{key}: expected int, got {type(value)}")
|
||||
elif value_type == "float" and not isinstance(value, (int, float)):
|
||||
errors.append(f"Invalid type for {section}.{key}: expected float, got {type(value)}")
|
||||
elif value_type == "bool" and not isinstance(value, bool):
|
||||
errors.append(f"Invalid type for {section}.{key}: expected bool, got {type(value)}")
|
||||
|
||||
# Range validation
|
||||
if "min" in validation and value < validation["min"]:
|
||||
errors.append(f"Value too small for {section}.{key}: {value} < {validation['min']}")
|
||||
if "max" in validation and value > validation["max"]:
|
||||
errors.append(f"Value too large for {section}.{key}: {value} > {validation['max']}")
|
||||
|
||||
# Allowed values validation
|
||||
if "allowed" in validation and value not in validation["allowed"]:
|
||||
errors.append(f"Invalid value for {section}.{key}: {value} not in {validation['allowed']}")
|
||||
|
||||
return errors
|
||||
|
||||
def get_default_config(self) -> Dict[str, Any]:
|
||||
"""Get enhanced default configuration"""
|
||||
from utils import get_default_browser_path, get_default_driver_path
|
||||
|
||||
config_dir = self.config_dir or os.path.join(self._get_documents_path(), ".cursor-free-vip")
|
||||
|
||||
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')
|
||||
},
|
||||
'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
|
||||
},
|
||||
'Security': {
|
||||
'enable_encryption': True,
|
||||
'encryption_key': '',
|
||||
'enable_backup': True,
|
||||
'backup_retention_days': 30,
|
||||
'enable_audit_log': True,
|
||||
'sensitive_fields': ['password', 'token', 'key', 'secret']
|
||||
},
|
||||
'Performance': {
|
||||
'enable_caching': True,
|
||||
'cache_ttl': 3600,
|
||||
'max_concurrent_operations': 5,
|
||||
'enable_compression': True
|
||||
},
|
||||
'Logging': {
|
||||
'log_level': 'INFO',
|
||||
'log_file': 'cursor_free_vip.log',
|
||||
'max_log_size': 10485760, # 10MB
|
||||
'log_rotation': 5
|
||||
}
|
||||
}
|
||||
|
||||
# Add system-specific paths
|
||||
self._add_system_paths(default_config)
|
||||
|
||||
return default_config
|
||||
|
||||
def _add_system_paths(self, config: Dict[str, Any]) -> None:
|
||||
system = platform.system()
|
||||
|
||||
if system == "Windows":
|
||||
self._add_windows_paths(config)
|
||||
elif system == "Darwin":
|
||||
self._add_macos_paths(config)
|
||||
else:
|
||||
self._add_linux_paths(config)
|
||||
|
||||
def _add_windows_paths(self, config: Dict[str, Any]) -> None:
|
||||
username = os.getenv('USERNAME', 'user')
|
||||
config['WindowsPaths'] = {
|
||||
'storage_path': f"C:\\Users\\{username}\\AppData\\Roaming\\Cursor\\User\\globalStorage\\storage.json",
|
||||
'sqlite_path': f"C:\\Users\\{username}\\AppData\\Roaming\\Cursor\\User\\globalStorage\\state.vscdb",
|
||||
'machine_id_path': f"C:\\Users\\{username}\\AppData\\Roaming\\Cursor\\machineId",
|
||||
'cursor_path': f"C:\\Users\\{username}\\AppData\\Local\\Programs\\Cursor\\resources\\app",
|
||||
'updater_path': f"C:\\Users\\{username}\\AppData\\Local\\cursor-updater",
|
||||
'update_yml_path': f"C:\\Users\\{username}\\AppData\\Local\\Programs\\Cursor\\resources\\app-update.yml",
|
||||
'product_json_path': f"C:\\Users\\{username}\\AppData\\Local\\Programs\\Cursor\\resources\\app\\product.json"
|
||||
}
|
||||
|
||||
def _add_macos_paths(self, config: Dict[str, Any]) -> None:
|
||||
username = os.getenv('USER', 'user')
|
||||
config['MacOSPaths'] = {
|
||||
'storage_path': f"/Users/{username}/Library/Application Support/Cursor/User/globalStorage/storage.json",
|
||||
'sqlite_path': f"/Users/{username}/Library/Application Support/Cursor/User/globalStorage/state.vscdb",
|
||||
'machine_id_path': f"/Users/{username}/Library/Application Support/Cursor/machineId",
|
||||
'cursor_path': f"/Applications/Cursor.app/Contents/Resources/app",
|
||||
'updater_path': f"/Users/{username}/Library/Application Support/cursor-updater",
|
||||
'update_yml_path': f"/Applications/Cursor.app/Contents/Resources/app-update.yml",
|
||||
'product_json_path': f"/Applications/Cursor.app/Contents/Resources/app/product.json"
|
||||
}
|
||||
|
||||
def _add_linux_paths(self, config: Dict[str, Any]) -> None:
|
||||
username = os.getenv('USER', 'user')
|
||||
config['LinuxPaths'] = {
|
||||
'storage_path': f"/home/{username}/.config/Cursor/User/globalStorage/storage.json",
|
||||
'sqlite_path': f"/home/{username}/.config/Cursor/User/globalStorage/state.vscdb",
|
||||
'machine_id_path': f"/home/{username}/.config/Cursor/machineid",
|
||||
'cursor_path': "/opt/Cursor/resources/app",
|
||||
'updater_path': f"/home/{username}/.config/cursor-updater",
|
||||
'update_yml_path': "/opt/Cursor/resources/app-update.yml",
|
||||
'product_json_path': "/opt/Cursor/resources/app/product.json"
|
||||
}
|
||||
|
||||
def save_config(self, config_data: Dict[str, Any], backup: bool = True) -> None:
|
||||
try:
|
||||
errors = self.validate_config(config_data)
|
||||
if errors:
|
||||
raise ValidationError(f"Configuration validation failed:\n" + "\n".join(errors))
|
||||
|
||||
if backup and self.backup_dir:
|
||||
self._create_backup()
|
||||
|
||||
if self.config_format == ConfigFormat.JSON:
|
||||
self._save_json_config(config_data)
|
||||
elif self.config_format == ConfigFormat.YAML:
|
||||
self._save_yaml_config(config_data)
|
||||
else:
|
||||
self._save_ini_config(config_data)
|
||||
|
||||
self._log_config_change("Configuration saved successfully")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save configuration: {e}")
|
||||
raise
|
||||
|
||||
def _create_backup(self) -> None:
|
||||
try:
|
||||
if not self.config_file or not os.path.exists(self.config_file):
|
||||
return
|
||||
|
||||
if not self.backup_dir:
|
||||
return
|
||||
|
||||
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
backup_filename = f"config_backup_{timestamp}.{self.config_format.value}"
|
||||
backup_path = os.path.join(self.backup_dir, backup_filename)
|
||||
|
||||
shutil.copy2(self.config_file, backup_path)
|
||||
|
||||
self._cleanup_old_backups()
|
||||
|
||||
logger.info(f"Configuration backup created: {backup_path}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to create backup: {e}")
|
||||
|
||||
def _cleanup_old_backups(self) -> None:
|
||||
try:
|
||||
if not self.backup_dir:
|
||||
return
|
||||
|
||||
retention_days = 30
|
||||
cutoff_time = datetime.datetime.now() - datetime.timedelta(days=retention_days)
|
||||
|
||||
for filename in os.listdir(self.backup_dir):
|
||||
if filename.startswith("config_backup_"):
|
||||
file_path = os.path.join(self.backup_dir, filename)
|
||||
file_time = datetime.datetime.fromtimestamp(os.path.getctime(file_path))
|
||||
|
||||
if file_time < cutoff_time:
|
||||
os.remove(file_path)
|
||||
logger.info(f"Removed old backup: {filename}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cleanup old backups: {e}")
|
||||
|
||||
def _save_json_config(self, config_data: Dict[str, Any]) -> None:
|
||||
if self.config_file:
|
||||
with open(self.config_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(config_data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
def _save_yaml_config(self, config_data: Dict[str, Any]) -> None:
|
||||
if self.config_file:
|
||||
with open(self.config_file, 'w', encoding='utf-8') as f:
|
||||
yaml.dump(config_data, f, default_flow_style=False, allow_unicode=True)
|
||||
|
||||
def _save_ini_config(self, config_data: Dict[str, Any]) -> None:
|
||||
config = configparser.ConfigParser()
|
||||
|
||||
for section, items in config_data.items():
|
||||
config.add_section(section)
|
||||
for key, value in items.items():
|
||||
config.set(section, key, str(value))
|
||||
|
||||
if self.config_file:
|
||||
with open(self.config_file, 'w', encoding='utf-8') as f:
|
||||
config.write(f)
|
||||
|
||||
def _log_config_change(self, message: str) -> None:
|
||||
try:
|
||||
if self.audit_log_file:
|
||||
timestamp = datetime.datetime.now().isoformat()
|
||||
log_entry = f"{timestamp} - {message}\n"
|
||||
|
||||
with open(self.audit_log_file, 'a', encoding='utf-8') as f:
|
||||
f.write(log_entry)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to log configuration change: {e}")
|
||||
|
||||
def create_config_manager(translator: Any = None, format_type: str = "ini") -> EnhancedConfigManager:
|
||||
format_map = {
|
||||
"ini": ConfigFormat.INI,
|
||||
"json": ConfigFormat.JSON,
|
||||
"yaml": ConfigFormat.YAML
|
||||
}
|
||||
|
||||
config_format = format_map.get(format_type.lower(), ConfigFormat.INI)
|
||||
return EnhancedConfigManager(translator, config_format)
|
||||
351
enhanced_error_handler.py
Normal file
351
enhanced_error_handler.py
Normal file
@@ -0,0 +1,351 @@
|
||||
|
||||
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
import traceback
|
||||
import json
|
||||
import time
|
||||
from typing import Optional, Dict, Any, Callable, List, Union
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
import threading
|
||||
from contextlib import contextmanager
|
||||
|
||||
class ErrorSeverity(Enum):
|
||||
LOW = "low"
|
||||
MEDIUM = "medium"
|
||||
HIGH = "high"
|
||||
CRITICAL = "critical"
|
||||
|
||||
class ErrorCategory(Enum):
|
||||
CONFIGURATION = "configuration"
|
||||
NETWORK = "network"
|
||||
PROCESS = "process"
|
||||
FILE_SYSTEM = "file_system"
|
||||
PERMISSION = "permission"
|
||||
VALIDATION = "validation"
|
||||
BROWSER = "browser"
|
||||
AUTHENTICATION = "authentication"
|
||||
SYSTEM = "system"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
@dataclass
|
||||
class ErrorInfo:
|
||||
timestamp: float
|
||||
category: ErrorCategory
|
||||
severity: ErrorSeverity
|
||||
message: str
|
||||
exception: Optional[Exception] = None
|
||||
traceback: Optional[str] = None
|
||||
context: Dict[str, Any] = field(default_factory=dict)
|
||||
user_action: Optional[str] = None
|
||||
resolved: bool = False
|
||||
resolution_time: Optional[float] = None
|
||||
|
||||
class ErrorRecoveryStrategy:
|
||||
|
||||
def __init__(self, max_retries: int = 3, backoff_factor: float = 2.0):
|
||||
self.max_retries = max_retries
|
||||
self.backoff_factor = backoff_factor
|
||||
self.retry_counts = {}
|
||||
|
||||
def should_retry(self, error_info: ErrorInfo) -> bool:
|
||||
"""Determine if operation should be retried"""
|
||||
error_key = f"{error_info.category.value}_{error_info.message}"
|
||||
current_retries = self.retry_counts.get(error_key, 0)
|
||||
|
||||
if current_retries >= self.max_retries:
|
||||
return False
|
||||
|
||||
# Don't retry critical errors
|
||||
if error_info.severity == ErrorSeverity.CRITICAL:
|
||||
return False
|
||||
|
||||
# Don't retry permission errors
|
||||
if error_info.category == ErrorCategory.PERMISSION:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def get_retry_delay(self, error_info: ErrorInfo) -> float:
|
||||
"""Get delay before next retry"""
|
||||
error_key = f"{error_info.category.value}_{error_info.message}"
|
||||
current_retries = self.retry_counts.get(error_key, 0)
|
||||
return (self.backoff_factor ** current_retries)
|
||||
|
||||
def record_retry(self, error_info: ErrorInfo) -> None:
|
||||
"""Record a retry attempt"""
|
||||
error_key = f"{error_info.category.value}_{error_info.message}"
|
||||
self.retry_counts[error_key] = self.retry_counts.get(error_key, 0) + 1
|
||||
|
||||
class EnhancedErrorHandler:
|
||||
"""Enhanced error handler with logging, categorization, and recovery"""
|
||||
|
||||
def __init__(self, log_file: Optional[str] = None, enable_recovery: bool = True):
|
||||
self.log_file = log_file or "cursor_free_vip_errors.log"
|
||||
self.enable_recovery = enable_recovery
|
||||
self.recovery_strategy = ErrorRecoveryStrategy()
|
||||
self.error_history: List[ErrorInfo] = []
|
||||
self.error_callbacks: Dict[ErrorCategory, List[Callable]] = {}
|
||||
self._setup_logging()
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def _setup_logging(self) -> None:
|
||||
"""Setup enhanced logging"""
|
||||
# Create logs directory if it doesn't exist
|
||||
log_dir = os.path.dirname(self.log_file)
|
||||
if log_dir:
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
|
||||
# Configure file handler
|
||||
file_handler = logging.FileHandler(self.log_file, encoding='utf-8')
|
||||
file_handler.setLevel(logging.DEBUG)
|
||||
|
||||
# Configure console handler
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(logging.INFO)
|
||||
|
||||
# Create formatter
|
||||
formatter = logging.Formatter(
|
||||
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
file_handler.setFormatter(formatter)
|
||||
console_handler.setFormatter(formatter)
|
||||
|
||||
# Configure logger
|
||||
self.logger = logging.getLogger('CursorFreeVIP.ErrorHandler')
|
||||
self.logger.setLevel(logging.DEBUG)
|
||||
self.logger.addHandler(file_handler)
|
||||
self.logger.addHandler(console_handler)
|
||||
|
||||
def categorize_error(self, exception: Exception, context: Optional[Dict[str, Any]] = None) -> ErrorCategory:
|
||||
"""Categorize error based on exception type and context"""
|
||||
exception_type = type(exception).__name__
|
||||
exception_message = str(exception).lower()
|
||||
|
||||
# Network errors
|
||||
if any(keyword in exception_message for keyword in ['connection', 'timeout', 'network', 'http', 'url']):
|
||||
return ErrorCategory.NETWORK
|
||||
|
||||
# File system errors
|
||||
if any(keyword in exception_message for keyword in ['file', 'directory', 'path', 'not found', 'permission']):
|
||||
if 'permission' in exception_message:
|
||||
return ErrorCategory.PERMISSION
|
||||
return ErrorCategory.FILE_SYSTEM
|
||||
|
||||
# Process errors
|
||||
if any(keyword in exception_message for keyword in ['process', 'pid', 'terminate', 'kill']):
|
||||
return ErrorCategory.PROCESS
|
||||
|
||||
# Browser errors
|
||||
if any(keyword in exception_message for keyword in ['browser', 'driver', 'selenium', 'chrome', 'firefox']):
|
||||
return ErrorCategory.BROWSER
|
||||
|
||||
# Authentication errors
|
||||
if any(keyword in exception_message for keyword in ['auth', 'login', 'token', 'credential']):
|
||||
return ErrorCategory.AUTHENTICATION
|
||||
|
||||
# Configuration errors
|
||||
if any(keyword in exception_message for keyword in ['config', 'setting', 'parameter']):
|
||||
return ErrorCategory.CONFIGURATION
|
||||
|
||||
# Validation errors
|
||||
if any(keyword in exception_message for keyword in ['validation', 'invalid', 'format']):
|
||||
return ErrorCategory.VALIDATION
|
||||
|
||||
# System errors
|
||||
if any(keyword in exception_message for keyword in ['system', 'os', 'platform']):
|
||||
return ErrorCategory.SYSTEM
|
||||
|
||||
return ErrorCategory.UNKNOWN
|
||||
|
||||
def determine_severity(self, exception: Exception, category: ErrorCategory) -> ErrorSeverity:
|
||||
"""Determine error severity"""
|
||||
exception_message = str(exception).lower()
|
||||
|
||||
# Critical errors
|
||||
if any(keyword in exception_message for keyword in ['fatal', 'critical', 'corrupt', 'broken']):
|
||||
return ErrorSeverity.CRITICAL
|
||||
|
||||
# High severity errors
|
||||
if category in [ErrorCategory.PERMISSION, ErrorCategory.AUTHENTICATION]:
|
||||
return ErrorSeverity.HIGH
|
||||
|
||||
if any(keyword in exception_message for keyword in ['access denied', 'unauthorized', 'forbidden']):
|
||||
return ErrorSeverity.HIGH
|
||||
|
||||
# Medium severity errors
|
||||
if category in [ErrorCategory.NETWORK, ErrorCategory.FILE_SYSTEM, ErrorCategory.PROCESS]:
|
||||
return ErrorSeverity.MEDIUM
|
||||
|
||||
# Low severity errors
|
||||
if category in [ErrorCategory.VALIDATION, ErrorCategory.CONFIGURATION]:
|
||||
return ErrorSeverity.LOW
|
||||
|
||||
return ErrorSeverity.MEDIUM
|
||||
|
||||
def handle_error(self, exception: Exception, context: Optional[Dict[str, Any]] = None,
|
||||
user_action: Optional[str] = None) -> ErrorInfo:
|
||||
with self._lock:
|
||||
category = self.categorize_error(exception, context)
|
||||
severity = self.determine_severity(exception, category)
|
||||
error_info = ErrorInfo(
|
||||
timestamp=time.time(),
|
||||
category=category,
|
||||
severity=severity,
|
||||
message=str(exception),
|
||||
exception=exception,
|
||||
traceback=traceback.format_exc(),
|
||||
context=context or {},
|
||||
user_action=user_action
|
||||
)
|
||||
|
||||
self._log_error(error_info)
|
||||
|
||||
self.error_history.append(error_info)
|
||||
|
||||
self._execute_callbacks(error_info)
|
||||
|
||||
if self.enable_recovery:
|
||||
self._attempt_recovery(error_info)
|
||||
|
||||
return error_info
|
||||
|
||||
def _log_error(self, error_info: ErrorInfo) -> None:
|
||||
log_message = f"""
|
||||
Error Details:
|
||||
- Category: {error_info.category.value}
|
||||
- Severity: {error_info.severity.value}
|
||||
- Message: {error_info.message}
|
||||
- Timestamp: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(error_info.timestamp))}
|
||||
- User Action: {error_info.user_action or 'N/A'}
|
||||
- Context: {json.dumps(error_info.context, indent=2)}
|
||||
"""
|
||||
|
||||
if error_info.traceback:
|
||||
log_message += f"\nTraceback:\n{error_info.traceback}"
|
||||
|
||||
if error_info.severity == ErrorSeverity.CRITICAL:
|
||||
self.logger.critical(log_message)
|
||||
elif error_info.severity == ErrorSeverity.HIGH:
|
||||
self.logger.error(log_message)
|
||||
elif error_info.severity == ErrorSeverity.MEDIUM:
|
||||
self.logger.warning(log_message)
|
||||
else:
|
||||
self.logger.info(log_message)
|
||||
|
||||
def _execute_callbacks(self, error_info: ErrorInfo) -> None:
|
||||
callbacks = self.error_callbacks.get(error_info.category, [])
|
||||
for callback in callbacks:
|
||||
try:
|
||||
callback(error_info)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error in callback: {e}")
|
||||
|
||||
def _attempt_recovery(self, error_info: ErrorInfo) -> None:
|
||||
if not self.recovery_strategy.should_retry(error_info):
|
||||
return
|
||||
|
||||
delay = self.recovery_strategy.get_retry_delay(error_info)
|
||||
self.logger.info(f"Attempting recovery in {delay:.2f} seconds...")
|
||||
|
||||
self.recovery_strategy.record_retry(error_info)
|
||||
|
||||
threading.Timer(delay, self._retry_operation, args=[error_info]).start()
|
||||
|
||||
def _retry_operation(self, error_info: ErrorInfo) -> None:
|
||||
self.logger.info(f"Retrying operation for error: {error_info.message}")
|
||||
|
||||
def register_callback(self, category: ErrorCategory, callback: Callable[[ErrorInfo], None]) -> None:
|
||||
if category not in self.error_callbacks:
|
||||
self.error_callbacks[category] = []
|
||||
self.error_callbacks[category].append(callback)
|
||||
|
||||
def get_error_summary(self, hours: int = 24) -> Dict[str, Any]:
|
||||
cutoff_time = time.time() - (hours * 3600)
|
||||
recent_errors = [e for e in self.error_history if e.timestamp >= cutoff_time]
|
||||
|
||||
summary = {
|
||||
'total_errors': len(recent_errors),
|
||||
'by_category': {},
|
||||
'by_severity': {},
|
||||
'most_common': [],
|
||||
'unresolved': len([e for e in recent_errors if not e.resolved])
|
||||
}
|
||||
|
||||
for error in recent_errors:
|
||||
category = error.category.value
|
||||
severity = error.severity.value
|
||||
|
||||
summary['by_category'][category] = summary['by_category'].get(category, 0) + 1
|
||||
summary['by_severity'][severity] = summary['by_severity'].get(severity, 0) + 1
|
||||
|
||||
error_messages = [e.message for e in recent_errors]
|
||||
from collections import Counter
|
||||
message_counts = Counter(error_messages)
|
||||
summary['most_common'] = message_counts.most_common(5)
|
||||
|
||||
return summary
|
||||
|
||||
def resolve_error(self, error_info: ErrorInfo, resolution: str) -> None:
|
||||
error_info.resolved = True
|
||||
error_info.resolution_time = time.time()
|
||||
error_info.user_action = resolution
|
||||
|
||||
self.logger.info(f"Error resolved: {error_info.message} - Resolution: {resolution}")
|
||||
|
||||
def clear_history(self, older_than_hours: int = 168) -> None:
|
||||
cutoff_time = time.time() - (older_than_hours * 3600)
|
||||
with self._lock:
|
||||
self.error_history = [e for e in self.error_history if e.timestamp >= cutoff_time]
|
||||
|
||||
self.logger.info(f"Cleared error history older than {older_than_hours} hours")
|
||||
|
||||
@contextmanager
|
||||
def error_context(handler: EnhancedErrorHandler, context: Optional[Dict[str, Any]] = None,
|
||||
user_action: Optional[str] = None):
|
||||
try:
|
||||
yield
|
||||
except Exception as e:
|
||||
handler.handle_error(e, context, user_action)
|
||||
raise
|
||||
|
||||
error_handler = EnhancedErrorHandler()
|
||||
|
||||
def handle_error(exception: Exception, context: Optional[Dict[str, Any]] = None,
|
||||
user_action: Optional[str] = None) -> ErrorInfo:
|
||||
return error_handler.handle_error(exception, context, user_action)
|
||||
|
||||
def safe_execute(func: Callable, *args, context: Optional[Dict[str, Any]] = None,
|
||||
user_action: Optional[str] = None, **kwargs) -> Any:
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
error_handler.handle_error(e, context, user_action)
|
||||
raise
|
||||
|
||||
def retry_on_error(func: Callable, max_retries: int = 3, *args,
|
||||
context: Optional[Dict[str, Any]] = None, user_action: Optional[str] = None, **kwargs) -> Any:
|
||||
last_exception: Optional[Exception] = None
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
last_exception = e
|
||||
error_info = error_handler.handle_error(e, context, user_action)
|
||||
|
||||
if attempt < max_retries - 1:
|
||||
delay = 2 ** attempt
|
||||
time.sleep(delay)
|
||||
continue
|
||||
else:
|
||||
break
|
||||
|
||||
if last_exception is not None:
|
||||
raise last_exception
|
||||
else:
|
||||
raise RuntimeError("Unexpected error in retry_on_error")
|
||||
677
enhanced_utils.py
Normal file
677
enhanced_utils.py
Normal file
@@ -0,0 +1,677 @@
|
||||
|
||||
|
||||
import os
|
||||
import sys
|
||||
import platform
|
||||
import random
|
||||
import shutil
|
||||
import logging
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
import hashlib
|
||||
import json
|
||||
from typing import Optional, Dict, List, Union, Tuple, Any, Callable
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
import psutil
|
||||
import requests
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
# Configure enhanced logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ProcessStatus(Enum):
|
||||
RUNNING = "running"
|
||||
STOPPED = "stopped"
|
||||
NOT_FOUND = "not_found"
|
||||
ERROR = "error"
|
||||
|
||||
@dataclass
|
||||
class ProcessInfo:
|
||||
pid: int
|
||||
name: str
|
||||
status: ProcessStatus
|
||||
memory_usage: float = 0.0
|
||||
cpu_usage: float = 0.0
|
||||
start_time: float = 0.0
|
||||
command_line: str = ""
|
||||
|
||||
@dataclass
|
||||
class SystemInfo:
|
||||
platform: str
|
||||
architecture: str
|
||||
python_version: str
|
||||
total_memory: float
|
||||
available_memory: float
|
||||
cpu_count: int
|
||||
disk_usage: Dict[str, float]
|
||||
|
||||
class EnhancedPathManager:
|
||||
|
||||
def __init__(self):
|
||||
self._path_cache = {}
|
||||
self._executable_cache = {}
|
||||
self._browser_cache = {}
|
||||
|
||||
def get_user_documents_path(self) -> str:
|
||||
"""Get user documents path with enhanced error handling"""
|
||||
cache_key = "documents_path"
|
||||
if cache_key in self._path_cache:
|
||||
return self._path_cache[cache_key]
|
||||
|
||||
try:
|
||||
if platform.system() == "Windows":
|
||||
path = self._get_windows_documents_path()
|
||||
elif platform.system() == "Darwin":
|
||||
path = self._get_macos_documents_path()
|
||||
else:
|
||||
path = self._get_linux_documents_path()
|
||||
|
||||
# Validate path exists
|
||||
if not os.path.exists(path):
|
||||
logger.warning(f"Documents path does not exist: {path}")
|
||||
path = os.path.expanduser("~/Documents")
|
||||
|
||||
self._path_cache[cache_key] = path
|
||||
return path
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get documents path: {e}")
|
||||
fallback = os.path.expanduser("~/Documents")
|
||||
self._path_cache[cache_key] = fallback
|
||||
return fallback
|
||||
|
||||
def _get_windows_documents_path(self) -> str:
|
||||
"""Get Windows documents path from registry"""
|
||||
try:
|
||||
import winreg
|
||||
with winreg.OpenKey(winreg.HKEY_CURRENT_USER,
|
||||
"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders") as key:
|
||||
documents_path, _ = winreg.QueryValueEx(key, "Personal")
|
||||
return documents_path
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to get Windows documents path from registry: {e}")
|
||||
return os.path.expanduser("~\\Documents")
|
||||
|
||||
def _get_macos_documents_path(self) -> str:
|
||||
"""Get macOS documents path"""
|
||||
return os.path.expanduser("~/Documents")
|
||||
|
||||
def _get_linux_documents_path(self) -> str:
|
||||
"""Get Linux documents path with XDG support"""
|
||||
try:
|
||||
xdg_config = os.path.expanduser("~/.config/user-dirs.dirs")
|
||||
if os.path.exists(xdg_config):
|
||||
with open(xdg_config, "r") as f:
|
||||
for line in f:
|
||||
if line.startswith("XDG_DOCUMENTS_DIR"):
|
||||
path = line.split("=")[1].strip().strip('"').replace("$HOME", os.path.expanduser("~"))
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to read XDG config: {e}")
|
||||
|
||||
return os.path.expanduser("~/Documents")
|
||||
|
||||
def find_executable(self, executable_names: List[str], validate: bool = True) -> Optional[str]:
|
||||
"""Find executable with enhanced validation"""
|
||||
cache_key = tuple(sorted(executable_names))
|
||||
if cache_key in self._executable_cache:
|
||||
return self._executable_cache[cache_key]
|
||||
|
||||
for name in executable_names:
|
||||
try:
|
||||
path = shutil.which(name)
|
||||
if path and (not validate or self._validate_executable(path)):
|
||||
self._executable_cache[cache_key] = path
|
||||
return path
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to find executable {name}: {e}")
|
||||
continue
|
||||
|
||||
self._executable_cache[cache_key] = None
|
||||
return None
|
||||
|
||||
def _validate_executable(self, path: str) -> bool:
|
||||
"""Validate executable file"""
|
||||
try:
|
||||
if not os.path.exists(path):
|
||||
return False
|
||||
|
||||
# Check if file is executable
|
||||
if platform.system() != "Windows":
|
||||
if not os.access(path, os.X_OK):
|
||||
return False
|
||||
|
||||
# Check file size (should not be 0)
|
||||
if os.path.getsize(path) == 0:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Executable validation failed for {path}: {e}")
|
||||
return False
|
||||
|
||||
class EnhancedBrowserManager:
|
||||
"""Enhanced browser management with automatic detection and validation"""
|
||||
|
||||
def __init__(self):
|
||||
self.path_manager = EnhancedPathManager()
|
||||
self._browser_paths = {}
|
||||
self._driver_paths = {}
|
||||
|
||||
def get_browser_path(self, browser_type: str) -> str:
|
||||
"""Get browser path with enhanced detection"""
|
||||
browser_type = browser_type.lower()
|
||||
|
||||
if browser_type in self._browser_paths:
|
||||
return self._browser_paths[browser_type]
|
||||
|
||||
try:
|
||||
if platform.system() == "Windows":
|
||||
path = self._get_windows_browser_path(browser_type)
|
||||
elif platform.system() == "Darwin":
|
||||
path = self._get_macos_browser_path(browser_type)
|
||||
else:
|
||||
path = self._get_linux_browser_path(browser_type)
|
||||
|
||||
self._browser_paths[browser_type] = path
|
||||
return path
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get browser path for {browser_type}: {e}")
|
||||
return ""
|
||||
|
||||
def get_driver_path(self, browser_type: str) -> str:
|
||||
"""Get driver path with enhanced detection"""
|
||||
browser_type = browser_type.lower()
|
||||
|
||||
if browser_type in self._driver_paths:
|
||||
return self._driver_paths[browser_type]
|
||||
|
||||
try:
|
||||
# Map browser types to driver types
|
||||
driver_map = {
|
||||
'chrome': 'chromedriver',
|
||||
'edge': 'msedgedriver',
|
||||
'firefox': 'geckodriver',
|
||||
'brave': 'chromedriver',
|
||||
'opera': 'chromedriver',
|
||||
'operagx': 'chromedriver'
|
||||
}
|
||||
|
||||
driver_name = driver_map.get(browser_type, 'chromedriver')
|
||||
path = self._find_driver_path(driver_name)
|
||||
|
||||
self._driver_paths[browser_type] = path
|
||||
return path
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get driver path for {browser_type}: {e}")
|
||||
return ""
|
||||
|
||||
def _get_windows_browser_path(self, browser_type: str) -> str:
|
||||
"""Get Windows browser path"""
|
||||
browser_paths = {
|
||||
'chrome': [
|
||||
shutil.which("chrome"),
|
||||
r"C:\Program Files\Google\Chrome\Application\chrome.exe",
|
||||
r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
|
||||
os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Google', 'Chrome', 'Application', 'chrome.exe')
|
||||
],
|
||||
'edge': [
|
||||
shutil.which("msedge"),
|
||||
r"C:\Program Files\Microsoft\Edge\Application\msedge.exe",
|
||||
r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
|
||||
],
|
||||
'firefox': [
|
||||
shutil.which("firefox"),
|
||||
r"C:\Program Files\Mozilla Firefox\firefox.exe",
|
||||
r"C:\Program Files (x86)\Mozilla Firefox\firefox.exe"
|
||||
],
|
||||
'opera': [
|
||||
shutil.which("opera"),
|
||||
r"C:\Program Files\Opera\opera.exe",
|
||||
r"C:\Program Files (x86)\Opera\opera.exe",
|
||||
os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Programs', 'Opera', 'launcher.exe')
|
||||
],
|
||||
'operagx': [
|
||||
os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Programs', 'Opera GX', 'launcher.exe'),
|
||||
r"C:\Program Files\Opera GX\opera.exe",
|
||||
r"C:\Program Files (x86)\Opera GX\opera.exe"
|
||||
],
|
||||
'brave': [
|
||||
shutil.which("brave"),
|
||||
os.path.join(os.environ.get('PROGRAMFILES', ''), 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'),
|
||||
os.path.join(os.environ.get('PROGRAMFILES(X86)', ''), 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe')
|
||||
]
|
||||
}
|
||||
|
||||
paths = browser_paths.get(browser_type, [])
|
||||
for path in paths:
|
||||
if path and os.path.exists(path):
|
||||
return path
|
||||
|
||||
return ""
|
||||
|
||||
def _get_macos_browser_path(self, browser_type: str) -> str:
|
||||
"""Get macOS browser path"""
|
||||
browser_paths = {
|
||||
'chrome': [
|
||||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
||||
"~/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
||||
],
|
||||
'edge': [
|
||||
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
||||
"~/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"
|
||||
],
|
||||
'firefox': [
|
||||
"/Applications/Firefox.app/Contents/MacOS/firefox",
|
||||
"~/Applications/Firefox.app/Contents/MacOS/firefox"
|
||||
],
|
||||
'opera': [
|
||||
"/Applications/Opera.app/Contents/MacOS/Opera",
|
||||
"~/Applications/Opera.app/Contents/MacOS/Opera"
|
||||
],
|
||||
'operagx': [
|
||||
"/Applications/Opera GX.app/Contents/MacOS/Opera GX",
|
||||
"~/Applications/Opera GX.app/Contents/MacOS/Opera GX"
|
||||
],
|
||||
'brave': [
|
||||
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
|
||||
"~/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
|
||||
]
|
||||
}
|
||||
|
||||
paths = browser_paths.get(browser_type, [])
|
||||
for path in paths:
|
||||
expanded_path = os.path.expanduser(path)
|
||||
if os.path.exists(expanded_path):
|
||||
return expanded_path
|
||||
|
||||
return ""
|
||||
|
||||
def _get_linux_browser_path(self, browser_type: str) -> str:
|
||||
"""Get Linux browser path"""
|
||||
browser_names = {
|
||||
'chrome': ['google-chrome', 'chrome', 'chromium-browser', 'chromium'],
|
||||
'edge': ['microsoft-edge', 'msedge', 'edge'],
|
||||
'firefox': ['firefox', 'firefox-esr'],
|
||||
'opera': ['opera', 'opera-stable'],
|
||||
'operagx': ['opera-gx'],
|
||||
'brave': ['brave-browser', 'brave']
|
||||
}
|
||||
|
||||
names = browser_names.get(browser_type, [browser_type])
|
||||
return self.path_manager.find_executable(names) or ""
|
||||
|
||||
def _find_driver_path(self, driver_name: str) -> str:
|
||||
"""Find driver executable path"""
|
||||
# Try to find in PATH first
|
||||
path = self.path_manager.find_executable([driver_name])
|
||||
if path:
|
||||
return path
|
||||
|
||||
# Try common installation paths
|
||||
if platform.system() == "Windows":
|
||||
driver_paths = [
|
||||
os.path.join(os.path.dirname(os.path.abspath(__file__)), "drivers", f"{driver_name}.exe"),
|
||||
os.path.join(os.environ.get('LOCALAPPDATA', ''), 'WebDriver', f"{driver_name}.exe"),
|
||||
f"C:\\Program Files\\{driver_name}\\{driver_name}.exe"
|
||||
]
|
||||
elif platform.system() == "Darwin":
|
||||
driver_paths = [
|
||||
os.path.join(os.path.dirname(os.path.abspath(__file__)), "drivers", driver_name),
|
||||
f"/usr/local/bin/{driver_name}",
|
||||
f"/opt/homebrew/bin/{driver_name}"
|
||||
]
|
||||
else:
|
||||
driver_paths = [
|
||||
os.path.join(os.path.dirname(os.path.abspath(__file__)), "drivers", driver_name),
|
||||
f"/usr/local/bin/{driver_name}",
|
||||
f"/usr/bin/{driver_name}"
|
||||
]
|
||||
|
||||
for driver_path in driver_paths:
|
||||
if os.path.exists(driver_path):
|
||||
return driver_path
|
||||
|
||||
return ""
|
||||
|
||||
class EnhancedProcessManager:
|
||||
"""Enhanced process management with monitoring and control"""
|
||||
|
||||
def __init__(self):
|
||||
self._process_cache = {}
|
||||
self._monitoring_threads = {}
|
||||
|
||||
def find_cursor_processes(self) -> List[ProcessInfo]:
|
||||
"""Find all Cursor processes with detailed information"""
|
||||
processes = []
|
||||
|
||||
try:
|
||||
for proc in psutil.process_iter(['pid', 'name', 'cmdline', 'memory_info', 'cpu_percent', 'create_time']):
|
||||
try:
|
||||
if self._is_cursor_process(proc.info['name']):
|
||||
process_info = ProcessInfo(
|
||||
pid=proc.info['pid'],
|
||||
name=proc.info['name'],
|
||||
status=ProcessStatus.RUNNING,
|
||||
memory_usage=proc.info['memory_info'].rss / 1024 / 1024, # MB
|
||||
cpu_usage=proc.info['cpu_percent'],
|
||||
start_time=proc.info['create_time'],
|
||||
command_line=' '.join(proc.info['cmdline']) if proc.info['cmdline'] else ''
|
||||
)
|
||||
processes.append(process_info)
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to find Cursor processes: {e}")
|
||||
|
||||
return processes
|
||||
|
||||
def _is_cursor_process(self, process_name: str) -> bool:
|
||||
"""Check if process is related to Cursor"""
|
||||
cursor_names = ['cursor', 'Cursor', 'CURSOR']
|
||||
return any(name in process_name for name in cursor_names)
|
||||
|
||||
def kill_cursor_processes(self, force: bool = False) -> bool:
|
||||
"""Kill all Cursor processes"""
|
||||
processes = self.find_cursor_processes()
|
||||
success = True
|
||||
|
||||
for process_info in processes:
|
||||
try:
|
||||
proc = psutil.Process(process_info.pid)
|
||||
if force:
|
||||
proc.kill()
|
||||
else:
|
||||
proc.terminate()
|
||||
|
||||
logger.info(f"Terminated Cursor process: {process_info.name} (PID: {process_info.pid})")
|
||||
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied) as e:
|
||||
logger.warning(f"Failed to terminate process {process_info.pid}: {e}")
|
||||
success = False
|
||||
|
||||
return success
|
||||
|
||||
def wait_for_process_termination(self, pids: List[int], timeout: int = 30) -> bool:
|
||||
"""Wait for processes to terminate"""
|
||||
start_time = time.time()
|
||||
|
||||
while time.time() - start_time < timeout:
|
||||
remaining_pids = []
|
||||
|
||||
for pid in pids:
|
||||
try:
|
||||
if psutil.pid_exists(pid):
|
||||
remaining_pids.append(pid)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if not remaining_pids:
|
||||
return True
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
logger.warning(f"Timeout waiting for process termination: {remaining_pids}")
|
||||
return False
|
||||
|
||||
def monitor_process(self, pid: int, callback: Callable[[ProcessInfo], None]) -> None:
|
||||
"""Monitor process and call callback with updates"""
|
||||
def monitor():
|
||||
try:
|
||||
proc = psutil.Process(pid)
|
||||
while proc.is_running():
|
||||
try:
|
||||
process_info = ProcessInfo(
|
||||
pid=proc.pid,
|
||||
name=proc.name(),
|
||||
status=ProcessStatus.RUNNING,
|
||||
memory_usage=proc.memory_info().rss / 1024 / 1024,
|
||||
cpu_usage=proc.cpu_percent(),
|
||||
start_time=proc.create_time(),
|
||||
command_line=' '.join(proc.cmdline()) if proc.cmdline() else ''
|
||||
)
|
||||
callback(process_info)
|
||||
time.sleep(1)
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Process monitoring failed for PID {pid}: {e}")
|
||||
|
||||
thread = threading.Thread(target=monitor, daemon=True)
|
||||
thread.start()
|
||||
self._monitoring_threads[pid] = thread
|
||||
|
||||
class EnhancedSystemManager:
|
||||
"""Enhanced system information and management"""
|
||||
|
||||
def __init__(self):
|
||||
self._system_info = None
|
||||
|
||||
def get_system_info(self) -> SystemInfo:
|
||||
"""Get comprehensive system information"""
|
||||
if self._system_info is None:
|
||||
try:
|
||||
cpu_count = psutil.cpu_count()
|
||||
if cpu_count is None:
|
||||
cpu_count = 1
|
||||
|
||||
self._system_info = SystemInfo(
|
||||
platform=platform.system(),
|
||||
architecture=platform.machine(),
|
||||
python_version=sys.version,
|
||||
total_memory=psutil.virtual_memory().total / 1024 / 1024 / 1024,
|
||||
available_memory=psutil.virtual_memory().available / 1024 / 1024 / 1024,
|
||||
cpu_count=cpu_count,
|
||||
disk_usage=self._get_disk_usage()
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get system info: {e}")
|
||||
# Return basic info
|
||||
self._system_info = SystemInfo(
|
||||
platform=platform.system(),
|
||||
architecture=platform.machine(),
|
||||
python_version=sys.version,
|
||||
total_memory=0.0,
|
||||
available_memory=0.0,
|
||||
cpu_count=1,
|
||||
disk_usage={}
|
||||
)
|
||||
|
||||
return self._system_info
|
||||
|
||||
def _get_disk_usage(self) -> Dict[str, float]:
|
||||
disk_usage = {}
|
||||
|
||||
try:
|
||||
for partition in psutil.disk_partitions():
|
||||
try:
|
||||
usage = psutil.disk_usage(partition.mountpoint)
|
||||
disk_usage[partition.mountpoint] = {
|
||||
'total': usage.total / 1024 / 1024 / 1024,
|
||||
'used': usage.used / 1024 / 1024 / 1024,
|
||||
'free': usage.free / 1024 / 1024 / 1024,
|
||||
'percent': usage.percent
|
||||
}
|
||||
except (OSError, PermissionError):
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to get disk usage: {e}")
|
||||
|
||||
return disk_usage
|
||||
|
||||
def check_system_requirements(self) -> Dict[str, bool]:
|
||||
requirements = {
|
||||
'python_version': sys.version_info >= (3, 8),
|
||||
'memory_available': self.get_system_info().available_memory >= 2.0,
|
||||
'disk_space': self._check_disk_space(),
|
||||
'permissions': self._check_permissions()
|
||||
}
|
||||
|
||||
return requirements
|
||||
|
||||
def _check_disk_space(self) -> bool:
|
||||
try:
|
||||
check_path = os.path.expanduser("~")
|
||||
usage = psutil.disk_usage(check_path)
|
||||
free_gb = usage.free / 1024 / 1024 / 1024
|
||||
return free_gb >= 1.0
|
||||
except Exception:
|
||||
return True
|
||||
|
||||
def _check_permissions(self) -> bool:
|
||||
try:
|
||||
test_file = os.path.join(os.path.expanduser("~"), ".cursor_free_vip_test")
|
||||
with open(test_file, 'w') as f:
|
||||
f.write("test")
|
||||
os.remove(test_file)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
class EnhancedNetworkManager:
|
||||
def __init__(self, timeout: int = 30, max_retries: int = 3):
|
||||
self.timeout = timeout
|
||||
self.max_retries = max_retries
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({
|
||||
'User-Agent': 'Cursor-Free-VIP/1.0'
|
||||
})
|
||||
|
||||
def make_request(self, url: str, method: str = 'GET', **kwargs) -> Optional[requests.Response]:
|
||||
for attempt in range(self.max_retries):
|
||||
try:
|
||||
response = self.session.request(
|
||||
method=method,
|
||||
url=url,
|
||||
timeout=self.timeout,
|
||||
**kwargs
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.warning(f"Request attempt {attempt + 1} failed: {e}")
|
||||
if attempt == self.max_retries - 1:
|
||||
logger.error(f"All request attempts failed for {url}")
|
||||
return None
|
||||
|
||||
time.sleep(2 ** attempt)
|
||||
|
||||
return None
|
||||
|
||||
def check_connectivity(self, urls: Optional[List[str]] = None) -> Dict[str, bool]:
|
||||
if urls is None:
|
||||
urls = [
|
||||
'https://www.google.com',
|
||||
'https://github.com',
|
||||
'https://cursor.sh'
|
||||
]
|
||||
|
||||
results = {}
|
||||
|
||||
with ThreadPoolExecutor(max_workers=5) as executor:
|
||||
future_to_url = {
|
||||
executor.submit(self._check_single_url, url): url
|
||||
for url in urls
|
||||
}
|
||||
|
||||
for future in as_completed(future_to_url):
|
||||
url = future_to_url[future]
|
||||
try:
|
||||
results[url] = future.result()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to check {url}: {e}")
|
||||
results[url] = False
|
||||
|
||||
return results
|
||||
|
||||
def _check_single_url(self, url: str) -> bool:
|
||||
try:
|
||||
response = self.make_request(url, timeout=10)
|
||||
return response is not None and response.status_code == 200
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
path_manager = EnhancedPathManager()
|
||||
browser_manager = EnhancedBrowserManager()
|
||||
process_manager = EnhancedProcessManager()
|
||||
system_manager = EnhancedSystemManager()
|
||||
network_manager = EnhancedNetworkManager()
|
||||
|
||||
def get_user_documents_path() -> str:
|
||||
return path_manager.get_user_documents_path()
|
||||
|
||||
def find_executable(executable_names: List[str]) -> Optional[str]:
|
||||
return path_manager.find_executable(executable_names)
|
||||
|
||||
def get_default_browser_path(browser_type: str = 'chrome') -> str:
|
||||
return browser_manager.get_browser_path(browser_type)
|
||||
|
||||
def get_default_driver_path(browser_type: str = 'chrome') -> str:
|
||||
return browser_manager.get_driver_path(browser_type)
|
||||
|
||||
def parse_time_range(time_str: str) -> Tuple[float, float]:
|
||||
try:
|
||||
if '-' in time_str:
|
||||
parts = time_str.split('-')
|
||||
if len(parts) == 2:
|
||||
return float(parts[0].strip()), float(parts[1].strip())
|
||||
else:
|
||||
value = float(time_str.strip())
|
||||
return value, value
|
||||
except (ValueError, TypeError) as e:
|
||||
logger.warning(f"Failed to parse time range '{time_str}': {e}")
|
||||
|
||||
return 0.5, 1.5
|
||||
|
||||
def get_random_wait_time(config: Dict, timing_key: str, default_range: Tuple[float, float] = (0.5, 1.5)) -> float:
|
||||
try:
|
||||
if timing_key in config:
|
||||
time_range = parse_time_range(config[timing_key])
|
||||
return random.uniform(time_range[0], time_range[1])
|
||||
else:
|
||||
return random.uniform(default_range[0], default_range[1])
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to get random wait time for {timing_key}: {e}")
|
||||
return random.uniform(default_range[0], default_range[1])
|
||||
|
||||
def get_linux_cursor_path() -> str:
|
||||
possible_paths = [
|
||||
"/opt/Cursor/resources/app",
|
||||
"/usr/share/cursor/resources/app",
|
||||
"/usr/local/share/cursor/resources/app",
|
||||
os.path.expanduser("~/.local/share/cursor/resources/app"),
|
||||
os.path.expanduser("~/snap/cursor/current/usr/share/cursor/resources/app")
|
||||
]
|
||||
|
||||
for path in possible_paths:
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
|
||||
cursor_executable = path_manager.find_executable(['cursor'])
|
||||
if cursor_executable:
|
||||
exec_dir = os.path.dirname(cursor_executable)
|
||||
possible_resource_paths = [
|
||||
os.path.join(exec_dir, "resources", "app"),
|
||||
os.path.join(os.path.dirname(exec_dir), "resources", "app"),
|
||||
os.path.join(exec_dir, "..", "resources", "app")
|
||||
]
|
||||
|
||||
for path in possible_resource_paths:
|
||||
if os.path.exists(path):
|
||||
return os.path.abspath(path)
|
||||
|
||||
return ""
|
||||
@@ -1,10 +1,23 @@
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
from colorama import Fore, Style
|
||||
import logging
|
||||
import os
|
||||
from typing import Optional, Dict, Any, Union
|
||||
from colorama import Fore, Style, init
|
||||
from config import get_config
|
||||
|
||||
# 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__)
|
||||
|
||||
# Define emoji constants
|
||||
EMOJI = {
|
||||
'START': '🚀',
|
||||
@@ -13,35 +26,85 @@ EMOJI = {
|
||||
'ERROR': '❌',
|
||||
'WAIT': '⏳',
|
||||
'INFO': 'ℹ️',
|
||||
'WARNING': '⚠️'
|
||||
'WARNING': '⚠️',
|
||||
'TOKEN': '🔖',
|
||||
'REFRESH': '🔄'
|
||||
}
|
||||
|
||||
def refresh_token(token, translator=None):
|
||||
"""Refresh the token using the Chinese server API
|
||||
def _get_message(translator: Any, key: str, fallback: str, **kwargs) -> str:
|
||||
"""Get translated message or fallback.
|
||||
|
||||
Args:
|
||||
token (str): The full WorkosCursorSessionToken cookie value
|
||||
translator: Translator object
|
||||
key: Translation key
|
||||
fallback: Fallback message if translation not available
|
||||
**kwargs: Format parameters for the message
|
||||
|
||||
Returns:
|
||||
str: Translated or fallback message
|
||||
"""
|
||||
if translator:
|
||||
return translator.get(key, **kwargs)
|
||||
return fallback.format(**kwargs) if kwargs else fallback
|
||||
|
||||
def refresh_token(token: str, translator: Any = None) -> str:
|
||||
"""Refresh the token using the refresh server API.
|
||||
|
||||
Args:
|
||||
token: The full WorkosCursorSessionToken cookie value
|
||||
translator: Optional translator object
|
||||
|
||||
Returns:
|
||||
str: The refreshed access token or original token if refresh fails
|
||||
"""
|
||||
try:
|
||||
logger.info("Attempting to refresh token")
|
||||
|
||||
# Validate input
|
||||
if not token or not isinstance(token, str):
|
||||
logger.error("Invalid token provided")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {_get_message(translator, 'token.invalid_token', 'Invalid token provided')}{Style.RESET_ALL}")
|
||||
return token
|
||||
|
||||
# Get configuration
|
||||
config = get_config(translator)
|
||||
|
||||
# Check if token refresh is enabled
|
||||
if config.has_option('Token', 'enable_refresh') and not config.getboolean('Token', 'enable_refresh'):
|
||||
logger.info("Token refresh is disabled in configuration")
|
||||
print(f"{Fore.YELLOW}{EMOJI['INFO']} {_get_message(translator, 'token.refresh_disabled', 'Token refresh is disabled in configuration')}{Style.RESET_ALL}")
|
||||
return _extract_token_part(token)
|
||||
|
||||
# Get refresh_server URL from config or use default
|
||||
refresh_server = config.get('Token', 'refresh_server', fallback='https://token.cursorpro.com.cn')
|
||||
logger.info(f"Using refresh server: {refresh_server}")
|
||||
|
||||
# Ensure the token is URL encoded properly
|
||||
if '%3A%3A' not in token and '::' in token:
|
||||
# Replace :: with URL encoded version if needed
|
||||
token = token.replace('::', '%3A%3A')
|
||||
encoded_token = _ensure_token_encoded(token)
|
||||
|
||||
# Make the request to the refresh server
|
||||
url = f"{refresh_server}/reftoken?token={token}"
|
||||
url = f"{refresh_server}/reftoken?token={encoded_token}"
|
||||
|
||||
print(f"{Fore.CYAN}{EMOJI['INFO']} {translator.get('token.refreshing') if translator else 'Refreshing token...'}{Style.RESET_ALL}")
|
||||
print(f"{Fore.CYAN}{EMOJI['REFRESH']} {_get_message(translator, 'token.refreshing', 'Refreshing token...')}{Style.RESET_ALL}")
|
||||
|
||||
response = requests.get(url, timeout=30)
|
||||
# Set timeout and headers
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
|
||||
# Make request with retry logic
|
||||
max_retries = 3
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
response = requests.get(url, headers=headers, timeout=30)
|
||||
break
|
||||
except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
|
||||
if attempt < max_retries - 1:
|
||||
logger.warning(f"Request attempt {attempt + 1} failed: {e}. Retrying...")
|
||||
time.sleep(2)
|
||||
else:
|
||||
raise
|
||||
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
@@ -53,60 +116,140 @@ def refresh_token(token, translator=None):
|
||||
expire_time = data.get('data', {}).get('expire_time', 'Unknown')
|
||||
|
||||
if access_token:
|
||||
print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {translator.get('token.refresh_success', days=days_left, expire=expire_time) if translator else f'Token refreshed successfully! Valid for {days_left} days (expires: {expire_time})'}{Style.RESET_ALL}")
|
||||
logger.info(f"Token refreshed successfully. Valid for {days_left} days")
|
||||
print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {_get_message(translator, 'token.refresh_success', 'Token refreshed successfully! Valid for {days} days (expires: {expire})', days=days_left, expire=expire_time)}{Style.RESET_ALL}")
|
||||
return access_token
|
||||
else:
|
||||
print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('token.no_access_token') if translator else 'No access token in response'}{Style.RESET_ALL}")
|
||||
logger.warning("No access token in response")
|
||||
print(f"{Fore.YELLOW}{EMOJI['WARNING']} {_get_message(translator, 'token.no_access_token', 'No access token in response')}{Style.RESET_ALL}")
|
||||
else:
|
||||
error_msg = data.get('msg', 'Unknown error')
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('token.refresh_failed', error=error_msg) if translator else f'Token refresh failed: {error_msg}'}{Style.RESET_ALL}")
|
||||
logger.error(f"Token refresh failed: {error_msg}")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {_get_message(translator, 'token.refresh_failed', 'Token refresh failed: {error}', error=error_msg)}{Style.RESET_ALL}")
|
||||
except json.JSONDecodeError:
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('token.invalid_response') if translator else 'Invalid JSON response from refresh server'}{Style.RESET_ALL}")
|
||||
logger.error("Invalid JSON response from refresh server")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {_get_message(translator, 'token.invalid_response', 'Invalid JSON response from refresh server')}{Style.RESET_ALL}")
|
||||
else:
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('token.server_error', status=response.status_code) if translator else f'Refresh server error: HTTP {response.status_code}'}{Style.RESET_ALL}")
|
||||
logger.error(f"Refresh server error: HTTP {response.status_code}")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {_get_message(translator, 'token.server_error', 'Refresh server error: HTTP {status}', status=response.status_code)}{Style.RESET_ALL}")
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('token.request_timeout') if translator else 'Request to refresh server timed out'}{Style.RESET_ALL}")
|
||||
logger.error("Request to refresh server timed out")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {_get_message(translator, 'token.request_timeout', 'Request to refresh server timed out')}{Style.RESET_ALL}")
|
||||
except requests.exceptions.ConnectionError:
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('token.connection_error') if translator else 'Connection error to refresh server'}{Style.RESET_ALL}")
|
||||
logger.error("Connection error to refresh server")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {_get_message(translator, 'token.connection_error', 'Connection error to refresh server')}{Style.RESET_ALL}")
|
||||
except Exception as e:
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('token.unexpected_error', error=str(e)) if translator else f'Unexpected error during token refresh: {str(e)}'}{Style.RESET_ALL}")
|
||||
logger.error(f"Unexpected error during token refresh: {e}")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {_get_message(translator, 'token.unexpected_error', 'Unexpected error during token refresh: {error}', error=str(e))}{Style.RESET_ALL}")
|
||||
|
||||
# Return original token if refresh fails
|
||||
return token.split('%3A%3A')[-1] if '%3A%3A' in token else token.split('::')[-1] if '::' in token else token
|
||||
# Return extracted token part if refresh fails
|
||||
return _extract_token_part(token)
|
||||
|
||||
def get_token_from_cookie(cookie_value, translator=None):
|
||||
"""Extract and process token from cookie value
|
||||
def _ensure_token_encoded(token: str) -> str:
|
||||
"""Ensure the token is properly URL encoded.
|
||||
|
||||
Args:
|
||||
cookie_value (str): The WorkosCursorSessionToken cookie value
|
||||
token: The token to encode
|
||||
|
||||
Returns:
|
||||
str: The properly encoded token
|
||||
"""
|
||||
if '%3A%3A' not in token and '::' in token:
|
||||
# Replace :: with URL encoded version if needed
|
||||
return token.replace('::', '%3A%3A')
|
||||
return token
|
||||
|
||||
def _extract_token_part(token: str) -> str:
|
||||
"""Extract the token part from the cookie value.
|
||||
|
||||
Args:
|
||||
token: The full cookie value
|
||||
|
||||
Returns:
|
||||
str: The extracted token part
|
||||
"""
|
||||
if '%3A%3A' in token:
|
||||
return token.split('%3A%3A')[-1]
|
||||
elif '::' in token:
|
||||
return token.split('::')[-1]
|
||||
else:
|
||||
return token
|
||||
|
||||
def get_token_from_cookie(cookie_value: str, translator: Any = None) -> str:
|
||||
"""Extract and process token from cookie value.
|
||||
|
||||
Args:
|
||||
cookie_value: The WorkosCursorSessionToken cookie value
|
||||
translator: Optional translator object
|
||||
|
||||
Returns:
|
||||
str: The processed token
|
||||
"""
|
||||
try:
|
||||
logger.info("Processing token from cookie")
|
||||
|
||||
# Validate input
|
||||
if not cookie_value or not isinstance(cookie_value, str):
|
||||
logger.error("Invalid cookie value provided")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {_get_message(translator, 'token.invalid_cookie', 'Invalid cookie value provided')}{Style.RESET_ALL}")
|
||||
return ""
|
||||
|
||||
# Try to refresh the token with the API first
|
||||
print(f"{Fore.CYAN}{EMOJI['TOKEN']} {_get_message(translator, 'token.processing', 'Processing token...')}{Style.RESET_ALL}")
|
||||
refreshed_token = refresh_token(cookie_value, translator)
|
||||
|
||||
# If refresh succeeded and returned a different token, use it
|
||||
if refreshed_token and refreshed_token != cookie_value:
|
||||
original_token_part = _extract_token_part(cookie_value)
|
||||
if refreshed_token and refreshed_token != original_token_part and refreshed_token != cookie_value:
|
||||
logger.info("Using refreshed token")
|
||||
return refreshed_token
|
||||
|
||||
# If refresh failed or returned same token, use traditional extraction method
|
||||
if '%3A%3A' in cookie_value:
|
||||
return cookie_value.split('%3A%3A')[-1]
|
||||
elif '::' in cookie_value:
|
||||
return cookie_value.split('::')[-1]
|
||||
else:
|
||||
return cookie_value
|
||||
logger.info("Using extracted token part")
|
||||
return original_token_part
|
||||
|
||||
except Exception as e:
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('token.extraction_error', error=str(e)) if translator else f'Error extracting token: {str(e)}'}{Style.RESET_ALL}")
|
||||
logger.error(f"Error extracting token: {e}")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {_get_message(translator, 'token.extraction_error', 'Error extracting token: {error}', error=str(e))}{Style.RESET_ALL}")
|
||||
# Fall back to original behavior
|
||||
if '%3A%3A' in cookie_value:
|
||||
return cookie_value.split('%3A%3A')[-1]
|
||||
elif '::' in cookie_value:
|
||||
return cookie_value.split('::')[-1]
|
||||
return _extract_token_part(cookie_value)
|
||||
|
||||
def validate_token(token: str, translator: Any = None) -> bool:
|
||||
"""Validate if the token looks legitimate.
|
||||
|
||||
Args:
|
||||
token: The token to validate
|
||||
translator: Optional translator object
|
||||
|
||||
Returns:
|
||||
bool: True if token looks valid, False otherwise
|
||||
"""
|
||||
if not token:
|
||||
logger.warning("Empty token provided")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {_get_message(translator, 'token.empty_token', 'Empty token provided')}{Style.RESET_ALL}")
|
||||
return False
|
||||
|
||||
# Basic validation - JWT tokens typically start with "eyJ"
|
||||
if token.startswith('eyJ') and len(token) > 100 and '.' in token:
|
||||
logger.info("Token appears to be in valid JWT format")
|
||||
return True
|
||||
|
||||
logger.warning("Token does not appear to be in valid JWT format")
|
||||
print(f"{Fore.YELLOW}{EMOJI['WARNING']} {_get_message(translator, 'token.invalid_format', 'Token does not appear to be in valid JWT format')}{Style.RESET_ALL}")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test functionality if run directly
|
||||
try:
|
||||
test_token = input(f"{Fore.CYAN}{EMOJI['TOKEN']} Enter a token to test: {Style.RESET_ALL}")
|
||||
processed_token = get_token_from_cookie(test_token)
|
||||
print(f"\n{Fore.GREEN}{EMOJI['INFO']} Processed token: {processed_token}{Style.RESET_ALL}")
|
||||
|
||||
if validate_token(processed_token):
|
||||
print(f"{Fore.GREEN}{EMOJI['SUCCESS']} Token appears to be valid{Style.RESET_ALL}")
|
||||
else:
|
||||
return cookie_value
|
||||
print(f"{Fore.YELLOW}{EMOJI['WARNING']} Token may not be valid{Style.RESET_ALL}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error in test: {e}")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} Test error: {str(e)}{Style.RESET_ALL}")
|
||||
174
logo.py
174
logo.py
@@ -1,101 +1,103 @@
|
||||
import sys
|
||||
import platform
|
||||
import logging
|
||||
from colorama import Fore, Style, init
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
import shutil
|
||||
import re
|
||||
|
||||
# Get the current script directory
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
# Build the full path to the .env file
|
||||
env_path = os.path.join(current_dir, '.env')
|
||||
|
||||
# Load environment variables, specifying the .env file path
|
||||
load_dotenv(env_path)
|
||||
# Get the version number, using the default value if not found
|
||||
version = os.getenv('VERSION', '1.0.0')
|
||||
from typing import Optional, Dict, List, Any, Tuple, Union
|
||||
|
||||
# Initialize colorama
|
||||
init()
|
||||
init(autoreset=True)
|
||||
|
||||
# get terminal width
|
||||
def get_terminal_width():
|
||||
try:
|
||||
columns, _ = shutil.get_terminal_size()/2
|
||||
return columns
|
||||
except:
|
||||
return 80 # default width
|
||||
# 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__)
|
||||
|
||||
# center display text (not handling Chinese characters)
|
||||
def center_multiline_text(text, handle_chinese=False):
|
||||
width = get_terminal_width()
|
||||
lines = text.split('\n')
|
||||
centered_lines = []
|
||||
|
||||
for line in lines:
|
||||
# calculate actual display width (remove ANSI color codes)
|
||||
clean_line = line
|
||||
for color in [Fore.CYAN, Fore.YELLOW, Fore.GREEN, Fore.RED, Fore.BLUE, Style.RESET_ALL]:
|
||||
clean_line = clean_line.replace(color, '')
|
||||
|
||||
# remove all ANSI escape sequences to get the actual length
|
||||
ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
|
||||
clean_line = ansi_escape.sub('', clean_line)
|
||||
|
||||
# calculate display width
|
||||
if handle_chinese:
|
||||
# consider Chinese characters occupying two positions
|
||||
display_width = 0
|
||||
for char in clean_line:
|
||||
if ord(char) > 127: # non-ASCII characters
|
||||
display_width += 2
|
||||
else:
|
||||
display_width += 1
|
||||
else:
|
||||
# not handling Chinese characters
|
||||
display_width = len(clean_line)
|
||||
|
||||
# calculate the number of spaces to add
|
||||
padding = max(0, (width - display_width) // 2)
|
||||
centered_lines.append(' ' * padding + line)
|
||||
|
||||
return '\n'.join(centered_lines)
|
||||
# Current version
|
||||
version = "1.9.9"
|
||||
|
||||
# original LOGO text
|
||||
LOGO_TEXT = f"""{Fore.CYAN}
|
||||
██████╗██╗ ██╗██████╗ ███████╗ ██████╗ ██████╗ ██████╗ ██████╗ ██████╗
|
||||
██╔════╝██║ ██║██╔══██╗██╔════╝██╔═══██╗██╔══██╗ ██╔══██╗██╔══██╗██╔═══██╗
|
||||
██║ ██║ ██║██████╔╝███████╗██║ ██║██████╔╝ ██████╔╝██████╔╝██║ ██║
|
||||
██║ ██║ ██║██╔══██╗╚════██║██║ ██║██╔══██╗ ██╔═══╝ ██╔══██╗██║ ██║
|
||||
╚██████╗╚██████╔╝██║ ██║███████║╚██████╔╝██║ ██║ ██║ ██║ ██║╚██████╔╝
|
||||
╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝
|
||||
# ASCII art logo
|
||||
LOGO = f"""
|
||||
{Fore.CYAN}
|
||||
██████╗██╗ ██╗██████╗ ███████╗ ██████╗ ██████╗ ███████╗██████╗ ███████╗███████╗ ██╗ ██╗██╗██████╗
|
||||
██╔════╝██║ ██║██╔══██╗██╔════╝██╔═══██╗██╔══██╗ ██╔════╝██╔══██╗██╔════╝██╔════╝ ██║ ██║██║██╔══██╗
|
||||
██║ ██║ ██║██████╔╝███████╗██║ ██║██████╔╝ █████╗ ██████╔╝█████╗ █████╗ ██║ ██║██║██████╔╝
|
||||
██║ ██║ ██║██╔══██╗╚════██║██║ ██║██╔══██╗ ██╔══╝ ██╔══██╗██╔══╝ ██╔══╝ ╚██╗ ██╔╝██║██╔═══╝
|
||||
╚██████╗╚██████╔╝██║ ██║███████║╚██████╔╝██║ ██║ ██║ ██║ ██║███████╗███████╗ ╚████╔╝ ██║██║
|
||||
╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝ ╚═══╝ ╚═╝╚═╝
|
||||
{Style.RESET_ALL}"""
|
||||
|
||||
DESCRIPTION_TEXT = f"""{Fore.YELLOW}
|
||||
Pro Version Activator v{version}{Fore.GREEN}
|
||||
Author: Pin Studios (yeongpin)"""
|
||||
# Simplified logo for terminals with limited width
|
||||
SIMPLIFIED_LOGO = f"""
|
||||
{Fore.CYAN}
|
||||
██████╗██╗ ██╗██████╗ ███████╗ ██████╗ ██████╗
|
||||
██╔════╝██║ ██║██╔══██╗██╔════╝██╔═══██╗██╔══██╗
|
||||
██║ ██║ ██║██████╔╝███████╗██║ ██║██████╔╝
|
||||
██║ ██║ ██║██╔══██╗╚════██║██║ ██║██╔══██╗
|
||||
╚██████╗╚██████╔╝██║ ██║███████║╚██████╔╝██║ ██║
|
||||
╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝
|
||||
{Fore.GREEN}FREE VIP {version}{Style.RESET_ALL}
|
||||
{Style.RESET_ALL}"""
|
||||
|
||||
CONTRIBUTORS_TEXT = f"""{Fore.BLUE}
|
||||
Contributors:
|
||||
BasaiCorp aliensb handwerk2016 Nigel1992
|
||||
UntaDotMy RenjiYuusei imbajin ahmed98Osama
|
||||
bingoohuang mALIk-sHAHId MFaiqKhan httpmerak
|
||||
muhammedfurkan plamkatawe Lucaszmv
|
||||
# Contributors info
|
||||
CURSOR_CONTRIBUTORS = f"""
|
||||
{Fore.CYAN}╔══════════════════════════════════════════════════════════════════╗
|
||||
║ {Fore.YELLOW}CURSOR FREE VIP{Fore.CYAN} ║
|
||||
╠══════════════════════════════════════════════════════════════════╣
|
||||
║ {Fore.GREEN}Author:{Fore.WHITE} yeongpin {Fore.CYAN}║
|
||||
║ {Fore.GREEN}GitHub:{Fore.WHITE} https://github.com/yeongpin/cursor-free-vip {Fore.CYAN}║
|
||||
║ {Fore.GREEN}Version:{Fore.WHITE} {version} {Fore.CYAN}║
|
||||
╚══════════════════════════════════════════════════════════════════╝{Style.RESET_ALL}
|
||||
"""
|
||||
OTHER_INFO_TEXT = f"""{Fore.YELLOW}
|
||||
Github: https://github.com/yeongpin/cursor-free-vip{Fore.RED}
|
||||
Press 4 to change language | 按下 4 键切换语言{Style.RESET_ALL}"""
|
||||
|
||||
# center display LOGO and DESCRIPTION
|
||||
CURSOR_LOGO = center_multiline_text(LOGO_TEXT, handle_chinese=False)
|
||||
CURSOR_DESCRIPTION = center_multiline_text(DESCRIPTION_TEXT, handle_chinese=False)
|
||||
CURSOR_CONTRIBUTORS = center_multiline_text(CONTRIBUTORS_TEXT, handle_chinese=False)
|
||||
CURSOR_OTHER_INFO = center_multiline_text(OTHER_INFO_TEXT, handle_chinese=True)
|
||||
def get_terminal_width() -> int:
|
||||
"""Get terminal width with fallback for different platforms.
|
||||
|
||||
Returns:
|
||||
int: Terminal width in characters
|
||||
"""
|
||||
try:
|
||||
# Try to get terminal size using different methods based on platform
|
||||
if platform.system() == "Windows":
|
||||
from shutil import get_terminal_size
|
||||
columns = get_terminal_size().columns
|
||||
else:
|
||||
import os
|
||||
columns = os.get_terminal_size().columns
|
||||
|
||||
return columns
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to get terminal width: {e}")
|
||||
# Default width if detection fails
|
||||
return 80
|
||||
|
||||
def print_logo():
|
||||
print(CURSOR_LOGO)
|
||||
print(CURSOR_DESCRIPTION)
|
||||
# print(CURSOR_CONTRIBUTORS)
|
||||
print(CURSOR_OTHER_INFO)
|
||||
def print_logo() -> None:
|
||||
"""Print logo with version information based on terminal width."""
|
||||
try:
|
||||
# Get terminal width
|
||||
terminal_width = get_terminal_width()
|
||||
|
||||
# Choose logo based on terminal width
|
||||
if terminal_width < 100:
|
||||
logo = SIMPLIFIED_LOGO
|
||||
else:
|
||||
logo = LOGO
|
||||
|
||||
# Print logo
|
||||
print(logo)
|
||||
|
||||
# Print version info
|
||||
print(f"{Fore.GREEN}Version: {version}{Style.RESET_ALL}")
|
||||
print(f"{Fore.CYAN}{'═' * min(80, terminal_width)}{Style.RESET_ALL}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error printing logo: {e}")
|
||||
# Fallback to simplified version if any error occurs
|
||||
print(SIMPLIFIED_LOGO)
|
||||
print(f"{Fore.GREEN}Version: {version}{Style.RESET_ALL}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print_logo()
|
||||
print(CURSOR_CONTRIBUTORS)
|
||||
|
||||
351
main.py
351
main.py
@@ -2,34 +2,49 @@
|
||||
# This script allows the user to choose which script to run.
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
from logo import print_logo, version
|
||||
from colorama import Fore, Style, init
|
||||
import locale
|
||||
import platform
|
||||
import requests
|
||||
import subprocess
|
||||
import logging
|
||||
from colorama import Fore, Style, init
|
||||
from typing import Optional, Dict, List, Any, Tuple, Union
|
||||
from pathlib import Path
|
||||
|
||||
# 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 local modules
|
||||
from logo import print_logo, version
|
||||
from config import get_config, force_update_config
|
||||
import shutil
|
||||
import re
|
||||
from utils import get_user_documents_path
|
||||
from utils import get_user_documents_path
|
||||
|
||||
# Add these imports for Arabic support
|
||||
try:
|
||||
import arabic_reshaper
|
||||
from bidi.algorithm import get_display
|
||||
except ImportError:
|
||||
logger.warning("Arabic support modules not found")
|
||||
arabic_reshaper = None
|
||||
get_display = None
|
||||
|
||||
# Only import windll on Windows systems
|
||||
if platform.system() == 'Windows':
|
||||
import ctypes
|
||||
# Only import windll on Windows systems
|
||||
from ctypes import windll
|
||||
|
||||
# Initialize colorama
|
||||
init()
|
||||
try:
|
||||
import ctypes
|
||||
from ctypes import windll
|
||||
except ImportError:
|
||||
logger.warning("Windows-specific modules not found")
|
||||
ctypes = None
|
||||
windll = None
|
||||
|
||||
# Define emoji and color constants
|
||||
EMOJI = {
|
||||
@@ -53,36 +68,38 @@ EMOJI = {
|
||||
}
|
||||
|
||||
# Function to check if running as frozen executable
|
||||
def is_frozen():
|
||||
def is_frozen() -> bool:
|
||||
"""Check if the script is running as a frozen executable."""
|
||||
return getattr(sys, 'frozen', False)
|
||||
|
||||
# Function to check admin privileges (Windows only)
|
||||
def is_admin():
|
||||
def is_admin() -> bool:
|
||||
"""Check if the script is running with admin privileges (Windows only)."""
|
||||
if platform.system() == 'Windows':
|
||||
if platform.system() == 'Windows' and ctypes and windll:
|
||||
try:
|
||||
return ctypes.windll.shell32.IsUserAnAdmin() != 0
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking admin privileges: {e}")
|
||||
return False
|
||||
# Always return True for non-Windows to avoid changing behavior
|
||||
return True
|
||||
|
||||
# Function to restart with admin privileges
|
||||
def run_as_admin():
|
||||
def run_as_admin() -> bool:
|
||||
"""Restart the current script with admin privileges (Windows only)."""
|
||||
if platform.system() != 'Windows':
|
||||
if platform.system() != 'Windows' or not ctypes or not windll:
|
||||
return False
|
||||
|
||||
try:
|
||||
args = [sys.executable] + sys.argv
|
||||
|
||||
# Request elevation via ShellExecute
|
||||
print(f"{Fore.YELLOW}{EMOJI['ADMIN']} Requesting administrator privileges...{Style.RESET_ALL}")
|
||||
print(f"{Fore.YELLOW}{EMOJI['ADMIN']} {translator.get('admin.requesting_privileges') if translator else 'Requesting administrator privileges...'}{Style.RESET_ALL}")
|
||||
ctypes.windll.shell32.ShellExecuteW(None, "runas", args[0], " ".join('"' + arg + '"' for arg in args[1:]), None, 1)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} Failed to restart with admin privileges: {e}{Style.RESET_ALL}")
|
||||
logger.error(f"Failed to restart with admin privileges: {e}")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('admin.restart_failed', error=str(e)) if translator else f'Failed to restart with admin privileges: {e}'}{Style.RESET_ALL}")
|
||||
return False
|
||||
|
||||
class Translator:
|
||||
@@ -687,119 +704,187 @@ def check_latest_version():
|
||||
print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('updater.continue_anyway')}{Style.RESET_ALL}")
|
||||
return
|
||||
|
||||
def main():
|
||||
# Check for admin privileges if running as executable on Windows only
|
||||
if platform.system() == 'Windows' and is_frozen() and not is_admin():
|
||||
print(f"{Fore.YELLOW}{EMOJI['ADMIN']} {translator.get('menu.admin_required')}{Style.RESET_ALL}")
|
||||
if run_as_admin():
|
||||
sys.exit(0) # Exit after requesting admin privileges
|
||||
else:
|
||||
print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('menu.admin_required_continue')}{Style.RESET_ALL}")
|
||||
|
||||
print_logo()
|
||||
|
||||
# Initialize configuration
|
||||
config = get_config(translator)
|
||||
if not config:
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('menu.config_init_failed')}{Style.RESET_ALL}")
|
||||
return
|
||||
force_update_config(translator)
|
||||
|
||||
if config.getboolean('Utils', 'enabled_update_check'):
|
||||
check_latest_version() # Add version check before showing menu
|
||||
print_menu()
|
||||
|
||||
while True:
|
||||
try:
|
||||
choice_num = 17
|
||||
choice = input(f"\n{EMOJI['ARROW']} {Fore.CYAN}{translator.get('menu.input_choice', choices=f'0-{choice_num}')}: {Style.RESET_ALL}")
|
||||
|
||||
match choice:
|
||||
case "0":
|
||||
print(f"\n{Fore.YELLOW}{EMOJI['INFO']} {translator.get('menu.exit')}...{Style.RESET_ALL}")
|
||||
print(f"{Fore.CYAN}{'═' * 50}{Style.RESET_ALL}")
|
||||
return
|
||||
case "1":
|
||||
import reset_machine_manual
|
||||
reset_machine_manual.run(translator)
|
||||
print_menu()
|
||||
case "2":
|
||||
import cursor_register_manual
|
||||
cursor_register_manual.main(translator)
|
||||
print_menu()
|
||||
case "3":
|
||||
import quit_cursor
|
||||
quit_cursor.quit_cursor(translator)
|
||||
print_menu()
|
||||
case "4":
|
||||
if select_language():
|
||||
print_menu()
|
||||
continue
|
||||
case "5":
|
||||
from oauth_auth import main as oauth_main
|
||||
oauth_main('google',translator)
|
||||
print_menu()
|
||||
case "6":
|
||||
from oauth_auth import main as oauth_main
|
||||
oauth_main('github',translator)
|
||||
print_menu()
|
||||
case "7":
|
||||
import disable_auto_update
|
||||
disable_auto_update.run(translator)
|
||||
print_menu()
|
||||
case "8":
|
||||
import totally_reset_cursor
|
||||
totally_reset_cursor.run(translator)
|
||||
# print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('menu.fixed_soon')}{Style.RESET_ALL}")
|
||||
print_menu()
|
||||
case "9":
|
||||
import logo
|
||||
print(logo.CURSOR_CONTRIBUTORS)
|
||||
print_menu()
|
||||
case "10":
|
||||
from config import print_config
|
||||
print_config(get_config(), translator)
|
||||
print_menu()
|
||||
case "11":
|
||||
import bypass_version
|
||||
bypass_version.main(translator)
|
||||
print_menu()
|
||||
case "12":
|
||||
import check_user_authorized
|
||||
check_user_authorized.main(translator)
|
||||
print_menu()
|
||||
case "13":
|
||||
import bypass_token_limit
|
||||
bypass_token_limit.run(translator)
|
||||
print_menu()
|
||||
case "14":
|
||||
import restore_machine_id
|
||||
restore_machine_id.run(translator)
|
||||
print_menu()
|
||||
case "15":
|
||||
import delete_cursor_google
|
||||
delete_cursor_google.main(translator)
|
||||
print_menu()
|
||||
case "16":
|
||||
from oauth_auth import OAuthHandler
|
||||
oauth = OAuthHandler(translator)
|
||||
oauth._select_profile()
|
||||
print_menu()
|
||||
case "17":
|
||||
import manual_custom_auth
|
||||
manual_custom_auth.main(translator)
|
||||
print_menu()
|
||||
case _:
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('menu.invalid_choice')}{Style.RESET_ALL}")
|
||||
print_menu()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print(f"\n{Fore.YELLOW}{EMOJI['INFO']} {translator.get('menu.program_terminated')}{Style.RESET_ALL}")
|
||||
print(f"{Fore.CYAN}{'═' * 50}{Style.RESET_ALL}")
|
||||
def main() -> None:
|
||||
"""Main entry point for the application."""
|
||||
try:
|
||||
# Check for admin privileges if running as executable on Windows only
|
||||
if platform.system() == 'Windows' and is_frozen() and not is_admin():
|
||||
logger.warning("Running without admin privileges on Windows")
|
||||
print(f"{Fore.YELLOW}{EMOJI['ADMIN']} {translator.get('menu.admin_required') if translator else 'Administrator privileges are required for some features'}{Style.RESET_ALL}")
|
||||
if run_as_admin():
|
||||
sys.exit(0) # Exit after requesting admin privileges
|
||||
else:
|
||||
print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('menu.admin_required_continue') if translator else 'Continuing without administrator privileges. Some features may not work correctly.'}{Style.RESET_ALL}")
|
||||
|
||||
# Display logo
|
||||
print_logo()
|
||||
|
||||
# Initialize configuration
|
||||
logger.info("Initializing configuration")
|
||||
config = get_config(translator)
|
||||
if not config:
|
||||
logger.error("Failed to initialize configuration")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('menu.config_init_failed') if translator else 'Failed to initialize configuration'}{Style.RESET_ALL}")
|
||||
return
|
||||
except Exception as e:
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('menu.error_occurred', error=str(e))}{Style.RESET_ALL}")
|
||||
print_menu()
|
||||
|
||||
# Update configuration
|
||||
force_update_config(translator)
|
||||
|
||||
# Check for updates
|
||||
if config.has_option('Utils', 'enabled_update_check') and config.getboolean('Utils', 'enabled_update_check'):
|
||||
logger.info("Checking for updates")
|
||||
check_latest_version()
|
||||
|
||||
# Display menu
|
||||
print_menu()
|
||||
|
||||
# Main menu loop
|
||||
while True:
|
||||
try:
|
||||
choice_num = 17
|
||||
choice = input(f"\n{EMOJI['ARROW']} {Fore.CYAN}{translator.get('menu.input_choice', choices=f'0-{choice_num}') if translator else f'Enter your choice (0-{choice_num})'}: {Style.RESET_ALL}")
|
||||
|
||||
# Process menu choice
|
||||
process_menu_choice(choice)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Program terminated by user (KeyboardInterrupt)")
|
||||
print(f"\n{Fore.YELLOW}{EMOJI['INFO']} {translator.get('menu.program_terminated') if translator else 'Program terminated by user'}{Style.RESET_ALL}")
|
||||
print(f"{Fore.CYAN}{'═' * 50}{Style.RESET_ALL}")
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error in main loop: {e}")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('menu.unexpected_error', error=str(e)) if translator else f'An unexpected error occurred: {e}'}{Style.RESET_ALL}")
|
||||
print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('menu.please_try_again') if translator else 'Please try again'}{Style.RESET_ALL}")
|
||||
|
||||
except Exception as e:
|
||||
logger.critical(f"Critical error in main function: {e}")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('menu.critical_error', error=str(e)) if translator else f'A critical error occurred: {e}'}{Style.RESET_ALL}")
|
||||
print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('menu.please_restart') if translator else 'Please restart the application'}{Style.RESET_ALL}")
|
||||
return
|
||||
|
||||
def process_menu_choice(choice: str) -> None:
|
||||
"""Process menu choice and execute corresponding action.
|
||||
|
||||
Args:
|
||||
choice: User's menu choice
|
||||
"""
|
||||
try:
|
||||
match choice:
|
||||
case "0":
|
||||
logger.info("User selected to exit")
|
||||
print(f"\n{Fore.YELLOW}{EMOJI['INFO']} {translator.get('menu.exit') if translator else 'Exiting'}...{Style.RESET_ALL}")
|
||||
print(f"{Fore.CYAN}{'═' * 50}{Style.RESET_ALL}")
|
||||
sys.exit(0)
|
||||
case "1":
|
||||
logger.info("User selected reset machine manual")
|
||||
import reset_machine_manual
|
||||
reset_machine_manual.run(translator)
|
||||
print_menu()
|
||||
case "2":
|
||||
logger.info("User selected cursor register manual")
|
||||
import cursor_register_manual
|
||||
cursor_register_manual.main(translator)
|
||||
print_menu()
|
||||
case "3":
|
||||
logger.info("User selected quit cursor")
|
||||
import quit_cursor
|
||||
quit_cursor.quit_cursor(translator)
|
||||
print_menu()
|
||||
case "4":
|
||||
logger.info("User selected language settings")
|
||||
if select_language():
|
||||
print_menu()
|
||||
return
|
||||
case "5":
|
||||
logger.info("User selected Google OAuth")
|
||||
from oauth_auth import main as oauth_main
|
||||
oauth_main('google', translator)
|
||||
print_menu()
|
||||
case "6":
|
||||
logger.info("User selected GitHub OAuth")
|
||||
from oauth_auth import main as oauth_main
|
||||
oauth_main('github', translator)
|
||||
print_menu()
|
||||
case "7":
|
||||
logger.info("User selected disable auto update")
|
||||
import disable_auto_update
|
||||
disable_auto_update.run(translator)
|
||||
print_menu()
|
||||
case "8":
|
||||
logger.info("User selected totally reset cursor")
|
||||
import totally_reset_cursor
|
||||
totally_reset_cursor.run(translator)
|
||||
print_menu()
|
||||
case "9":
|
||||
logger.info("User selected view contributors")
|
||||
import logo
|
||||
print(logo.CURSOR_CONTRIBUTORS)
|
||||
print_menu()
|
||||
case "10":
|
||||
logger.info("User selected view config")
|
||||
from config import print_config
|
||||
print_config(get_config(), translator)
|
||||
print_menu()
|
||||
case "11":
|
||||
logger.info("User selected bypass version")
|
||||
import bypass_version
|
||||
bypass_version.main(translator)
|
||||
print_menu()
|
||||
case "12":
|
||||
logger.info("User selected check user authorized")
|
||||
import check_user_authorized
|
||||
check_user_authorized.main(translator)
|
||||
print_menu()
|
||||
case "13":
|
||||
logger.info("User selected bypass token limit")
|
||||
import bypass_token_limit
|
||||
bypass_token_limit.run(translator)
|
||||
print_menu()
|
||||
case "14":
|
||||
logger.info("User selected restore machine ID")
|
||||
import restore_machine_id
|
||||
restore_machine_id.run(translator)
|
||||
print_menu()
|
||||
case "15":
|
||||
logger.info("User selected delete cursor Google")
|
||||
import delete_cursor_google
|
||||
delete_cursor_google.main(translator)
|
||||
print_menu()
|
||||
case "16":
|
||||
logger.info("User selected select profile")
|
||||
from oauth_auth import OAuthHandler
|
||||
oauth = OAuthHandler(translator)
|
||||
oauth._select_profile()
|
||||
print_menu()
|
||||
case "17":
|
||||
logger.info("User selected manual custom auth")
|
||||
import manual_custom_auth
|
||||
manual_custom_auth.main(translator)
|
||||
print_menu()
|
||||
case _:
|
||||
logger.warning(f"Invalid choice: {choice}")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('menu.invalid_choice') if translator else 'Invalid choice'}{Style.RESET_ALL}")
|
||||
print_menu()
|
||||
except ImportError as e:
|
||||
logger.error(f"Failed to import module: {e}")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('menu.module_not_found', error=str(e)) if translator else f'Module not found: {e}'}{Style.RESET_ALL}")
|
||||
print_menu()
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing menu choice: {e}")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('menu.action_failed', error=str(e)) if translator else f'Action failed: {e}'}{Style.RESET_ALL}")
|
||||
print_menu()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
# Initialize translator
|
||||
translator = Translator()
|
||||
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
print(f"\n{Fore.YELLOW}{EMOJI['INFO']} {translator.get('menu.program_terminated') if translator else 'Program terminated by user'}{Style.RESET_ALL}")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
logger.critical(f"Unhandled exception: {e}")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('menu.unhandled_exception', error=str(e)) if translator else f'Unhandled exception: {e}'}{Style.RESET_ALL}")
|
||||
sys.exit(1)
|
||||
220
quit_cursor.py
220
quit_cursor.py
@@ -1,11 +1,22 @@
|
||||
import psutil
|
||||
import time
|
||||
from colorama import Fore, Style, init
|
||||
import logging
|
||||
import platform
|
||||
import sys
|
||||
import os
|
||||
from colorama import Fore, Style, init
|
||||
from typing import Optional, Dict, List, Any, Tuple, Union, Set
|
||||
|
||||
# Initialize colorama
|
||||
init()
|
||||
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__)
|
||||
|
||||
# Define emoji constants
|
||||
EMOJI = {
|
||||
@@ -13,43 +24,126 @@ EMOJI = {
|
||||
"SUCCESS": "✅",
|
||||
"ERROR": "❌",
|
||||
"INFO": "ℹ️",
|
||||
"WAIT": "⏳"
|
||||
"WAIT": "⏳",
|
||||
"KILL": "🛑",
|
||||
"SEARCH": "🔍"
|
||||
}
|
||||
|
||||
class CursorQuitter:
|
||||
def __init__(self, timeout=5, translator=None):
|
||||
self.timeout = timeout
|
||||
self.translator = translator # Use the passed translator
|
||||
"""Class to handle termination of Cursor processes."""
|
||||
|
||||
def __init__(self, timeout: int = 5, translator: Any = None):
|
||||
"""Initialize CursorQuitter.
|
||||
|
||||
def quit_cursor(self):
|
||||
"""Gently close Cursor processes"""
|
||||
try:
|
||||
print(f"{Fore.CYAN}{EMOJI['PROCESS']} {self.translator.get('quit_cursor.start')}...{Style.RESET_ALL}")
|
||||
cursor_processes = []
|
||||
Args:
|
||||
timeout: Maximum time to wait for processes to terminate naturally
|
||||
translator: Optional translator for internationalization
|
||||
"""
|
||||
self.timeout = max(1, timeout) # Ensure timeout is at least 1 second
|
||||
self.translator = translator
|
||||
|
||||
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
|
||||
|
||||
# Collect all Cursor processes
|
||||
for proc in psutil.process_iter(['pid', 'name']):
|
||||
try:
|
||||
if proc.info['name'].lower() in ['cursor.exe', 'cursor']:
|
||||
cursor_processes.append(proc)
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
Returns:
|
||||
str: Translated or fallback message
|
||||
"""
|
||||
if self.translator:
|
||||
return self.translator.get(key, **kwargs)
|
||||
return fallback.format(**kwargs) if kwargs else fallback
|
||||
|
||||
def _find_cursor_processes(self) -> List[psutil.Process]:
|
||||
"""Find all Cursor processes.
|
||||
|
||||
Returns:
|
||||
List[psutil.Process]: List of Cursor processes
|
||||
"""
|
||||
cursor_processes = []
|
||||
cursor_names = {
|
||||
'windows': ['cursor.exe', 'cursor helper.exe', 'cursor crash handler.exe'],
|
||||
'darwin': ['Cursor', 'Cursor Helper', 'Cursor Crash Handler'],
|
||||
'linux': ['cursor', 'cursor-helper', 'cursor-crash-handler']
|
||||
}
|
||||
|
||||
# Get platform-specific process names
|
||||
system = platform.system().lower()
|
||||
if system in cursor_names:
|
||||
target_names = cursor_names[system]
|
||||
else:
|
||||
# Fallback to all possible names
|
||||
target_names = [name for names in cursor_names.values() for name in names]
|
||||
|
||||
logger.info(f"Looking for Cursor processes with names: {target_names}")
|
||||
print(f"{Fore.CYAN}{EMOJI['SEARCH']} {self._get_message('quit_cursor.searching', 'Searching for Cursor processes...')}{Style.RESET_ALL}")
|
||||
|
||||
# Collect all Cursor processes
|
||||
for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
|
||||
try:
|
||||
proc_name = proc.info['name'].lower() if proc.info['name'] else ""
|
||||
|
||||
# Check process name
|
||||
if any(target.lower() in proc_name for target in target_names):
|
||||
cursor_processes.append(proc)
|
||||
continue
|
||||
|
||||
|
||||
# Check command line for additional detection
|
||||
if proc.info['cmdline']:
|
||||
cmdline = " ".join(proc.info['cmdline']).lower()
|
||||
if 'cursor' in cmdline and ('electron' in cmdline or 'app' in cmdline):
|
||||
cursor_processes.append(proc)
|
||||
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied, Exception) as e:
|
||||
logger.warning(f"Error accessing process: {e}")
|
||||
continue
|
||||
|
||||
return cursor_processes
|
||||
|
||||
def quit_cursor(self) -> bool:
|
||||
"""Gently close Cursor processes.
|
||||
|
||||
Returns:
|
||||
bool: True if all processes were terminated successfully, False otherwise
|
||||
"""
|
||||
try:
|
||||
msg = self._get_message('quit_cursor.start', 'Attempting to close Cursor processes')
|
||||
logger.info(msg)
|
||||
print(f"{Fore.CYAN}{EMOJI['PROCESS']} {msg}...{Style.RESET_ALL}")
|
||||
|
||||
# Find Cursor processes
|
||||
cursor_processes = self._find_cursor_processes()
|
||||
|
||||
if not cursor_processes:
|
||||
print(f"{Fore.GREEN}{EMOJI['INFO']} {self.translator.get('quit_cursor.no_process')}{Style.RESET_ALL}")
|
||||
msg = self._get_message('quit_cursor.no_process', 'No Cursor processes found')
|
||||
logger.info(msg)
|
||||
print(f"{Fore.GREEN}{EMOJI['INFO']} {msg}{Style.RESET_ALL}")
|
||||
return True
|
||||
|
||||
# Log found processes
|
||||
logger.info(f"Found {len(cursor_processes)} Cursor processes")
|
||||
print(f"{Fore.CYAN}{EMOJI['INFO']} {self._get_message('quit_cursor.processes_found', 'Found {count} Cursor processes', count=len(cursor_processes))}{Style.RESET_ALL}")
|
||||
|
||||
# Gently request processes to terminate
|
||||
for proc in cursor_processes:
|
||||
try:
|
||||
if proc.is_running():
|
||||
print(f"{Fore.YELLOW}{EMOJI['PROCESS']} {self.translator.get('quit_cursor.terminating', pid=proc.pid)}...{Style.RESET_ALL}")
|
||||
msg = self._get_message('quit_cursor.terminating', 'Terminating process {pid}', pid=proc.pid)
|
||||
logger.info(f"Terminating process {proc.pid}")
|
||||
print(f"{Fore.YELLOW}{EMOJI['PROCESS']} {msg}...{Style.RESET_ALL}")
|
||||
proc.terminate()
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied, Exception) as e:
|
||||
logger.warning(f"Error terminating process {proc.pid}: {e}")
|
||||
continue
|
||||
|
||||
# Wait for processes to terminate naturally
|
||||
print(f"{Fore.CYAN}{EMOJI['WAIT']} {self.translator.get('quit_cursor.waiting')}...{Style.RESET_ALL}")
|
||||
msg = self._get_message('quit_cursor.waiting', f'Waiting up to {self.timeout} seconds for processes to close')
|
||||
logger.info(f"Waiting up to {self.timeout} seconds for processes to close")
|
||||
print(f"{Fore.CYAN}{EMOJI['WAIT']} {msg}...{Style.RESET_ALL}")
|
||||
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < self.timeout:
|
||||
still_running = []
|
||||
@@ -57,33 +151,89 @@ class CursorQuitter:
|
||||
try:
|
||||
if proc.is_running():
|
||||
still_running.append(proc)
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied, Exception):
|
||||
continue
|
||||
|
||||
if not still_running:
|
||||
print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {self.translator.get('quit_cursor.success')}{Style.RESET_ALL}")
|
||||
msg = self._get_message('quit_cursor.success', 'All Cursor processes have been closed successfully')
|
||||
logger.info("All Cursor processes have been closed successfully")
|
||||
print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {msg}{Style.RESET_ALL}")
|
||||
return True
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
# If processes are still running after timeout
|
||||
# If processes are still running after timeout, try to kill them
|
||||
if still_running:
|
||||
process_list = ", ".join([str(p.pid) for p in still_running])
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {self.translator.get('quit_cursor.timeout', pids=process_list)}{Style.RESET_ALL}")
|
||||
return False
|
||||
msg = self._get_message('quit_cursor.timeout', 'Timeout reached. Some processes are still running: {pids}', pids=process_list)
|
||||
logger.warning(f"Timeout reached. Still running: {process_list}")
|
||||
print(f"{Fore.YELLOW}{EMOJI['WAIT']} {msg}{Style.RESET_ALL}")
|
||||
|
||||
# Try to kill remaining processes
|
||||
print(f"{Fore.RED}{EMOJI['KILL']} {self._get_message('quit_cursor.force_kill', 'Attempting to force kill remaining processes')}{Style.RESET_ALL}")
|
||||
for proc in still_running:
|
||||
try:
|
||||
if proc.is_running():
|
||||
logger.info(f"Force killing process {proc.pid}")
|
||||
proc.kill()
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied, Exception) as e:
|
||||
logger.error(f"Error killing process {proc.pid}: {e}")
|
||||
continue
|
||||
|
||||
# Check if all processes are now killed
|
||||
time.sleep(1)
|
||||
final_check = [p for p in still_running if p.is_running()]
|
||||
if not final_check:
|
||||
msg = self._get_message('quit_cursor.force_success', 'All Cursor processes have been forcefully terminated')
|
||||
logger.info("All Cursor processes have been forcefully terminated")
|
||||
print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {msg}{Style.RESET_ALL}")
|
||||
return True
|
||||
else:
|
||||
failed_list = ", ".join([str(p.pid) for p in final_check])
|
||||
msg = self._get_message('quit_cursor.force_failed', 'Failed to terminate some processes: {pids}', pids=failed_list)
|
||||
logger.error(f"Failed to terminate processes: {failed_list}")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {msg}{Style.RESET_ALL}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {self.translator.get('quit_cursor.error', error=str(e))}{Style.RESET_ALL}")
|
||||
logger.error(f"Error in quit_cursor: {e}")
|
||||
msg = self._get_message('quit_cursor.error', 'An error occurred: {error}', error=str(e))
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} {msg}{Style.RESET_ALL}")
|
||||
return False
|
||||
|
||||
def quit_cursor(translator=None, timeout=5):
|
||||
"""Convenient function for directly calling the quit function"""
|
||||
quitter = CursorQuitter(timeout, translator)
|
||||
return quitter.quit_cursor()
|
||||
def quit_cursor(translator: Any = None, timeout: int = 5) -> bool:
|
||||
"""Convenient function for directly calling the quit function.
|
||||
|
||||
Args:
|
||||
translator: Optional translator for internationalization
|
||||
timeout: Maximum time to wait for processes to terminate naturally
|
||||
|
||||
Returns:
|
||||
bool: True if all processes were terminated successfully, False otherwise
|
||||
"""
|
||||
try:
|
||||
quitter = CursorQuitter(timeout, translator)
|
||||
return quitter.quit_cursor()
|
||||
except Exception as e:
|
||||
logger.error(f"Error in quit_cursor function: {e}")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} An unexpected error occurred: {str(e)}{Style.RESET_ALL}")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
# If run directly, use the default translator
|
||||
from main import translator as main_translator
|
||||
quit_cursor(main_translator)
|
||||
try:
|
||||
# If run directly, try to use the default translator
|
||||
try:
|
||||
from main import translator as main_translator
|
||||
result = quit_cursor(main_translator)
|
||||
except ImportError:
|
||||
logger.warning("Failed to import translator from main.py, running without translation")
|
||||
result = quit_cursor()
|
||||
|
||||
# Exit with appropriate status code
|
||||
sys.exit(0 if result else 1)
|
||||
except Exception as e:
|
||||
logger.critical(f"Critical error: {e}")
|
||||
print(f"{Fore.RED}{EMOJI['ERROR']} Critical error: {str(e)}{Style.RESET_ALL}")
|
||||
sys.exit(1)
|
||||
@@ -1,13 +1,29 @@
|
||||
watchdog
|
||||
python-dotenv>=1.0.0
|
||||
colorama>=0.4.6
|
||||
requests
|
||||
psutil>=5.8.0
|
||||
pywin32; platform_system == "Windows"
|
||||
pyinstaller
|
||||
DrissionPage>=4.0.0
|
||||
selenium
|
||||
webdriver_manager
|
||||
arabic-reshaper
|
||||
python-bidi
|
||||
faker
|
||||
# Core dependencies
|
||||
colorama>=0.4.6,<0.5.0
|
||||
requests>=2.31.0,<3.0.0
|
||||
python-dotenv>=1.0.0,<2.0.0
|
||||
psutil>=5.9.5,<6.0.0
|
||||
watchdog>=3.0.0,<4.0.0
|
||||
|
||||
# Web automation
|
||||
selenium>=4.14.0,<5.0.0
|
||||
webdriver_manager>=4.0.0,<5.0.0
|
||||
DrissionPage>=4.0.0,<5.0.0
|
||||
|
||||
# Internationalization
|
||||
arabic-reshaper>=3.0.0,<4.0.0
|
||||
python-bidi>=0.4.2,<0.5.0
|
||||
|
||||
# Data generation
|
||||
faker>=19.3.0,<20.0.0
|
||||
|
||||
# Packaging
|
||||
pyinstaller>=6.0.0,<7.0.0
|
||||
|
||||
# Windows-specific dependencies
|
||||
pywin32>=306; platform_system == "Windows"
|
||||
|
||||
# Optional dependencies
|
||||
tqdm>=4.66.1,<5.0.0 # Progress bars
|
||||
cryptography>=41.0.4,<42.0.0 # Secure encryption
|
||||
pillow>=10.0.0,<11.0.0 # Image processing
|
||||
449
utils.py
449
utils.py
@@ -2,233 +2,332 @@ import os
|
||||
import sys
|
||||
import platform
|
||||
import random
|
||||
import shutil
|
||||
import logging
|
||||
from typing import Optional, Dict, List, Union, Tuple
|
||||
|
||||
def get_user_documents_path():
|
||||
"""Get user documents path"""
|
||||
# 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__)
|
||||
|
||||
def get_user_documents_path() -> str:
|
||||
"""Get user documents path across different operating systems.
|
||||
|
||||
Returns:
|
||||
str: Path to user's Documents directory
|
||||
"""
|
||||
if platform.system() == "Windows":
|
||||
try:
|
||||
import winreg
|
||||
# 打开注册表
|
||||
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders") as key:
|
||||
# 获取 "Personal" 键的值,这指向用户的文档目录
|
||||
documents_path, _ = winreg.QueryValueEx(key, "Personal")
|
||||
return documents_path
|
||||
except Exception as e:
|
||||
# fallback
|
||||
logger.warning(f"Failed to get Documents path from registry: {e}")
|
||||
return os.path.expanduser("~\\Documents")
|
||||
else:
|
||||
elif platform.system() == "Darwin": # macOS
|
||||
return os.path.expanduser("~/Documents")
|
||||
else: # Linux and other Unix-like systems
|
||||
# Check for XDG user directories
|
||||
try:
|
||||
with open(os.path.expanduser("~/.config/user-dirs.dirs"), "r") as f:
|
||||
for line in f:
|
||||
if line.startswith("XDG_DOCUMENTS_DIR"):
|
||||
path = line.split("=")[1].strip().strip('"').replace("$HOME", os.path.expanduser("~"))
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
except (FileNotFoundError, IOError):
|
||||
pass
|
||||
|
||||
# Fallback to ~/Documents
|
||||
return os.path.expanduser("~/Documents")
|
||||
|
||||
def get_default_driver_path(browser_type='chrome'):
|
||||
"""Get default driver path based on browser type"""
|
||||
browser_type = browser_type.lower()
|
||||
if browser_type == 'chrome':
|
||||
return get_default_chrome_driver_path()
|
||||
elif browser_type == 'edge':
|
||||
return get_default_edge_driver_path()
|
||||
elif browser_type == 'firefox':
|
||||
return get_default_firefox_driver_path()
|
||||
elif browser_type == 'brave':
|
||||
# Brave 使用 Chrome 的 driver
|
||||
return get_default_chrome_driver_path()
|
||||
else:
|
||||
# Default to Chrome if browser type is unknown
|
||||
return get_default_chrome_driver_path()
|
||||
|
||||
def get_default_chrome_driver_path():
|
||||
"""Get default Chrome driver path"""
|
||||
def find_executable(executable_names: List[str]) -> Optional[str]:
|
||||
"""Find executable in PATH by trying multiple possible names.
|
||||
|
||||
Args:
|
||||
executable_names: List of possible executable names to try
|
||||
|
||||
Returns:
|
||||
Path to the executable if found, None otherwise
|
||||
"""
|
||||
for name in executable_names:
|
||||
try:
|
||||
path = shutil.which(name)
|
||||
if path:
|
||||
return path
|
||||
except Exception:
|
||||
continue
|
||||
return None
|
||||
|
||||
def get_default_driver_path(browser_type: str = 'chrome') -> str:
|
||||
"""Get default driver path based on browser type.
|
||||
|
||||
Args:
|
||||
browser_type: Type of browser ('chrome', 'edge', 'firefox', 'brave')
|
||||
|
||||
Returns:
|
||||
str: Path to the browser driver
|
||||
"""
|
||||
browser_type = browser_type.lower()
|
||||
driver_map = {
|
||||
'chrome': get_default_chrome_driver_path,
|
||||
'edge': get_default_edge_driver_path,
|
||||
'firefox': get_default_firefox_driver_path,
|
||||
'brave': get_default_chrome_driver_path, # Brave uses Chrome driver
|
||||
'opera': get_default_chrome_driver_path, # Opera uses Chrome driver
|
||||
'operagx': get_default_chrome_driver_path # OperaGX uses Chrome driver
|
||||
}
|
||||
|
||||
driver_func = driver_map.get(browser_type, get_default_chrome_driver_path)
|
||||
return driver_func()
|
||||
|
||||
def get_default_chrome_driver_path() -> str:
|
||||
"""Get default Chrome driver path based on platform."""
|
||||
if sys.platform == "win32":
|
||||
return os.path.join(os.path.dirname(os.path.abspath(__file__)), "drivers", "chromedriver.exe")
|
||||
elif sys.platform == "darwin":
|
||||
return os.path.join(os.path.dirname(os.path.abspath(__file__)), "drivers", "chromedriver")
|
||||
else:
|
||||
else: # Linux and other Unix-like systems
|
||||
# Try to find chromedriver in PATH first
|
||||
path = find_executable(["chromedriver"])
|
||||
if path:
|
||||
return path
|
||||
return "/usr/local/bin/chromedriver"
|
||||
|
||||
def get_default_edge_driver_path():
|
||||
"""Get default Edge driver path"""
|
||||
def get_default_edge_driver_path() -> str:
|
||||
"""Get default Edge driver path based on platform."""
|
||||
if sys.platform == "win32":
|
||||
return os.path.join(os.path.dirname(os.path.abspath(__file__)), "drivers", "msedgedriver.exe")
|
||||
elif sys.platform == "darwin":
|
||||
return os.path.join(os.path.dirname(os.path.abspath(__file__)), "drivers", "msedgedriver")
|
||||
else:
|
||||
else: # Linux and other Unix-like systems
|
||||
path = find_executable(["msedgedriver"])
|
||||
if path:
|
||||
return path
|
||||
return "/usr/local/bin/msedgedriver"
|
||||
|
||||
def get_default_firefox_driver_path():
|
||||
"""Get default Firefox driver path"""
|
||||
def get_default_firefox_driver_path() -> str:
|
||||
"""Get default Firefox driver path based on platform."""
|
||||
if sys.platform == "win32":
|
||||
return os.path.join(os.path.dirname(os.path.abspath(__file__)), "drivers", "geckodriver.exe")
|
||||
elif sys.platform == "darwin":
|
||||
return os.path.join(os.path.dirname(os.path.abspath(__file__)), "drivers", "geckodriver")
|
||||
else:
|
||||
else: # Linux and other Unix-like systems
|
||||
path = find_executable(["geckodriver"])
|
||||
if path:
|
||||
return path
|
||||
return "/usr/local/bin/geckodriver"
|
||||
|
||||
def get_default_brave_driver_path():
|
||||
"""Get default Brave driver path (uses Chrome driver)"""
|
||||
# Brave 浏览器基于 Chromium,所以使用相同的 chromedriver
|
||||
return get_default_chrome_driver_path()
|
||||
|
||||
def get_default_browser_path(browser_type='chrome'):
|
||||
"""Get default browser executable path"""
|
||||
def get_default_browser_path(browser_type: str = 'chrome') -> str:
|
||||
"""Get default browser executable path based on platform and browser type.
|
||||
|
||||
Args:
|
||||
browser_type: Type of browser ('chrome', 'edge', 'firefox', 'brave', 'opera', 'operagx')
|
||||
|
||||
Returns:
|
||||
str: Path to the browser executable
|
||||
"""
|
||||
browser_type = browser_type.lower()
|
||||
|
||||
# Platform-specific browser paths
|
||||
if sys.platform == "win32":
|
||||
if browser_type == 'chrome':
|
||||
# 尝试在 PATH 中找到 Chrome
|
||||
try:
|
||||
import shutil
|
||||
chrome_in_path = shutil.which("chrome")
|
||||
if chrome_in_path:
|
||||
return chrome_in_path
|
||||
except:
|
||||
pass
|
||||
# 使用默认路径
|
||||
return r"C:\Program Files\Google\Chrome\Application\chrome.exe"
|
||||
elif browser_type == 'edge':
|
||||
return r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
|
||||
elif browser_type == 'firefox':
|
||||
return r"C:\Program Files\Mozilla Firefox\firefox.exe"
|
||||
elif browser_type == 'opera':
|
||||
# 尝试多个可能的 Opera 路径
|
||||
opera_paths = [
|
||||
r"C:\Program Files\Opera\opera.exe",
|
||||
r"C:\Program Files (x86)\Opera\opera.exe",
|
||||
os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Programs', 'Opera', 'launcher.exe'),
|
||||
os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Programs', 'Opera', 'opera.exe')
|
||||
]
|
||||
for path in opera_paths:
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
return opera_paths[0] # 返回第一个路径,即使它不存在
|
||||
elif browser_type == 'operagx':
|
||||
# 尝试多个可能的 Opera GX 路径
|
||||
operagx_paths = [
|
||||
os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Programs', 'Opera GX', 'launcher.exe'),
|
||||
os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Programs', 'Opera GX', 'opera.exe'),
|
||||
r"C:\Program Files\Opera GX\opera.exe",
|
||||
r"C:\Program Files (x86)\Opera GX\opera.exe"
|
||||
]
|
||||
for path in operagx_paths:
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
return operagx_paths[0] # 返回第一个路径,即使它不存在
|
||||
elif browser_type == 'brave':
|
||||
# Brave 浏览器的默认安装路径
|
||||
paths = [
|
||||
os.path.join(os.environ.get('PROGRAMFILES', ''), 'BraveSoftware/Brave-Browser/Application/brave.exe'),
|
||||
os.path.join(os.environ.get('PROGRAMFILES(X86)', ''), 'BraveSoftware/Brave-Browser/Application/brave.exe'),
|
||||
os.path.join(os.environ.get('LOCALAPPDATA', ''), 'BraveSoftware/Brave-Browser/Application/brave.exe')
|
||||
]
|
||||
for path in paths:
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
return paths[0] # 返回第一个路径,即使它不存在
|
||||
|
||||
return _get_windows_browser_path(browser_type)
|
||||
elif sys.platform == "darwin":
|
||||
if browser_type == 'chrome':
|
||||
return "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
||||
elif browser_type == 'edge':
|
||||
return "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"
|
||||
elif browser_type == 'firefox':
|
||||
return "/Applications/Firefox.app/Contents/MacOS/firefox"
|
||||
elif browser_type == 'brave':
|
||||
return "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
|
||||
elif browser_type == 'opera':
|
||||
return "/Applications/Opera.app/Contents/MacOS/Opera"
|
||||
elif browser_type == 'operagx':
|
||||
return "/Applications/Opera GX.app/Contents/MacOS/Opera"
|
||||
|
||||
else: # Linux
|
||||
if browser_type == 'chrome':
|
||||
# 尝试多种可能的名称
|
||||
chrome_names = ["google-chrome", "chrome", "chromium", "chromium-browser"]
|
||||
for name in chrome_names:
|
||||
try:
|
||||
import shutil
|
||||
path = shutil.which(name)
|
||||
if path:
|
||||
return path
|
||||
except:
|
||||
pass
|
||||
return "/usr/bin/google-chrome"
|
||||
elif browser_type == 'edge':
|
||||
return "/usr/bin/microsoft-edge"
|
||||
elif browser_type == 'firefox':
|
||||
return "/usr/bin/firefox"
|
||||
elif browser_type == 'opera':
|
||||
return "/usr/bin/opera"
|
||||
elif browser_type == 'operagx':
|
||||
# 尝试常见的 Opera GX 路径
|
||||
operagx_names = ["opera-gx"]
|
||||
for name in operagx_names:
|
||||
try:
|
||||
import shutil
|
||||
path = shutil.which(name)
|
||||
if path:
|
||||
return path
|
||||
except:
|
||||
pass
|
||||
return "/usr/bin/opera-gx"
|
||||
elif browser_type == 'brave':
|
||||
# 尝试常见的 Brave 路径
|
||||
brave_names = ["brave", "brave-browser"]
|
||||
for name in brave_names:
|
||||
try:
|
||||
import shutil
|
||||
path = shutil.which(name)
|
||||
if path:
|
||||
return path
|
||||
except:
|
||||
pass
|
||||
return "/usr/bin/brave-browser"
|
||||
|
||||
# 如果找不到指定的浏览器类型,则返回 Chrome 的路径
|
||||
return get_default_browser_path('chrome')
|
||||
return _get_macos_browser_path(browser_type)
|
||||
else: # Linux and other Unix-like systems
|
||||
return _get_linux_browser_path(browser_type)
|
||||
|
||||
def get_linux_cursor_path():
|
||||
"""Get Linux Cursor path"""
|
||||
def _get_windows_browser_path(browser_type: str) -> str:
|
||||
"""Get browser path for Windows."""
|
||||
browser_paths = {
|
||||
'chrome': [
|
||||
shutil.which("chrome"),
|
||||
r"C:\Program Files\Google\Chrome\Application\chrome.exe",
|
||||
r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
|
||||
os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Google', 'Chrome', 'Application', 'chrome.exe')
|
||||
],
|
||||
'edge': [
|
||||
shutil.which("msedge"),
|
||||
r"C:\Program Files\Microsoft\Edge\Application\msedge.exe",
|
||||
r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
|
||||
],
|
||||
'firefox': [
|
||||
shutil.which("firefox"),
|
||||
r"C:\Program Files\Mozilla Firefox\firefox.exe",
|
||||
r"C:\Program Files (x86)\Mozilla Firefox\firefox.exe"
|
||||
],
|
||||
'opera': [
|
||||
shutil.which("opera"),
|
||||
r"C:\Program Files\Opera\opera.exe",
|
||||
r"C:\Program Files (x86)\Opera\opera.exe",
|
||||
os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Programs', 'Opera', 'launcher.exe'),
|
||||
os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Programs', 'Opera', 'opera.exe')
|
||||
],
|
||||
'operagx': [
|
||||
shutil.which("opera"),
|
||||
os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Programs', 'Opera GX', 'launcher.exe'),
|
||||
os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Programs', 'Opera GX', 'opera.exe'),
|
||||
r"C:\Program Files\Opera GX\opera.exe",
|
||||
r"C:\Program Files (x86)\Opera GX\opera.exe"
|
||||
],
|
||||
'brave': [
|
||||
shutil.which("brave"),
|
||||
os.path.join(os.environ.get('PROGRAMFILES', ''), 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'),
|
||||
os.path.join(os.environ.get('PROGRAMFILES(X86)', ''), 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'),
|
||||
os.path.join(os.environ.get('LOCALAPPDATA', ''), 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe')
|
||||
]
|
||||
}
|
||||
|
||||
# Return first existing path
|
||||
paths = browser_paths.get(browser_type, browser_paths['chrome'])
|
||||
for path in paths:
|
||||
if path and os.path.exists(path):
|
||||
return path
|
||||
|
||||
# Return first path as fallback
|
||||
return next((p for p in paths if p), r"C:\Program Files\Google\Chrome\Application\chrome.exe")
|
||||
|
||||
def _get_macos_browser_path(browser_type: str) -> str:
|
||||
"""Get browser path for macOS."""
|
||||
browser_paths = {
|
||||
'chrome': [
|
||||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
||||
"~/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
||||
],
|
||||
'edge': [
|
||||
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
||||
"~/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"
|
||||
],
|
||||
'firefox': [
|
||||
"/Applications/Firefox.app/Contents/MacOS/firefox",
|
||||
"~/Applications/Firefox.app/Contents/MacOS/firefox"
|
||||
],
|
||||
'brave': [
|
||||
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
|
||||
"~/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
|
||||
],
|
||||
'opera': [
|
||||
"/Applications/Opera.app/Contents/MacOS/Opera",
|
||||
"~/Applications/Opera.app/Contents/MacOS/Opera"
|
||||
],
|
||||
'operagx': [
|
||||
"/Applications/Opera GX.app/Contents/MacOS/Opera",
|
||||
"~/Applications/Opera GX.app/Contents/MacOS/Opera"
|
||||
]
|
||||
}
|
||||
|
||||
# Return first existing path
|
||||
paths = browser_paths.get(browser_type, browser_paths['chrome'])
|
||||
for path in paths:
|
||||
expanded_path = os.path.expanduser(path)
|
||||
if os.path.exists(expanded_path):
|
||||
return expanded_path
|
||||
|
||||
# Return first path as fallback
|
||||
return os.path.expanduser(paths[0])
|
||||
|
||||
def _get_linux_browser_path(browser_type: str) -> str:
|
||||
"""Get browser path for Linux."""
|
||||
browser_executables = {
|
||||
'chrome': ["google-chrome", "chrome", "chromium", "chromium-browser"],
|
||||
'edge': ["microsoft-edge", "msedge"],
|
||||
'firefox': ["firefox", "firefox-esr"],
|
||||
'opera': ["opera"],
|
||||
'operagx': ["opera-gx", "opera"],
|
||||
'brave': ["brave-browser", "brave"]
|
||||
}
|
||||
|
||||
# Try to find executable in PATH
|
||||
executables = browser_executables.get(browser_type, browser_executables['chrome'])
|
||||
path = find_executable(executables)
|
||||
if path:
|
||||
return path
|
||||
|
||||
# Fallback to common locations
|
||||
common_locations = {
|
||||
'chrome': "/usr/bin/google-chrome",
|
||||
'edge': "/usr/bin/microsoft-edge",
|
||||
'firefox': "/usr/bin/firefox",
|
||||
'opera': "/usr/bin/opera",
|
||||
'operagx': "/usr/bin/opera",
|
||||
'brave': "/usr/bin/brave-browser"
|
||||
}
|
||||
|
||||
return common_locations.get(browser_type, common_locations['chrome'])
|
||||
|
||||
def get_linux_cursor_path() -> str:
|
||||
"""Get Linux Cursor path by checking multiple possible locations."""
|
||||
possible_paths = [
|
||||
"/opt/Cursor/resources/app",
|
||||
"/usr/share/cursor/resources/app",
|
||||
"/opt/cursor-bin/resources/app",
|
||||
"/usr/lib/cursor/resources/app",
|
||||
os.path.expanduser("~/.local/share/cursor/resources/app")
|
||||
os.path.expanduser("~/.local/share/cursor/resources/app"),
|
||||
# Add extracted AppImage paths
|
||||
*[p for p in [os.path.expanduser("~/squashfs-root/usr/share/cursor/resources/app")] if os.path.exists(p)]
|
||||
]
|
||||
|
||||
# return the first path that exists
|
||||
return next((path for path in possible_paths if os.path.exists(path)), possible_paths[0])
|
||||
# Return first existing path or default if none exists
|
||||
for path in possible_paths:
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
|
||||
# Log warning if no path found
|
||||
logger.warning("No Cursor installation found in common Linux paths")
|
||||
return possible_paths[0]
|
||||
|
||||
def get_random_wait_time(config, timing_key):
|
||||
"""Get random wait time based on configuration timing settings
|
||||
def parse_time_range(time_str: str) -> Tuple[float, float]:
|
||||
"""Parse a time range string into min and max values.
|
||||
|
||||
Args:
|
||||
config (dict): Configuration dictionary containing timing settings
|
||||
timing_key (str): Key to look up in the timing settings
|
||||
time_str: String representing time range (e.g., "0.5-1.5" or "0.5,1.5")
|
||||
|
||||
Returns:
|
||||
Tuple of (min_time, max_time)
|
||||
"""
|
||||
try:
|
||||
if isinstance(time_str, (int, float)):
|
||||
return float(time_str), float(time_str)
|
||||
|
||||
if '-' in time_str:
|
||||
min_time, max_time = map(float, time_str.split('-'))
|
||||
elif ',' in time_str:
|
||||
min_time, max_time = map(float, time_str.split(','))
|
||||
else:
|
||||
min_time = max_time = float(time_str)
|
||||
|
||||
return min_time, max_time
|
||||
except (ValueError, TypeError):
|
||||
return 0.5, 1.5
|
||||
|
||||
def get_random_wait_time(config: Dict, timing_key: str, default_range: Tuple[float, float] = (0.5, 1.5)) -> float:
|
||||
"""Get random wait time based on configuration timing settings.
|
||||
|
||||
Args:
|
||||
config: Configuration dictionary containing timing settings
|
||||
timing_key: Key to look up in the timing settings
|
||||
default_range: Default time range to use if config value is invalid
|
||||
|
||||
Returns:
|
||||
float: Random wait time in seconds
|
||||
"""
|
||||
try:
|
||||
# Get timing value from config
|
||||
if not config or 'Timing' not in config:
|
||||
return random.uniform(*default_range)
|
||||
|
||||
timing = config.get('Timing', {}).get(timing_key)
|
||||
if not timing:
|
||||
# Default to 0.5-1.5 seconds if timing not found
|
||||
return random.uniform(0.5, 1.5)
|
||||
|
||||
# Check if timing is a range (e.g., "0.5-1.5" or "0.5,1.5")
|
||||
if isinstance(timing, str):
|
||||
if '-' in timing:
|
||||
min_time, max_time = map(float, timing.split('-'))
|
||||
elif ',' in timing:
|
||||
min_time, max_time = map(float, timing.split(','))
|
||||
else:
|
||||
# Single value, use it as both min and max
|
||||
min_time = max_time = float(timing)
|
||||
else:
|
||||
# If timing is a number, use it as both min and max
|
||||
min_time = max_time = float(timing)
|
||||
return random.uniform(*default_range)
|
||||
|
||||
min_time, max_time = parse_time_range(timing)
|
||||
return random.uniform(min_time, max_time)
|
||||
|
||||
except (ValueError, TypeError, AttributeError):
|
||||
# Return default value if any error occurs
|
||||
return random.uniform(0.5, 1.5)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error getting wait time for {timing_key}: {e}")
|
||||
return random.uniform(*default_range)
|
||||
Reference in New Issue
Block a user