diff --git a/winehelper_gui.py b/winehelper_gui.py index 4ad2f16..4affe8c 100644 --- a/winehelper_gui.py +++ b/winehelper_gui.py @@ -33,393 +33,6 @@ class Var: GENERAL = os.environ.get("GENERAL") WH_WINETRICKS = os.environ.get("WH_WINETRICKS") -class DependencyManager: - """Класс для управления проверкой и установкой системных зависимостей.""" - - def __init__(self): - """Инициализирует менеджер, определяя необходимые пути.""" - self.dependencies_script_path = self._get_dependencies_path() - self.config_dir = os.path.join(os.path.expanduser("~"), ".config", "winehelper") - self.hash_flag_file = os.path.join(self.config_dir, "dependencies_hash.txt") - os.makedirs(self.config_dir, exist_ok=True) - self.app_icon = QIcon(Var.WH_ICON_PATH) if Var.WH_ICON_PATH and os.path.exists(Var.WH_ICON_PATH) else QIcon() - - def _get_dependencies_path(self): - """Определяет и возвращает путь к скрипту dependencies.sh.""" - if not Var.DATA_PATH: - return None - - return os.path.join(Var.DATA_PATH, 'dependencies.sh') - - def _calculate_file_hash(self): - """Вычисляет хэш SHA256 файла зависимостей.""" - if not self.dependencies_script_path or not os.path.exists(self.dependencies_script_path): - return None - hasher = hashlib.sha256() - try: - with open(self.dependencies_script_path, 'rb') as f: - while chunk := f.read(4096): - hasher.update(chunk) - return hasher.hexdigest() - except IOError: - return None - - def _parse_dependencies_from_script(self): - """ - Парсит скрипт dependencies.sh для извлечения списка базовых пакетов. - Возвращает список пакетов или None в случае ошибки. - """ - if not os.path.exists(self.dependencies_script_path): - return None - - base_packages = [] - try: - with open(self.dependencies_script_path, 'r', encoding='utf-8') as f: - content = f.read() - - content = content.replace('\\\n', '') - pattern = r'apt-get install\s+\{i586-,\}(\{.*?\}|[\w.-]+)' - matches = re.findall(pattern, content) - - for match in matches: - match = match.strip() - if match.startswith('{') and match.endswith('}'): - group_content = match[1:-1] - packages = [pkg.strip() for pkg in group_content.split(',')] - base_packages.extend(packages) - else: - base_packages.append(match) - - return sorted(list(set(pkg for pkg in base_packages if pkg))) or None - except Exception: - return None - - def _parse_repo_error_from_script(self): - """ - Парсит скрипт dependencies.sh для извлечения сообщения об ошибке репозитория. - Возвращает сообщение или None в случае ошибки. - """ - if not os.path.exists(self.dependencies_script_path): - return None - - try: - with open(self.dependencies_script_path, 'r', encoding='utf-8') as f: - content = f.read() - - content = content.replace('\\\n', ' ') - match = re.search(r'apt-repo.*\|\|.*fatal\s+"([^"]+)"', content) - if match: - error_message = match.group(1).strip() - return re.sub(r'\s+', ' ', error_message) - return None - except Exception: - return None - - def _show_startup_messages(self): - """ - Проверяет, является ли это первым запуском или обновлением, - и показывает соответствующие сообщения. - """ - current_hash = self._calculate_file_hash() - stored_hash = None - hash_file_exists = os.path.exists(self.hash_flag_file) - - if hash_file_exists: - try: - with open(self.hash_flag_file, 'r', encoding='utf-8') as f: - stored_hash = f.read().strip() - except IOError: - pass - - if not hash_file_exists: - msg_box = QMessageBox(QMessageBox.Information, "Первый запуск WineHelper", - "Поскольку это первый запуск, программа проверит наличие необходимых системных зависимостей.") - msg_box.setWindowIcon(self.app_icon) - msg_box.exec_() - elif current_hash != stored_hash: - msg_box = QMessageBox(QMessageBox.Information, "Обновление зависимостей", - "Обнаружены изменения в системных требованиях.\n\n" - "Программа выполнит проверку, чтобы убедиться, что все компоненты на месте.") - msg_box.setWindowIcon(self.app_icon) - msg_box.exec_() - - def _save_dependency_hash(self): - """Сохраняет хэш зависимостей в конфигурационный файл.""" - current_hash = self._calculate_file_hash() - if not current_hash: - return - try: - with open(self.hash_flag_file, 'w', encoding='utf-8') as f: - f.write(current_hash) - except IOError: - print("Предупреждение: не удалось записать файл с хэшем зависимостей.") - - def _perform_check_and_install(self): - """ - Выполняет основную логику проверки и установки зависимостей. - Возвращает True в случае успеха, иначе False. - """ - # Проверка наличия ключевых утилит - if not shutil.which('apt-repo') or not shutil.which('rpm'): - return True - - if not self.dependencies_script_path or not os.path.exists(self.dependencies_script_path): - msg_box = QMessageBox(QMessageBox.Critical, "Критическая ошибка", - f"Файл зависимостей не найден по пути:\n'{self.dependencies_script_path}'.\n\n" - "Программа не может продолжить работу." - ) - msg_box.setWindowIcon(self.app_icon) - msg_box.exec_() - return False - - # 1. Проверка наличия репозитория x86_64-i586 - try: - result = subprocess.run(['apt-repo'], capture_output=True, text=True, check=False, encoding='utf-8') - if result.returncode != 0 or 'x86_64-i586' not in result.stdout: - error_message = self._parse_repo_error_from_script() - if not error_message: - msg_box = QMessageBox(QMessageBox.Critical, "Критическая ошибка", - f"Репозиторий x86_64-i586 не подключен, но не удалось извлечь текст ошибки из файла:\n'{self.dependencies_script_path}'.\n\n" - "Проверьте целостность скрипта. Работа программы будет прекращена." - ) - msg_box.setWindowIcon(self.app_icon) - msg_box.exec_() - return False - - msg_box = QMessageBox(QMessageBox.Critical, "Ошибка репозитория", - f"{error_message}\n\nРабота программы будет прекращена.") - msg_box.setWindowIcon(self.app_icon) - msg_box.exec_() - return False - except FileNotFoundError: - return True - - # 2. Определение списка пакетов из dependencies.sh - base_packages = self._parse_dependencies_from_script() - - # Если парсинг не удался или скрипт не найден, прерываем работу. - if not base_packages: - msg_box = QMessageBox(QMessageBox.Critical, "Критическая ошибка", - f"Не удалось найти или проанализировать файл зависимостей:\n'{self.dependencies_script_path}'.\n\n" - "Программа не может продолжить работу без списка необходимых пакетов." - ) - msg_box.setWindowIcon(self.app_icon) - msg_box.exec_() - return False - - required_packages = [f"i586-{pkg}" for pkg in base_packages] + base_packages - - # 3. Проверка, какие пакеты отсутствуют - try: - # Запрашиваем список всех установленных пакетов один раз для эффективности - result = subprocess.run( - ['rpm', '-qa', '--queryformat', '%{NAME}\n'], - capture_output=True, text=True, check=True, encoding='utf-8' - ) - installed_packages_set = set(result.stdout.splitlines()) - required_packages_set = set(required_packages) - - # Находим разницу между требуемыми и установленными пакетами - missing_packages = sorted(list(required_packages_set - installed_packages_set)) - - except (subprocess.CalledProcessError, FileNotFoundError) as e: - # В случае ошибки (например, rpm не найден), показываем критическое сообщение - msg_box = QMessageBox(QMessageBox.Critical, "Критическая ошибка", - f"Не удалось получить список установленных пакетов с помощью rpm.\n\nОшибка: {e}\n\n" - "Программа не может проверить зависимости и будет закрыта." - ) - msg_box.setWindowIcon(self.app_icon) - msg_box.exec_() - return False - - if not missing_packages: - return True - - # 4. Запрос у пользователя на установку - msg_box = QMessageBox() - msg_box.setWindowIcon(self.app_icon) - msg_box.setIcon(QMessageBox.Warning) - msg_box.setWindowTitle("Отсутствуют зависимости") - # Устанавливаем формат текста в RichText, чтобы QMessageBox корректно обрабатывал HTML-теги. - msg_box.setTextFormat(Qt.RichText) - - # Формируем весь текст как единый HTML-блок для корректного отображения. - full_html_text = ( - "Для корректной работы WineHelper требуются дополнительные системные компоненты.

" - "Отсутствуют следующие пакеты:
" - # Ограничиваем высоту блока с пакетами и добавляем прокрутку, если список длинный. - f"""
- {'
'.join(sorted(missing_packages))} -
""" - "Нажмите 'Установить', чтобы запустить установку с правами администратора. " - "Вам потребуется ввести пароль. Этот процесс может занять некоторое время." - ) - msg_box.setText(full_html_text) - install_button = msg_box.addButton("Установить", QMessageBox.AcceptRole) - cancel_button = msg_box.addButton("Отмена", QMessageBox.RejectRole) - msg_box.setDefaultButton(install_button) - - msg_box.exec_() - - if msg_box.clickedButton() != install_button: - cancel_box = QMessageBox(QMessageBox.Warning, "Отмена", - "Установка отменена.\n\n" - "WineHelper не может продолжить работу\nбез необходимых зависимостей.") - cancel_box.setWindowIcon(self.app_icon) - cancel_box.exec_() - return False - - # 5. Запуск установки с отображением лога в диалоговом окне - if not shutil.which('pkexec'): - error_box = QMessageBox(QMessageBox.Critical, "Ошибка", - "Не найден компонент 'pkexec' для повышения прав.\n" - "Пожалуйста, установите пакет 'polkit-pkexec', " - f"а затем установите зависимости вручную:\n\n sudo apt-get install {' '.join(missing_packages)}" - ) - error_box.setWindowIcon(self.app_icon) - error_box.exec_() - return False - - install_cmd_str = f"apt-get update && apt-get install -y {' '.join(missing_packages)}" - - # Этот флаг будет установлен в True, если установка и проверка пройдут успешно. - installation_successful = False - - # Создаем диалог для вывода лога установки - dialog = QDialog() - dialog.setWindowIcon(self.app_icon) - dialog.setWindowTitle("Установка зависимостей") - dialog.setMinimumSize(680, 400) - dialog.setModal(True) - - # Центрирование окна по центру экрана - screen_geometry = QApplication.primaryScreen().availableGeometry() - dialog.move( - (screen_geometry.width() - dialog.width()) // 2, - (screen_geometry.height() - dialog.height()) // 2 - ) - - layout = QVBoxLayout(dialog) - log_output = QTextEdit() - log_output.setReadOnly(True) - log_output.setFont(QFont("DejaVu Sans Mono", 10)) - layout.addWidget(log_output) - - close_button = QPushButton("Закрыть") - close_button.setEnabled(False) - close_button.clicked.connect(dialog.accept) - layout.addWidget(close_button) - - process = QProcess(dialog) - process.setProcessChannelMode(QProcess.MergedChannels) - - def handle_output(): - # Используем insertPlainText для корректного отображения потокового вывода (например, прогресс-баров) - log_output.insertPlainText(process.readAll().data().decode('utf-8', 'ignore')) - log_output.moveCursor(QTextCursor.End) - - def handle_finish(exit_code, exit_status): - nonlocal installation_successful - log_output.moveCursor(QTextCursor.End) - if exit_code == 0 and exit_status == QProcess.NormalExit: - log_output.append("\n=== Установка успешно завершена ===") - log_output.ensureCursorVisible() - - # Повторная проверка зависимостей сразу после установки - try: - result = subprocess.run( - ['rpm', '-qa', '--queryformat', '%{NAME}\n'], - capture_output=True, text=True, check=True, encoding='utf-8' - ) - installed_packages_set = set(result.stdout.splitlines()) - missing_packages_set = set(missing_packages) - still_missing = sorted(list(missing_packages_set - installed_packages_set)) - except (subprocess.CalledProcessError, FileNotFoundError): - warn_box = QMessageBox(dialog) - warn_box.setWindowIcon(self.app_icon) - warn_box.setIcon(QMessageBox.Warning) - warn_box.setWindowTitle("Проверка не удалась") - warn_box.setText("Не удалось повторно проверить зависимости после установки.") - warn_box.exec_() - still_missing = missing_packages - - if not still_missing: - installation_successful = True - close_button.setText("Запустить WineHelper") - else: - warn_box = QMessageBox(dialog) - warn_box.setWindowIcon(self.app_icon) - warn_box.setIcon(QMessageBox.Warning) - warn_box.setWindowTitle("Установка не завершена") - warn_box.setText( - "Не все зависимости были установлены. WineHelper может работать некорректно.\n\n" - f"Отсутствуют: {', '.join(sorted(still_missing))}\n\n" - "Попробуйте запустить установку снова или установите пакеты вручную." - ) - warn_box.exec_() - else: - if exit_code == 127: # pkexec: пользователь отменил аутентификацию - log_output.append("\n=== УСТАНОВКА ОТМЕНЕНА ПОЛЬЗОВАТЕЛЕМ ===") - log_output.append("Вы отменили ввод пароля. Установка зависимостей не была выполнена.") - elif exit_code == 126: # pkexec: у пользователя нет прав - log_output.append("\n=== ОШИБКА: НЕДОСТАТОЧНО ПРАВ ===") - log_output.append("У вашего пользователя нет прав для выполнения этой операции.") - else: - log_tag = "ПРЕРВАНО" if exit_status == QProcess.CrashExit else "ОШИБКА" - log_output.append(f"\n=== {log_tag} (код: {exit_code}) ===") - log_output.append("Произошла непредвиденная ошибка во время установки.") - log_output.ensureCursorVisible() - - close_button.setEnabled(True) - - def dialog_close_handler(event): - """Обрабатывает закрытие окна во время установки зависимостей.""" - if process.state() == QProcess.Running: - # QMessageBox без кнопок может некорректно обрабатывать закрытие. - # Используем простой QDialog для надежности. - info_dialog = QDialog(dialog) - info_dialog.setWindowTitle("Идет установка") - info_dialog.setModal(True) - info_dialog.setFixedSize(450, 150) - - layout = QVBoxLayout(info_dialog) - label = QLabel( - "

Установка зависимостей еще не завершена.

" - "

Пожалуйста, дождитесь окончания процесса.

" - "

Закрыть основное окно можно будет после завершения установки.

" - ) - label.setTextFormat(Qt.RichText) - label.setAlignment(Qt.AlignCenter) - layout.addWidget(label) - - info_dialog.exec_() - event.ignore() - else: - event.accept() - - process.readyRead.connect(handle_output) - process.finished.connect(handle_finish) - - log_output.append(f"Выполнение команды:\npkexec sh -c \"{install_cmd_str}\"\n") - log_output.append("Пожалуйста, введите пароль в появившемся окне аутентификации...") - log_output.append("-" * 40 + "\n") - - dialog.closeEvent = dialog_close_handler - process.start('pkexec', ['sh', '-c', install_cmd_str]) - dialog.exec_() - - return installation_successful - - def run(self): - """ - Основной публичный метод для запуска полной проверки зависимостей. - Возвращает True, если все зависимости удовлетворены, иначе False. - """ - self._show_startup_messages() - if self._perform_check_and_install(): - self._save_dependency_hash() - return True - return False class WinetricksManagerDialog(QDialog): """Диалог для управления компонентами Winetricks.""" @@ -5201,38 +4814,35 @@ def main(): # На всякий случай удаляем старый файл сокета, если он остался от сбоя. QLocalServer.removeServer(socket_name) - dependency_manager = DependencyManager() - if dependency_manager.run(): - window = WineHelperGUI() + window = WineHelperGUI() - # Создаем локальный сервер для приема "сигналов" от последующих запусков - server = QLocalServer(window) + # Создаем локальный сервер для приема "сигналов" от последующих запусков + server = QLocalServer(window) - # Функция для обработки входящих подключений - def handle_new_connection(): - client_socket = server.nextPendingConnection() - if client_socket: - # Ждем данные (не обязательно, но для надежности) - client_socket.waitForReadyRead(100) - client_socket.readAll() # Очищаем буфер - window.activate() - client_socket.close() + # Функция для обработки входящих подключений + def handle_new_connection(): + client_socket = server.nextPendingConnection() + if client_socket: + # Ждем данные (не обязательно, но для надежности) + client_socket.waitForReadyRead(100) + client_socket.readAll() # Очищаем буфер + window.activate() + client_socket.close() - server.newConnection.connect(handle_new_connection) + server.newConnection.connect(handle_new_connection) - # Начинаем слушать. Если не удалось, программа все равно будет работать, - # но без функции активации существующего окна. - if not server.listen(socket_name): - print(f"Предупреждение: не удалось запустить сервер {socket_name}: {server.errorString()}") + # Начинаем слушать. Если не удалось, программа все равно будет работать, + # но без функции активации существующего окна. + if not server.listen(socket_name): + print(f"Предупреждение: не удалось запустить сервер {socket_name}: {server.errorString()}") - # Сохраняем ссылку на сервер, чтобы он не был удален сборщиком мусора - window.server = server - window.show() - # Создаем иконку в системном трее после создания окна - # window.create_tray_icon() # Временно отключено - return app.exec_() + # Сохраняем ссылку на сервер, чтобы он не был удален сборщиком мусора + window.server = server + window.show() + # Создаем иконку в системном трее после создания окна + # window.create_tray_icon() # Временно отключено + return app.exec_() - return 1 if __name__ == "__main__": sys.exit(main())