saikyo-packages-src/saikyo-license/bin/saikyo-license-gui

574 lines
19 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
SAIKYO OS License Activator - Modern GUI
Beautiful activation interface with dark theme
"""
import sys
import os
import subprocess
import re
from datetime import datetime
try:
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QPushButton, QLineEdit, QTextEdit, QFrame, QMessageBox,
QFileDialog, QStackedWidget, QGroupBox, QSpacerItem, QSizePolicy
)
from PyQt6.QtCore import Qt, QTimer, QSize
from PyQt6.QtGui import QFont, QPixmap, QIcon, QPalette, QColor
PYQT_VERSION = 6
except ImportError:
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QPushButton, QLineEdit, QTextEdit, QFrame, QMessageBox,
QFileDialog, QStackedWidget, QGroupBox, QSpacerItem, QSizePolicy
)
from PyQt5.QtCore import Qt, QTimer, QSize
from PyQt5.QtGui import QFont, QPixmap, QIcon, QPalette, QColor
PYQT_VERSION = 5
# Saikyo OS color scheme
COLORS = {
'bg_dark': '#0a0a0a',
'bg_card': '#1a1a1a',
'bg_input': '#252525',
'accent': '#00ff66',
'accent_hover': '#00cc52',
'text': '#ffffff',
'text_secondary': '#888888',
'border': '#333333',
'success': '#00ff66',
'warning': '#ffaa00',
'error': '#ff4444',
}
STYLESHEET = f"""
QMainWindow, QWidget {{
background-color: {COLORS['bg_dark']};
color: {COLORS['text']};
font-family: 'Segoe UI', 'Ubuntu', sans-serif;
}}
QLabel {{
color: {COLORS['text']};
background: transparent;
}}
QLabel#title {{
font-size: 28px;
font-weight: bold;
color: {COLORS['accent']};
}}
QLabel#subtitle {{
font-size: 14px;
color: {COLORS['text_secondary']};
}}
QLabel#status_label {{
font-size: 16px;
font-weight: bold;
padding: 10px;
border-radius: 8px;
}}
QLabel#status_active {{
background-color: rgba(0, 255, 102, 0.15);
color: {COLORS['success']};
border: 1px solid {COLORS['success']};
}}
QLabel#status_trial {{
background-color: rgba(255, 170, 0, 0.15);
color: {COLORS['warning']};
border: 1px solid {COLORS['warning']};
}}
QLabel#status_expired, QLabel#status_missing {{
background-color: rgba(255, 68, 68, 0.15);
color: {COLORS['error']};
border: 1px solid {COLORS['error']};
}}
QPushButton {{
background-color: {COLORS['bg_card']};
color: {COLORS['text']};
border: 1px solid {COLORS['border']};
border-radius: 8px;
padding: 12px 24px;
font-size: 14px;
font-weight: 500;
min-width: 120px;
}}
QPushButton:hover {{
background-color: {COLORS['bg_input']};
border-color: {COLORS['accent']};
}}
QPushButton:pressed {{
background-color: {COLORS['accent']};
color: {COLORS['bg_dark']};
}}
QPushButton#primary {{
background-color: {COLORS['accent']};
color: {COLORS['bg_dark']};
border: none;
font-weight: bold;
}}
QPushButton#primary:hover {{
background-color: {COLORS['accent_hover']};
}}
QLineEdit, QTextEdit {{
background-color: {COLORS['bg_input']};
color: {COLORS['text']};
border: 1px solid {COLORS['border']};
border-radius: 6px;
padding: 10px;
font-size: 14px;
}}
QLineEdit:focus, QTextEdit:focus {{
border-color: {COLORS['accent']};
}}
QGroupBox {{
background-color: {COLORS['bg_card']};
border: 1px solid {COLORS['border']};
border-radius: 12px;
margin-top: 16px;
padding: 20px;
font-size: 14px;
font-weight: bold;
}}
QGroupBox::title {{
subcontrol-origin: margin;
left: 20px;
padding: 0 10px;
color: {COLORS['text_secondary']};
}}
QFrame#separator {{
background-color: {COLORS['border']};
max-height: 1px;
}}
QFrame#card {{
background-color: {COLORS['bg_card']};
border: 1px solid {COLORS['border']};
border-radius: 12px;
padding: 20px;
}}
"""
class LicenseManager:
"""Interface to saikyo-license CLI"""
LICENSE_CMD = '/usr/sbin/saikyo-license'
@classmethod
def run_cmd(cls, *args):
try:
result = subprocess.run(
[cls.LICENSE_CMD] + list(args),
capture_output=True, text=True, timeout=30
)
return result.returncode, result.stdout, result.stderr
except Exception as e:
return -1, '', str(e)
@classmethod
def run_privileged(cls, *args):
try:
result = subprocess.run(
['pkexec', cls.LICENSE_CMD] + list(args),
capture_output=True, text=True, timeout=60
)
return result.returncode, result.stdout, result.stderr
except Exception as e:
return -1, '', str(e)
@classmethod
def get_status(cls):
code, out, err = cls.run_cmd('status')
status = {
'status': 'unknown',
'trial_days': '',
'trial_remaining_seconds': '0',
'valid_until': '',
'expired': 'no',
'org_name': '',
'tier': '',
'machine_id': '',
}
for line in out.split('\n'):
if '=' in line:
k, v = line.split('=', 1)
k = k.strip().replace('-', '_')
status[k] = v.strip()
return status
@classmethod
def verify(cls):
code, _, _ = cls.run_cmd('verify')
return code == 0
@classmethod
def install_code(cls, code):
return cls.run_privileged('install-code', code)
@classmethod
def install_files(cls, json_path, sig_path):
return cls.run_privileged('install', json_path, sig_path)
@classmethod
def activate_online(cls, token, server='https://saikyo-os.ru/license'):
return cls.run_privileged('activate', '--token', token, '--server', server)
@classmethod
def get_machine_id(cls):
try:
with open('/etc/machine-id', 'r') as f:
return f.read().strip()
except:
return 'Не удалось прочитать'
def human_duration(seconds):
try:
s = int(seconds)
except:
return ''
if s <= 0:
return ''
d = s // 86400
h = (s % 86400) // 3600
m = (s % 3600) // 60
if d > 0:
return f'{d}д {h}ч {m}м'
elif h > 0:
return f'{h}ч {m}м'
else:
return f'{m}м'
class StatusCard(QFrame):
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName('card')
self.setup_ui()
def setup_ui(self):
layout = QVBoxLayout(self)
layout.setSpacing(16)
# Status badge
self.status_label = QLabel('Загрузка...')
self.status_label.setObjectName('status_label')
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter if PYQT_VERSION == 6 else Qt.AlignCenter)
layout.addWidget(self.status_label)
# Info grid
info_layout = QVBoxLayout()
info_layout.setSpacing(8)
self.org_label = self._create_info_row('Организация:', '')
self.tier_label = self._create_info_row('Тариф:', '')
self.remaining_label = self._create_info_row('Осталось:', '')
self.valid_until_label = self._create_info_row('Действует до:', '')
self.machine_id_label = self._create_info_row('Machine ID:', '')
info_layout.addLayout(self.org_label[0])
info_layout.addLayout(self.tier_label[0])
info_layout.addLayout(self.remaining_label[0])
info_layout.addLayout(self.valid_until_label[0])
info_layout.addLayout(self.machine_id_label[0])
layout.addLayout(info_layout)
def _create_info_row(self, label_text, value_text):
layout = QHBoxLayout()
label = QLabel(label_text)
label.setStyleSheet(f'color: {COLORS["text_secondary"]}; font-size: 13px;')
value = QLabel(value_text)
value.setStyleSheet('font-size: 13px; font-weight: 500;')
value.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse if PYQT_VERSION == 6 else Qt.TextSelectableByMouse)
layout.addWidget(label)
layout.addStretch()
layout.addWidget(value)
return (layout, value)
def update_status(self, status_data):
status = status_data.get('status', 'unknown')
# Update status badge
if status == 'trial':
self.status_label.setText('⏱ ПРОБНЫЙ ПЕРИОД')
self.status_label.setObjectName('status_trial')
elif status == 'missing':
self.status_label.setText('НЕ АКТИВИРОВАНА')
self.status_label.setObjectName('status_missing')
elif status_data.get('expired') == 'yes':
self.status_label.setText('✗ ИСТЕКЛА')
self.status_label.setObjectName('status_expired')
elif LicenseManager.verify():
self.status_label.setText('✓ АКТИВНА')
self.status_label.setObjectName('status_active')
else:
self.status_label.setText('? НЕИЗВЕСТНО')
self.status_label.setObjectName('status_missing')
# Force style refresh
self.status_label.setStyleSheet(self.status_label.styleSheet())
# Update info
self.org_label[1].setText(status_data.get('org_name') or '')
self.tier_label[1].setText(status_data.get('tier') or '')
# Calculate remaining time
if status == 'trial':
remaining = human_duration(status_data.get('trial_remaining_seconds', 0))
elif status_data.get('valid_until'):
try:
exp = datetime.fromisoformat(status_data['valid_until'].replace('Z', '+00:00'))
now = datetime.now(exp.tzinfo) if exp.tzinfo else datetime.now()
delta = (exp - now).total_seconds()
remaining = human_duration(delta) if delta > 0 else 'Истекла'
except:
remaining = ''
else:
remaining = ''
self.remaining_label[1].setText(remaining)
self.valid_until_label[1].setText(status_data.get('valid_until') or '')
self.machine_id_label[1].setText(LicenseManager.get_machine_id()[:16] + '...')
class ActivationWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setup_ui()
def setup_ui(self):
layout = QVBoxLayout(self)
layout.setSpacing(20)
# Online activation
online_group = QGroupBox('Онлайн активация')
online_layout = QVBoxLayout(online_group)
self.token_input = QLineEdit()
self.token_input.setPlaceholderText('Введите токен активации...')
online_layout.addWidget(self.token_input)
self.activate_btn = QPushButton('Активировать')
self.activate_btn.setObjectName('primary')
self.activate_btn.clicked.connect(self.activate_online)
online_layout.addWidget(self.activate_btn)
layout.addWidget(online_group)
# Offline activation
offline_group = QGroupBox('Оффлайн активация (response-код)')
offline_layout = QVBoxLayout(offline_group)
self.code_input = QTextEdit()
self.code_input.setPlaceholderText('Вставьте response-код (SAI-RC:... или SAI-RESP2:...)')
self.code_input.setMaximumHeight(100)
offline_layout.addWidget(self.code_input)
self.offline_btn = QPushButton('Активировать оффлайн')
self.offline_btn.clicked.connect(self.activate_offline)
offline_layout.addWidget(self.offline_btn)
layout.addWidget(offline_group)
# File installation
file_group = QGroupBox('Установка из файлов')
file_layout = QVBoxLayout(file_group)
file_hint = QLabel('Выберите license.json и license.sig')
file_hint.setStyleSheet(f'color: {COLORS["text_secondary"]}; font-size: 12px;')
file_layout.addWidget(file_hint)
self.file_btn = QPushButton('Выбрать файлы...')
self.file_btn.clicked.connect(self.install_from_files)
file_layout.addWidget(self.file_btn)
layout.addWidget(file_group)
layout.addStretch()
def activate_online(self):
token = self.token_input.text().strip()
if not token:
QMessageBox.warning(self, 'Ошибка', 'Введите токен активации')
return
code, out, err = LicenseManager.activate_online(token)
if code == 0:
QMessageBox.information(self, 'Успех', 'Лицензия успешно активирована!')
self.token_input.clear()
self.parent().parent().refresh_status()
else:
QMessageBox.critical(self, 'Ошибка', f'Ошибка активации:\n{err or out}')
def activate_offline(self):
code = self.code_input.toPlainText().strip()
if not code:
QMessageBox.warning(self, 'Ошибка', 'Вставьте response-код')
return
ret, out, err = LicenseManager.install_code(code)
if ret == 0:
QMessageBox.information(self, 'Успех', 'Лицензия успешно установлена!')
self.code_input.clear()
self.parent().parent().refresh_status()
else:
QMessageBox.critical(self, 'Ошибка', f'Ошибка установки:\n{err or out}')
def install_from_files(self):
json_path, _ = QFileDialog.getOpenFileName(
self, 'Выберите license.json', os.path.expanduser('~'),
'JSON files (*.json);;All files (*)'
)
if not json_path:
return
sig_path, _ = QFileDialog.getOpenFileName(
self, 'Выберите license.sig', os.path.dirname(json_path),
'Signature files (*.sig);;All files (*)'
)
if not sig_path:
return
ret, out, err = LicenseManager.install_files(json_path, sig_path)
if ret == 0:
QMessageBox.information(self, 'Успех', 'Лицензия успешно установлена!')
self.parent().parent().refresh_status()
else:
QMessageBox.critical(self, 'Ошибка', f'Ошибка установки:\n{err or out}')
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle('SAIKYO OS — Активация лицензии')
self.setMinimumSize(500, 650)
self.setMaximumSize(600, 800)
# Set window icon
icon_path = '/usr/share/icons/hicolor/512x512/apps/saikyo-activation-logo.png'
if os.path.exists(icon_path):
self.setWindowIcon(QIcon(icon_path))
self.setup_ui()
self.refresh_status()
def setup_ui(self):
central = QWidget()
self.setCentralWidget(central)
layout = QVBoxLayout(central)
layout.setContentsMargins(30, 30, 30, 30)
layout.setSpacing(20)
# Header
header_layout = QVBoxLayout()
header_layout.setSpacing(8)
# Logo
logo_path = '/usr/share/icons/hicolor/512x512/apps/saikyo-activation-logo.png'
if os.path.exists(logo_path):
logo_label = QLabel()
pixmap = QPixmap(logo_path).scaled(
80, 80,
Qt.AspectRatioMode.KeepAspectRatio if PYQT_VERSION == 6 else Qt.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation if PYQT_VERSION == 6 else Qt.SmoothTransformation
)
logo_label.setPixmap(pixmap)
logo_label.setAlignment(Qt.AlignmentFlag.AlignCenter if PYQT_VERSION == 6 else Qt.AlignCenter)
header_layout.addWidget(logo_label)
title = QLabel('SAIKYO OS')
title.setObjectName('title')
title.setAlignment(Qt.AlignmentFlag.AlignCenter if PYQT_VERSION == 6 else Qt.AlignCenter)
header_layout.addWidget(title)
subtitle = QLabel('Активация лицензии')
subtitle.setObjectName('subtitle')
subtitle.setAlignment(Qt.AlignmentFlag.AlignCenter if PYQT_VERSION == 6 else Qt.AlignCenter)
header_layout.addWidget(subtitle)
layout.addLayout(header_layout)
# Status card
self.status_card = StatusCard()
layout.addWidget(self.status_card)
# Activation widget
self.activation_widget = ActivationWidget(self)
layout.addWidget(self.activation_widget)
# Footer buttons
footer_layout = QHBoxLayout()
refresh_btn = QPushButton('Обновить')
refresh_btn.clicked.connect(self.refresh_status)
footer_layout.addWidget(refresh_btn)
portal_btn = QPushButton('Открыть портал')
portal_btn.clicked.connect(self.open_portal)
footer_layout.addWidget(portal_btn)
close_btn = QPushButton('Закрыть')
close_btn.clicked.connect(self.close)
footer_layout.addWidget(close_btn)
layout.addLayout(footer_layout)
def refresh_status(self):
status = LicenseManager.get_status()
self.status_card.update_status(status)
def open_portal(self):
try:
subprocess.Popen(['xdg-open', 'https://saikyo-os.ru/license'],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except:
pass
def main():
app = QApplication(sys.argv)
app.setStyle('Fusion')
app.setStyleSheet(STYLESHEET)
# Dark palette
palette = QPalette()
palette.setColor(QPalette.ColorRole.Window if PYQT_VERSION == 6 else QPalette.Window, QColor(COLORS['bg_dark']))
palette.setColor(QPalette.ColorRole.WindowText if PYQT_VERSION == 6 else QPalette.WindowText, QColor(COLORS['text']))
palette.setColor(QPalette.ColorRole.Base if PYQT_VERSION == 6 else QPalette.Base, QColor(COLORS['bg_input']))
palette.setColor(QPalette.ColorRole.Text if PYQT_VERSION == 6 else QPalette.Text, QColor(COLORS['text']))
palette.setColor(QPalette.ColorRole.Button if PYQT_VERSION == 6 else QPalette.Button, QColor(COLORS['bg_card']))
palette.setColor(QPalette.ColorRole.ButtonText if PYQT_VERSION == 6 else QPalette.ButtonText, QColor(COLORS['text']))
palette.setColor(QPalette.ColorRole.Highlight if PYQT_VERSION == 6 else QPalette.Highlight, QColor(COLORS['accent']))
app.setPalette(palette)
window = MainWindow()
window.show()
sys.exit(app.exec() if PYQT_VERSION == 6 else app.exec_())
if __name__ == '__main__':
main()