36 Commits

Author SHA1 Message Date
11253286f8 Improved configuration file management with advanced features
- Added send2trash library for safer file deletion
- Implemented folder and file moving functionality in ConfigSelectorWindow
- Created FolderSelectorDialog for moving files to a folder
- Improved file and folder renaming with improved error handling
- Added emoji icons for better visual representation of actions
- Updated requirements.txt to include send2trash library
- Removed drag-and-drop support. Postponed for future
2025-02-19 23:06:15 +03:00
ea432d2893 Add configuration file selection and management window
- Implement ConfigSelectorWindow for advanced configuration file selection
- Add drag-and-drop support for configuration files
- Create file management features: add, edit, rename, and delete configs
- Enhance file selection process with a tree-view interface
- Improve configuration file handling with user-friendly interactions
2025-02-19 22:25:28 +03:00
d3f832cdbb Add connection state verification in command execution
- Check serial connection status before sending commands
- Prevent command execution on closed or inactive connections
- Add informative log message when connection is lost
- Enhance robustness of command sending mechanism
2025-02-19 21:51:55 +03:00
7d92682482 Enhance terminal widgets with read-only mode and clear functionality
- Add read-only mode to TerminalWidget to prevent manual text editing
- Disable text paste and key input in terminal widgets
- Modify context menu to remove cut and paste options
- Add clear buttons for interactive, file execution, and TFTP log terminals
- Implement temporary state changes for text appending and clearing
2025-02-19 21:49:49 +03:00
0ebcf9cb6a Enhance command execution with robust error handling and connection monitoring
- Add consecutive error tracking and connection state checks
- Implement automatic pause and user notification for persistent connection issues
- Improve block and line mode execution with enhanced error detection
- Modify logging and status reporting for better visibility of execution problems
- Add connection verification before and during command execution
2025-02-19 21:39:58 +03:00
99e9163760 Add advanced port monitoring and execution progress tracking
- Implement real-time COM port state monitoring with automatic reconnection
- Add connection indicator with dynamic status updates
- Create progress bar and timer for file command execution
- Enhance execution control with detailed progress tracking
- Implement thread-safe port monitoring and execution mechanisms
- Improve error handling and user feedback during command execution
2025-02-19 21:23:47 +03:00
b573de9166 Add advanced file execution control with pause and stop functionality
- Implement multi-stage command execution from file
- Add start, pause, and stop buttons for file command execution
- Create thread-safe execution control with pause and stop mechanisms
- Enhance error handling and user feedback during file command execution
- Improve command execution flow with step-by-step processing
2025-02-19 21:04:38 +03:00
f45f1bf556 Improve command logging and terminal display formatting
- Simplify command logging messages with concise `[CMD]` prefix
- Remove redundant command attempt counter from log messages
- Modify TerminalWidget to handle `[CMD]` tagged messages
- Enhance command display with consistent formatting and separators
2025-02-19 20:56:25 +03:00
e6766660c6 Add Enter key support for command input
- Bind Enter key to send command in interactive tab
2025-02-18 20:10:51 +03:00
e0705ad6b5 Add advanced TerminalWidget with enhanced text display and formatting
- Implement TerminalWidget class with color-coded message types
- Add support for error, warning, info, and command message styles
- Include automatic scrollbar and text formatting
- Replace CustomText with TerminalWidget in interactive tabs
- Enhance text appending methods with type-specific display
- Improve terminal readability with command separators
2025-02-18 00:35:14 +03:00
c0d8fd8d89 Improve interactive tab buttons with Unicode icons
- Add Unicode icons to the connect, disconnect and send buttons in the interactive tab
2025-02-18 00:11:42 +03:00
ce81100150 Enhance status bar with TFTP server indicator
- Add TFTP server status indicator to the status bar
- Implement tooltip for TFTP server status
- Update status bar update method to reflect TFTP server state
- Improve status bar layout with separate frames for indicators
2025-02-17 19:39:29 +03:00
48c9bd2d40 Bump version to 1.0.2
- Update application version number in ComConfigCopy.py
- Minor version increment to reflect recent changes
2025-02-17 18:35:58 +03:00
e0f64060f5 Add status bar with connection and settings information
- Implement create_status_bar method to add a new status bar
- Add connection indicator with color-coded status
- Create tooltip for connection status details
- Update status bar with current port, baudrate, and copy mode settings
- Integrate status bar updates in connection and settings change events
2025-02-17 18:31:58 +03:00
f5935d6b8f Merge pull request 'v1.0.1' (#4) from v1.0.1 into main
Reviewed-on: #4
2025-02-16 22:02:20 +00:00
1a511ff54f Enhance update checking
- Refactor UpdateChecker
- Add support for parsing release details with improved formatting
- Implement more robust version comparison and release type handling
- Add logging for update checking process
- Improve error handling and release information extraction
- Update update checking logic to handle stable and pre-release versions
2025-02-17 00:53:24 +03:00
f84e20631b Update contact email in README.md
- Change contact email from LowaWorkMail@gmail.com to SPRF555@gmail.com
2025-02-17 00:11:10 +03:00
4a67e70a92 Обновить README.md 2025-02-16 20:52:02 +00:00
12562e615f Bump version to 1.0.1
- Update application version number in ComConfigCopy.py
- Minor version increment to reflect recent changes
2025-02-16 23:48:59 +03:00
7ebeb52808 Refactor and optimize code structure with base widget and utility functions
- Create CustomWidgetBase for shared context menu and shortcut functionality
- Add utility functions for common tasks like text appending and file selection
- Simplify command processing with a generic send_command_and_process_response function
- Remove redundant comments and unused imports
- Improve code organization and readability
- Enhance modularity of custom widgets and utility methods
2025-02-16 23:45:19 +03:00
a140b7d8a0 Clean up .gitignore file
- Remove specific config and test files from tracking
- Add Configs/ directory to ignore list
- Remove unnecessary icon and test script entries
2025-02-16 05:36:53 +03:00
2c9edcd859 Remove outdated ComConfigCopy executable binary 2025-02-16 05:35:35 +03:00
5a00efd175 Update executable binary after feature removal 2025-02-16 05:08:44 +03:00
2f4b2985cd Remove documentation and CLI mode features
- Remove "Documentation" menu option
- Delete unused CLI mode code
- Clean up commented-out argument parsing function
2025-02-16 05:00:43 +03:00
d1a870fed7 Add update checking and documentation features
- Implement UpdateChecker for version comparison and update notifications
- Add menu options for documentation and update checking
- Enhance AboutWindow with dynamic version display
- Update requirements.txt with new dependencies
- Create infrastructure for opening local documentation
- Improve application menu with additional help options
2025-02-16 04:50:33 +03:00
2e2dd9e705 Add custom text and entry widgets with enhanced copy/paste functionality
- Implement CustomText and CustomEntry classes with advanced text interaction features
- Add context menu for text widgets with cut, copy, paste, and select all options
- Support multiple keyboard shortcuts for text manipulation
- Replace standard Tkinter Text and Entry widgets with custom implementations
- Remove global text/entry widget bindings in favor of class-specific methods
2025-02-16 03:57:48 +03:00
d937042ea2 Update application title to reflect project name
- Change window title from "Serial Device Manager" to "ComConfigCopy"
2025-02-16 03:53:21 +03:00
136c7877d3 Refactor TFTP file transfer with improved reliability and error handling
- Implement more robust file transfer mechanism with configurable retry and timeout settings
- Add detailed logging for transfer progress and error scenarios
- Enhance block transfer logic with better error recovery
- Simplify transfer socket management and cleanup process
- Improve overall transfer reliability and error tracking
2025-02-16 03:50:27 +03:00
467d582095 Improve file transfer progress tracking and display
- Add dynamic transfer speed calculation
- Compute and display estimated remaining transfer time
- Enhance remaining bytes display with more informative status
- Update transfers table with more detailed transfer progress information
2025-02-16 03:43:34 +03:00
16526b4643 Merge pull request 'TFTP' (#3) from TFTP into main
Reviewed-on: #3
2025-02-16 00:39:43 +00:00
6d2819a860 Add network adapter selection for TFTP server
- Implement `get_network_adapters()` function to dynamically retrieve available network interfaces
- Replace IP address entry with a combobox for network adapter selection
- Add "Update" button to refresh network adapter list
- Improve IP address validation and error handling for TFTP server configuration
- Enhance UI with more user-friendly network interface selection
2025-02-16 03:34:57 +03:00
a252a0f153 Prevent duplicate TFTP server log messages and improve state tracking
- Add filtering mechanism to prevent repeated server start/stop log entries
- Implement state tracking flags to manage server status
- Remove redundant log messages in both ComConfigCopy.py and TFTPServer.py
- Enhance log callback to avoid unnecessary logging of server state changes
2025-02-16 03:31:32 +03:00
3126811f09 Improve TFTP server shutdown and error handling
- Enhance server stop mechanism with more robust socket and thread management
- Add better handling of active transfers during server shutdown
- Implement additional safety checks and timeout handling
- Improve logging and error reporting for server stop process
- Prevent potential deadlocks and resource leaks during server termination
2025-02-16 03:28:53 +03:00
f1ca31c198 Enhance TFTP server implementation with advanced monitoring and UI improvements
- Completely refactor TFTP server implementation with more robust file transfer handling
- Add detailed transfer tracking with active transfers table
- Implement periodic transfer status updates
- Improve log and UI layout for TFTP server tab
- Add more granular error handling and logging
- Enhance threading and socket management for file transfers
2025-02-16 03:19:44 +03:00
c95915483f Update .gitignore to include virtual environment directory
- Add '.venv/' to .gitignore to exclude Python virtual environment files
2025-02-16 03:09:23 +03:00
299ce329f7 Add TFTP server functionality to the application
- Implement TFTP server tab with IP and port configuration
- Create methods to start and stop TFTP server
- Add logging functionality for TFTP server events
- Integrate TFTPServer class into the main application
- Re-enable Firmware directory creation
2025-02-16 02:51:47 +03:00
9 changed files with 2411 additions and 278 deletions

5
.gitignore vendored
View File

@@ -1,9 +1,6 @@
app.log
Settings/settings.json
Configs/Eltex MES2424 AC - Сеть FTTB 2G, доп.txt
Configs/конфиг доп 3750-52 с айпи 172.17.141.133 .txt
DALL·E 2024-12-29 01.01.02 - Square vector logo_ A clean and minimalistic app icon for serial port management software. The design prominently features a simplified rectangular CO.ico
test.py
Configs/
__pycache__/
Firmware/1.jpg
Firmware/2

File diff suppressed because it is too large Load Diff

BIN
Icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

44
README.md Normal file
View File

@@ -0,0 +1,44 @@
# ComConfigCopy
Программа для копирования конфигураций на коммутаторы.
## Описание
ComConfigCopy - это утилита, разработанная для автоматизации процесса копирования конфигураций на сетевые коммутаторы. Программа предоставляет удобный графический интерфейс для управления процессом копирования и настройки параметров подключения.
## Основные возможности
- Копирование конфигураций на коммутаторы через COM-порт
- Поддержка различных скоростей подключения
- Автоматическое определение доступных COM-портов
- Возможность сохранения и загрузки настроек
- Автоматическое обновление через GitHub
## Системные требования
- Windows 7/8/10/11
- Python 3.8 или выше
- Доступ к COM-портам
## Установка
1. Скачайте последнюю версию программы из [репозитория](https://gitea.filow.ru/LowaSC/ComConfigCopy/releases)
2. Распакуйте архив в удобное место
3. Запустите файл `ComConfigCopy.exe`
## Использование
1. Выберите COM-порт из списка доступных
2. Настройте параметры подключения (скорость, биты данных и т.д.)
3. Выберите файл конфигурации для отправки
4. Нажмите кнопку "Отправить" для начала процесса копирования
## Контакты
- Email: SPRF555@gmail.com
- Telegram: [@LowaSC](https://t.me/LowaSC)
- Репозиторий: [ComConfigCopy](https://gitea.filow.ru/LowaSC/ComConfigCopy)
## Лицензия
Этот проект распространяется под лицензией MIT. Подробности смотрите в файле [LICENSE](LICENSE).

349
TFTPServer.py Normal file
View File

@@ -0,0 +1,349 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
TFTP сервер для передачи прошивки с компьютера на коммутатор.
- Создает сервер по заданному IP и порту.
- Расшаривает папку Firmware.
- Показывает текущее состояние сервера и статус передачи файла:
- кому (IP устройства),
- сколько осталось байт,
- сколько передано байт,
- время передачи.
"""
import os
import socket
import struct
import threading
import time
class TFTPServer:
def __init__(self, share_folder):
"""
Инициализация TFTP сервера.
:param share_folder: Путь к папке, содержащей файлы (например, папка 'Firmware')
"""
self.share_folder = share_folder
self.log_callback = None
self.running = False
self.server_socket = None
self.lock = threading.Lock()
self.transfer_sockets = set() # Множество для хранения всех активных сокетов передачи
# Словарь активных передач для мониторинга их статуса.
# Ключ адрес клиента, значение словарь с информацией о передаче.
self.active_transfers = {}
def set_log_callback(self, callback):
"""
Установка функции обратного вызова для логирования сообщений.
:param callback: Функция, принимающая строку сообщения.
"""
self.log_callback = callback
def log(self, message):
"""
Функция логирования: вызывает callback (если задан) или выводит сообщение в консоль.
:param message: Строка с сообщением для логирования.
"""
if self.log_callback:
self.log_callback(message)
else:
print(message)
def start_server(self, ip, port):
"""
Запуск TFTP сервера на указанном IP и порту.
:param ip: IP-адрес для привязки сервера.
:param port: Порт для TFTP сервера.
"""
if self.running:
self.log("[WARN] Сервер уже запущен")
return
self.running = True
try:
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.server_socket.bind((ip, port))
self.log(f"[INFO] TFTP сервер запущен на {ip}:{port}")
while self.running:
try:
self.server_socket.settimeout(1.0)
data, client_addr = self.server_socket.recvfrom(2048)
if data and self.running:
threading.Thread(target=self.handle_request, args=(data, client_addr), daemon=True).start()
except socket.timeout:
continue
except socket.error as e:
if self.running: # Логируем ошибку только если сервер еще запущен
self.log(f"[ERROR] Ошибка получения данных: {str(e)}")
break
except Exception as e:
if self.running: # Логируем ошибку только если сервер еще запущен
self.log(f"[ERROR] Ошибка получения данных: {str(e)}")
break
except Exception as e:
self.log(f"[ERROR] Ошибка запуска сервера: {str(e)}")
finally:
self.running = False
if self.server_socket:
try:
self.server_socket.close()
except:
pass
self.server_socket = None
def stop_server(self):
"""
Остановка TFTP сервера.
"""
if not self.running:
return
self.log("[INFO] Остановка TFTP сервера...")
self.running = False
try:
# Закрываем основной сокет сервера первым
if self.server_socket:
try:
# Создаем временный сокет и отправляем пакет самому себе,
# чтобы разблокировать recvfrom
temp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
server_address = self.server_socket.getsockname()
temp_socket.sendto(b'', server_address)
except:
pass
finally:
try:
temp_socket.close()
except:
pass
try:
self.server_socket.close()
except Exception as e:
self.log(f"[WARN] Ошибка при закрытии основного сокета: {str(e)}")
finally:
self.server_socket = None
# Закрываем все активные сокеты передачи
with self.lock:
active_sockets = list(self.transfer_sockets)
self.transfer_sockets.clear()
active_transfers = dict(self.active_transfers)
self.active_transfers.clear()
# Закрываем сокеты передачи после очистки множества
for sock in active_sockets:
try:
if sock:
sock.close()
except Exception as e:
self.log(f"[WARN] Ошибка при закрытии сокета передачи: {str(e)}")
# Отправляем сообщения об остановке для активных передач
for client_addr, transfer_info in active_transfers.items():
try:
self.send_error(client_addr, 0, "Сервер остановлен")
except:
pass
except Exception as e:
self.log(f"[ERROR] Ошибка при остановке сервера: {str(e)}")
finally:
self.running = False # Гарантируем, что флаг running будет False
self.log("[INFO] TFTP сервер остановлен")
def handle_request(self, data, client_addr):
"""
Обработка входящего запроса от клиента.
:param data: Полученные данные (UDP-пакет).
:param client_addr: Адрес клиента, отправившего пакет.
"""
if len(data) < 2:
self.log(f"[WARN] Получен некорректный пакет от {client_addr}")
return
opcode = struct.unpack("!H", data[:2])[0]
if opcode == 1: # RRQ (Read Request) запрос на чтение файла
self.handle_rrq(data, client_addr)
else:
self.log(f"[WARN] Неподдерживаемый запрос (опкод {opcode}) от {client_addr}")
def handle_rrq(self, data, client_addr):
"""
Обработка запроса на чтение файла (RRQ).
:param data: Данные запроса.
:param client_addr: Адрес клиента.
"""
try:
# RRQ формата: 2 байта опкода, затем строка имени файла, за которой следует 0,
# затем строка режима (например, "octet"), и завершается 0.
parts = data[2:].split(b'\0')
if len(parts) < 2:
self.log(f"[WARN] Некорректный RRQ пакет от {client_addr}")
return
filename = parts[0].decode('utf-8')
mode = parts[1].decode('utf-8').lower()
self.log(f"[INFO] Получен RRQ от {client_addr}: файл '{filename}', режим '{mode}'")
if mode != "octet":
self.send_error(client_addr, 0, "Поддерживается только octet режим")
return
file_path = os.path.join(self.share_folder, filename)
if not os.path.isfile(file_path):
self.send_error(client_addr, 1, "Файл не найден")
return
# Запускаем передачу файла в новом потоке.
threading.Thread(target=self.send_file, args=(file_path, client_addr), daemon=True).start()
except Exception as e:
self.log(f"[ERROR] Ошибка обработки RRQ: {str(e)}")
def send_error(self, client_addr, error_code, error_message):
"""
Отправка сообщения об ошибке клиенту.
:param client_addr: Адрес клиента.
:param error_code: Код ошибки.
:param error_message: Текст ошибки.
"""
# Формируем TFTP пакет ошибки: 2 байта опкода (5), 2 байта кода ошибки, сообщение об ошибке и завершающий 0.
packet = struct.pack("!HH", 5, error_code) + error_message.encode('utf-8') + b'\0'
self.server_socket.sendto(packet, client_addr)
self.log(f"[INFO] Отправлено сообщение об ошибке '{error_message}' клиенту {client_addr}")
def send_file(self, file_path, client_addr):
"""
Передача файла клиенту по протоколу TFTP.
"""
BLOCK_SIZE = 512
MAX_RETRIES = 5
TIMEOUT = 2.0
transfer_socket = None
try:
if not os.path.exists(file_path):
self.log(f"[ERROR] Файл '{file_path}' не существует")
self.send_error(client_addr, 1, "Файл не найден")
return
filesize = os.path.getsize(file_path)
if filesize == 0:
self.log(f"[ERROR] Файл '{file_path}' пуст")
self.send_error(client_addr, 0, "Файл пуст")
return
start_time = time.time()
file_basename = os.path.basename(file_path)
# Регистрируем активную передачу
with self.lock:
self.active_transfers[client_addr] = {
'filename': file_basename,
'filesize': filesize,
'bytes_sent': 0,
'start_time': start_time
}
# Создаем новый сокет для передачи данных
transfer_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
transfer_socket.settimeout(TIMEOUT)
with self.lock:
self.transfer_sockets.add(transfer_socket)
self.log(f"[INFO] Начало передачи файла '{file_basename}' клиенту {client_addr}. Размер файла: {filesize} байт.")
with open(file_path, 'rb') as file:
block_number = 1
last_successful_block = 0
while True:
# Читаем блок данных
data = file.read(BLOCK_SIZE)
# Формируем и отправляем пакет данных
packet = struct.pack('!HH', 3, block_number) + data
retries = 0
while retries < MAX_RETRIES:
try:
transfer_socket.sendto(packet, client_addr)
# Ожидаем подтверждение
while True:
try:
ack_data, ack_addr = transfer_socket.recvfrom(4)
if ack_addr == client_addr and len(ack_data) >= 4:
opcode, ack_block = struct.unpack('!HH', ack_data)
if opcode == 4: # ACK
if ack_block == block_number:
# Успешное подтверждение
last_successful_block = block_number
bytes_sent = min((block_number * BLOCK_SIZE), filesize)
# Обновляем информацию о прогрессе
with self.lock:
if client_addr in self.active_transfers:
self.active_transfers[client_addr]['bytes_sent'] = bytes_sent
# Логируем статус каждую секунду
current_time = time.time()
if current_time - start_time >= 1.0:
bytes_remaining = filesize - bytes_sent
elapsed_time = current_time - start_time
self.log(f"[STATUS] Клиент: {client_addr} | Файл: {file_basename} | "
f"Отправлено: {bytes_sent}/{filesize} байт | "
f"Осталось: {bytes_remaining} байт | "
f"Время: {elapsed_time:.2f} сек.")
break
elif ack_block < block_number:
# Получен старый ACK, игнорируем
continue
except socket.timeout:
break
if last_successful_block == block_number:
break
else:
retries += 1
self.log(f"[WARN] Таймаут ожидания ACK для блока {block_number} от {client_addr}. "
f"Попытка {retries + 1}.")
except Exception as e:
retries += 1
self.log(f"[ERROR] Ошибка при передаче блока {block_number}: {str(e)}")
if retries >= MAX_RETRIES:
self.log(f"[ERROR] Превышено максимальное количество попыток для блока {block_number}")
return
block_number += 1
# Если отправили меньше BLOCK_SIZE байт, это был последний блок
if len(data) < BLOCK_SIZE:
break
self.log(f"[INFO] Передача файла '{file_basename}' клиенту {client_addr} завершена успешно")
except Exception as e:
self.log(f"[ERROR] Ошибка при передаче файла: {str(e)}")
finally:
# Очищаем информацию о передаче
with self.lock:
if client_addr in self.active_transfers:
del self.active_transfers[client_addr]
if transfer_socket in self.transfer_sockets:
self.transfer_sockets.remove(transfer_socket)
if transfer_socket:
try:
transfer_socket.close()
except:
pass

View File

@@ -2,19 +2,22 @@
# -*- coding: utf-8 -*-
import tkinter as tk
from tkinter import ttk, BOTH, X, BOTTOM
from tkinter import ttk, BOTH, X, BOTTOM, END
import webbrowser
class AboutWindow(tk.Toplevel):
def __init__(self, parent):
super().__init__(parent)
self.title("О программе")
self.geometry("400x300")
self.geometry("600x500")
self.resizable(False, False)
# Создаем фрейм
# Сохраняем ссылку на родительское окно
self.parent = parent
# Создаем фрейм для содержимого
about_frame = ttk.Frame(self, padding="20")
about_frame.pack(fill=BOTH, expand=True)
about_frame.pack(fill=BOTH, expand=True, padx=10, pady=10)
# Заголовок
ttk.Label(
@@ -33,7 +36,7 @@ class AboutWindow(tk.Toplevel):
# Версия
ttk.Label(
about_frame,
text="Версия 1.0",
text=f"Версия {getattr(parent, 'VERSION', '1.0.0')}",
font=("Segoe UI", 10)
).pack(pady=(0, 20))
@@ -66,7 +69,7 @@ class AboutWindow(tk.Toplevel):
ttk.Label(
contact_frame,
text="Email: LowaWorkMail@gmail.com"
text="Email: SPRF555@gmail.com"
).pack(anchor="w")
telegram_link = ttk.Label(
@@ -80,14 +83,14 @@ class AboutWindow(tk.Toplevel):
# Кнопка закрытия
ttk.Button(
about_frame,
self,
text="Закрыть",
command=self.destroy
).pack(side=BOTTOM, pady=(20, 0))
).pack(side=BOTTOM, pady=10)
# Центрируем окно
self.center_window()
def center_window(self):
self.update_idletasks()
width = self.winfo_width()
@@ -95,6 +98,6 @@ class AboutWindow(tk.Toplevel):
x = (self.winfo_screenwidth() // 2) - (width // 2)
y = (self.winfo_screenheight() // 2) - (height // 2)
self.geometry(f"{width}x{height}+{x}+{y}")
def open_url(self, url):
webbrowser.open(url)

Binary file not shown.

5
requirements.txt Normal file
View File

@@ -0,0 +1,5 @@
tftpy>=0.8.0
pyserial>=3.5
requests>=2.31.0
packaging>=23.2
send2trash>=1.8.0

165
update_checker.py Normal file
View File

@@ -0,0 +1,165 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import requests
import threading
import re
from packaging import version
import xml.etree.ElementTree as ET
import html
class UpdateCheckError(Exception):
"""Исключение для ошибок проверки обновлений"""
pass
class ReleaseType:
"""Типы релизов"""
STABLE = "stable"
PRERELEASE = "prerelease"
class UpdateChecker:
"""Класс для проверки обновлений программы"""
def __init__(self, current_version, repo_url, include_prereleases=False):
self.current_version = current_version
self.repo_url = repo_url
self.include_prereleases = include_prereleases
self.rss_url = f"{repo_url}/releases.rss"
self.release_info = None
def _clean_html(self, html_text):
"""Очищает HTML-разметку и форматирует текст"""
if not html_text:
return ""
text = re.sub(r'<[^>]+>', '', html_text)
text = html.unescape(text)
text = re.sub(r'\n\s*\n', '\n\n', text)
return '\n'.join(line.strip() for line in text.splitlines()).strip()
def _parse_release_info(self, item):
"""Извлекает информацию о релизе из RSS item"""
title = item.find('title').text if item.find('title') is not None else ''
link = item.find('link').text if item.find('link') is not None else ''
description = item.find('description').text if item.find('description') is not None else ''
content = item.find('{http://purl.org/rss/1.0/modules/content/}encoded')
content_text = content.text if content is not None else ''
# Извлекаем версию и проверяем тип релиза из тега
version_match = re.search(r'/releases/tag/(?:pre-)?v?(\d+\.\d+(?:\.\d+)?)', link)
if not version_match:
return None
version_str = version_match.group(1)
# Проверяем наличие префикса pre- в теге
is_prerelease = 'pre-' in link.lower()
# Форматируем название релиза
formatted_title = title
if title == version_str or not title.strip():
# Если заголовок пустой или совпадает с версией, создаем стандартное название
release_type = "Пре-релиз" if is_prerelease else "Версия"
formatted_title = f"{release_type} {version_str}"
elif not re.search(version_str, title):
# Если версия не указана в заголовке, добавляем её
formatted_title = f"{title} ({version_str})"
# Форматируем описание
formatted_description = self._clean_html(content_text or description)
if not formatted_description.strip():
formatted_description = "Нет описания"
# Добавляем метку типа релиза в начало описания
release_type_label = "[Пре-релиз] " if is_prerelease else ""
formatted_description = f"{release_type_label}{formatted_description}"
return {
'title': formatted_title,
'link': link,
'description': formatted_description,
'version': version_str,
'type': ReleaseType.PRERELEASE if is_prerelease else ReleaseType.STABLE
}
def check_updates(self, callback=None):
"""Проверяет наличие обновлений в асинхронном режиме"""
def check_worker():
try:
logging.info(f"Текущая версия программы: {self.current_version}")
logging.info(f"Проверка пре-релизов: {self.include_prereleases}")
logging.info(f"Запрос RSS ленты: {self.rss_url}")
response = requests.get(self.rss_url, timeout=10)
response.raise_for_status()
root = ET.fromstring(response.content)
items = root.findall('.//item')
if not items:
raise UpdateCheckError("Релизы не найдены")
logging.info(f"Найдено {len(items)} релизов")
latest_version = None
latest_info = None
for item in items:
release_info = self._parse_release_info(item)
if not release_info:
continue
is_prerelease = release_info['type'] == ReleaseType.PRERELEASE
logging.info(
f"Проверка релиза: {release_info['title']}, "
f"версия: {release_info['version']}, "
f"тип: {'пре-релиз' if is_prerelease else 'стабильная'}"
)
# Пропускаем пре-релизы если они не включены
if is_prerelease and not self.include_prereleases:
logging.info(f"Пропуск пре-релиза: {release_info['version']}")
continue
# Сравниваем версии
try:
current_ver = version.parse(latest_version or "0.0.0")
new_ver = version.parse(release_info['version'].split('-')[0]) # Убираем суффикс для сравнения
if new_ver > current_ver:
latest_version = release_info['version']
latest_info = release_info
logging.info(f"Новая версия: {latest_version}")
except version.InvalidVersion as e:
logging.warning(f"Некорректный формат версии {release_info['version']}: {e}")
continue
if not latest_info:
raise UpdateCheckError("Не найдены подходящие версии")
self.release_info = latest_info
# Сравниваем с текущей версией
current_ver = version.parse(self.current_version)
latest_ver = version.parse(latest_version.split('-')[0])
update_available = latest_ver > current_ver
logging.info(f"Сравнение версий: текущая {current_ver} <-> последняя {latest_ver}")
logging.info(f"Доступно обновление: {update_available}")
if callback:
callback(update_available, None)
except UpdateCheckError as e:
logging.error(str(e))
if callback:
callback(False, str(e))
except Exception as e:
logging.error(f"Ошибка при проверке обновлений: {e}", exc_info=True)
if callback:
callback(False, str(e))
threading.Thread(target=check_worker, daemon=True).start()
def get_release_notes(self):
"""Возвращает информацию о последнем релизе"""
return self.release_info