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