forked from CastroFidel/winehelper
		
	added system dependency check
This commit is contained in:
		| @@ -28,6 +28,396 @@ class Var: | ||||
|     LICENSE_FILE = os.environ.get("LICENSE_FILE") | ||||
|     LICENSE_AGREEMENT_FILE = os.environ.get("AGREEMENT") | ||||
|  | ||||
| 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.""" | ||||
|         winehelper_script_path = os.environ.get("RUN_SCRIPT") | ||||
|         if winehelper_script_path and os.path.exists(winehelper_script_path): | ||||
|             return os.path.join(os.path.dirname(winehelper_script_path), 'dependencies.sh') | ||||
|         return None | ||||
|  | ||||
|     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 требуются дополнительные системные компоненты.<br><br>" | ||||
|             "Отсутствуют следующие пакеты:<br>" | ||||
|             # Ограничиваем высоту блока с пакетами и добавляем прокрутку, если список длинный. | ||||
|             f"""<div style="font-family: monospace; max-height: 150px; overflow-y: auto; margin-top: 5px; margin-bottom: 10px;"> | ||||
|             {'<br>'.join(sorted(missing_packages))} | ||||
|             </div>""" | ||||
|             "Нажмите <b>'Установить'</b>, чтобы запустить установку с правами администратора. " | ||||
|             "Вам потребуется ввести пароль. Этот процесс может занять некоторое время." | ||||
|         ) | ||||
|         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<b><font color='green'>=== Установка успешно завершена ===</font></b>") | ||||
|                 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: | ||||
|                     info_box = QMessageBox(dialog) | ||||
|                     info_box.setWindowIcon(self.app_icon) | ||||
|                     info_box.setIcon(QMessageBox.Information) | ||||
|                     info_box.setWindowTitle("Успех") | ||||
|                     info_box.setText("Все необходимые зависимости были успешно установлены.") | ||||
|                     info_box.exec_() | ||||
|                     installation_successful = True | ||||
|                 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: | ||||
|                 log_tag = "ПРЕРВАНО" if exit_status == QProcess.CrashExit else "ОШИБКА" | ||||
|                 log_output.append(f"\n<b><font color='red'>=== {log_tag} (код: {exit_code}) ===</font></b>") | ||||
|                 log_output.ensureCursorVisible() | ||||
|  | ||||
|             close_button.setEnabled(True) | ||||
|  | ||||
|         def dialog_close_handler(event): | ||||
|             """Обрабатывает закрытие окна во время установки зависимостей.""" | ||||
|             if process.state() == QProcess.Running: | ||||
|                 msg_box = QMessageBox(dialog) | ||||
|                 msg_box.setMinimumWidth(900) | ||||
|                 msg_box.setIcon(QMessageBox.Question) | ||||
|                 msg_box.setWindowTitle("Прервать установку?") | ||||
|                 msg_box.setText( | ||||
|                     "<p style='white-space: pre;'>Установка зависимостей еще не завершена.</p>" | ||||
|                     "<p style='white-space: pre;'>Вы действительно хотите прервать процесс?</p>" | ||||
|                     "<p style='white-space: pre;'>Это закроет только окно программы.<br>" | ||||
|                     "Процесс установки зависимостей будет продолжен.<p>" | ||||
|                 ) | ||||
|  | ||||
|                 yes_button = msg_box.addButton("Да, прервать", QMessageBox.YesRole) | ||||
|                 no_button = msg_box.addButton("Нет", QMessageBox.NoRole) | ||||
|                 msg_box.setDefaultButton(no_button) | ||||
|  | ||||
|                 msg_box.exec_() | ||||
|  | ||||
|                 if msg_box.clickedButton() == yes_button: | ||||
|                     process.readyRead.disconnect() | ||||
|                     process.finished.disconnect() | ||||
|                     process.terminate() | ||||
|                     event.accept() | ||||
|                 else: | ||||
|                     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.""" | ||||
|  | ||||
| @@ -542,7 +932,7 @@ class WineHelperGUI(QMainWindow): | ||||
|     def __init__(self): | ||||
|         super().__init__() | ||||
|         self.setWindowTitle("WineHelper") | ||||
|         self.setGeometry(100, 100, 950, 500) | ||||
|         self.setMinimumSize(950, 500) | ||||
|  | ||||
|         if Var.WH_ICON_PATH and os.path.exists(Var.WH_ICON_PATH): | ||||
|             self.setWindowIcon(QIcon(Var.WH_ICON_PATH)) | ||||
| @@ -2258,17 +2648,23 @@ class WineHelperGUI(QMainWindow): | ||||
|         if hasattr(self, 'install_process') and self.install_process: | ||||
|             if self.install_process.state() == QProcess.Running: | ||||
|                 self.install_process.terminate() | ||||
|                 # Даем процессу 3 секунды на завершение | ||||
|                 if not self.install_process.waitForFinished(3000): | ||||
|                     self.append_log("Процесс не ответил на terminate, отправляется kill...", is_error=True) | ||||
|                     self.install_process.kill() | ||||
|                     self.install_process.waitForFinished()  # Ждем завершения после kill | ||||
|                     self.install_process.waitForFinished() | ||||
|             self.install_process.deleteLater() | ||||
|             self.install_process = None | ||||
|  | ||||
| def main(): | ||||
|     """Основная точка входа в приложение.""" | ||||
|     app = QApplication(sys.argv) | ||||
|  | ||||
|     dependency_manager = DependencyManager() | ||||
|     if dependency_manager.run(): | ||||
|         window = WineHelperGUI() | ||||
|         window.show() | ||||
|         return app.exec_() | ||||
|     return 1 | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     app = QApplication(sys.argv) | ||||
|     window = WineHelperGUI() | ||||
|     window.show() | ||||
|     sys.exit(app.exec_()) | ||||
|     sys.exit(main()) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user