Compare commits
1 Commits
v1.1
...
4a67e70a92
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a67e70a92 |
1274
ComConfigCopy.py
Normal file
1274
ComConfigCopy.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,5 @@
|
|||||||
# ComConfigCopy
|
# ComConfigCopy
|
||||||
|
|
||||||
Исходный код для версии 1.1. Работоспособность не гарантирована, работа ведётся.
|
|
||||||
|
|
||||||
Программа для копирования конфигураций на коммутаторы.
|
Программа для копирования конфигураций на коммутаторы.
|
||||||
|
|
||||||
## Описание
|
## Описание
|
||||||
@@ -37,7 +35,7 @@ ComConfigCopy - это утилита, разработанная для авт
|
|||||||
|
|
||||||
## Контакты
|
## Контакты
|
||||||
|
|
||||||
- Email: LowaWorkMail@gmail.com
|
- Email: SPRF555@gmail.com
|
||||||
- Telegram: [@LowaSC](https://t.me/LowaSC)
|
- Telegram: [@LowaSC](https://t.me/LowaSC)
|
||||||
- Репозиторий: [ComConfigCopy](https://gitea.filow.ru/LowaSC/ComConfigCopy)
|
- Репозиторий: [ComConfigCopy](https://gitea.filow.ru/LowaSC/ComConfigCopy)
|
||||||
|
|
||||||
|
|||||||
349
TFTPServer.py
Normal file
349
TFTPServer.py
Normal 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
|
||||||
103
about_window.py
Normal file
103
about_window.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
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("600x500")
|
||||||
|
self.resizable(False, False)
|
||||||
|
|
||||||
|
# Сохраняем ссылку на родительское окно
|
||||||
|
self.parent = parent
|
||||||
|
|
||||||
|
# Создаем фрейм для содержимого
|
||||||
|
about_frame = ttk.Frame(self, padding="20")
|
||||||
|
about_frame.pack(fill=BOTH, expand=True, padx=10, pady=10)
|
||||||
|
|
||||||
|
# Заголовок
|
||||||
|
ttk.Label(
|
||||||
|
about_frame,
|
||||||
|
text="ComConfigCopy",
|
||||||
|
font=("Segoe UI", 16, "bold")
|
||||||
|
).pack(pady=(0, 10))
|
||||||
|
|
||||||
|
# Описание
|
||||||
|
ttk.Label(
|
||||||
|
about_frame,
|
||||||
|
text="Программа для копирования конфигураций на коммутаторы",
|
||||||
|
wraplength=350
|
||||||
|
).pack(pady=(0, 20))
|
||||||
|
|
||||||
|
# Версия
|
||||||
|
ttk.Label(
|
||||||
|
about_frame,
|
||||||
|
text=f"Версия {getattr(parent, 'VERSION', '1.0.0')}",
|
||||||
|
font=("Segoe UI", 10)
|
||||||
|
).pack(pady=(0, 20))
|
||||||
|
|
||||||
|
# Контактная информация
|
||||||
|
contact_frame = ttk.Frame(about_frame)
|
||||||
|
contact_frame.pack(fill=X, pady=(0, 20))
|
||||||
|
|
||||||
|
# Исходный код
|
||||||
|
ttk.Label(
|
||||||
|
contact_frame,
|
||||||
|
text="Исходный код:",
|
||||||
|
font=("Segoe UI", 10, "bold")
|
||||||
|
).pack(anchor="w")
|
||||||
|
|
||||||
|
source_link = ttk.Label(
|
||||||
|
contact_frame,
|
||||||
|
text="https://gitea.filow.ru/LowaSC/ComConfigCopy",
|
||||||
|
cursor="hand2",
|
||||||
|
foreground="blue"
|
||||||
|
)
|
||||||
|
source_link.pack(anchor="w")
|
||||||
|
source_link.bind("<Button-1>", lambda e: self.open_url("https://gitea.filow.ru/LowaSC/ComConfigCopy"))
|
||||||
|
|
||||||
|
# Контакты
|
||||||
|
ttk.Label(
|
||||||
|
contact_frame,
|
||||||
|
text="\nКонтакты:",
|
||||||
|
font=("Segoe UI", 10, "bold")
|
||||||
|
).pack(anchor="w")
|
||||||
|
|
||||||
|
ttk.Label(
|
||||||
|
contact_frame,
|
||||||
|
text="Email: LowaWorkMail@gmail.com"
|
||||||
|
).pack(anchor="w")
|
||||||
|
|
||||||
|
telegram_link = ttk.Label(
|
||||||
|
contact_frame,
|
||||||
|
text="Telegram: @LowaSC",
|
||||||
|
cursor="hand2",
|
||||||
|
foreground="blue"
|
||||||
|
)
|
||||||
|
telegram_link.pack(anchor="w")
|
||||||
|
telegram_link.bind("<Button-1>", lambda e: self.open_url("https://t.me/LowaSC"))
|
||||||
|
|
||||||
|
# Кнопка закрытия
|
||||||
|
ttk.Button(
|
||||||
|
self,
|
||||||
|
text="Закрыть",
|
||||||
|
command=self.destroy
|
||||||
|
).pack(side=BOTTOM, pady=10)
|
||||||
|
|
||||||
|
# Центрируем окно
|
||||||
|
self.center_window()
|
||||||
|
|
||||||
|
def center_window(self):
|
||||||
|
self.update_idletasks()
|
||||||
|
width = self.winfo_width()
|
||||||
|
height = self.winfo_height()
|
||||||
|
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)
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
# ComConfigCopy
|
|
||||||
|
|
||||||
Приложение для копирования конфигураций на сетевое оборудование через последовательный порт и TFTP.
|
|
||||||
|
|
||||||
## Структура проекта
|
|
||||||
|
|
||||||
```
|
|
||||||
src/
|
|
||||||
├── core/ # Ядро приложения
|
|
||||||
├── communication/ # Коммуникация с устройствами
|
|
||||||
├── filesystem/ # Работа с файловой системой
|
|
||||||
├── network/ # Сетевые компоненты
|
|
||||||
├── ui/ # Пользовательский интерфейс
|
|
||||||
└── utils/ # Утилиты
|
|
||||||
```
|
|
||||||
|
|
||||||
## Требования
|
|
||||||
|
|
||||||
- Python 3.8+
|
|
||||||
- pyserial>=3.5
|
|
||||||
- tftpy>=0.8.0
|
|
||||||
- requests>=2.31.0
|
|
||||||
- watchdog>=3.0.0
|
|
||||||
|
|
||||||
## Установка
|
|
||||||
|
|
||||||
1. Клонируйте репозиторий
|
|
||||||
2. Создайте виртуальное окружение: `python -m venv .venv`
|
|
||||||
3. Активируйте виртуальное окружение:
|
|
||||||
- Windows: `.venv\Scripts\activate`
|
|
||||||
- Linux/Mac: `source .venv/bin/activate`
|
|
||||||
4. Установите зависимости: `pip install -r requirements.txt`
|
|
||||||
|
|
||||||
## Использование
|
|
||||||
|
|
||||||
Запустите приложение:
|
|
||||||
```bash
|
|
||||||
python -m src.core.app
|
|
||||||
```
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import re
|
|
||||||
from typing import List, Optional, Dict, Any
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from core.exceptions import ValidationError
|
|
||||||
from .serial_manager import SerialManager
|
|
||||||
|
|
||||||
class CommandHandler:
|
|
||||||
"""Обработчик команд для работы с устройством."""
|
|
||||||
|
|
||||||
def __init__(self, serial_manager: SerialManager):
|
|
||||||
self._serial = serial_manager
|
|
||||||
self._logger = logging.getLogger(__name__)
|
|
||||||
self._command_cache: Dict[str, str] = {}
|
|
||||||
|
|
||||||
def validate_command(self, command: str) -> None:
|
|
||||||
"""
|
|
||||||
Валидация команды перед отправкой.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
command: Команда для проверки
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValidationError: Если команда не прошла валидацию
|
|
||||||
"""
|
|
||||||
# Проверяем на пустую команду
|
|
||||||
if not command or not command.strip():
|
|
||||||
raise ValidationError("Пустая команда")
|
|
||||||
|
|
||||||
# Проверяем на недопустимые символы
|
|
||||||
if re.search(r'[\x00-\x1F\x7F]', command):
|
|
||||||
raise ValidationError("Команда содержит недопустимые символы")
|
|
||||||
|
|
||||||
# Проверяем максимальную длину
|
|
||||||
if len(command) > 1024:
|
|
||||||
raise ValidationError("Превышена максимальная длина команды (1024 символа)")
|
|
||||||
|
|
||||||
def validate_config_commands(self, commands: List[str]) -> None:
|
|
||||||
"""
|
|
||||||
Валидация списка команд конфигурации.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
commands: Список команд для проверки
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValidationError: Если команды не прошли валидацию
|
|
||||||
"""
|
|
||||||
if not commands:
|
|
||||||
raise ValidationError("Пустой список команд")
|
|
||||||
|
|
||||||
for command in commands:
|
|
||||||
self.validate_command(command)
|
|
||||||
|
|
||||||
def execute_command(self, command: str, timeout: Optional[int] = None,
|
|
||||||
use_cache: bool = False) -> str:
|
|
||||||
"""
|
|
||||||
Выполнение команды на устройстве.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
command: Команда для выполнения
|
|
||||||
timeout: Таймаут ожидания ответа
|
|
||||||
use_cache: Использовать кэш команд
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Ответ устройства
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValidationError: При ошибке валидации
|
|
||||||
ConnectionError: При ошибке соединения
|
|
||||||
"""
|
|
||||||
# Проверяем команду
|
|
||||||
self.validate_command(command)
|
|
||||||
|
|
||||||
# Проверяем кэш
|
|
||||||
if use_cache and command in self._command_cache:
|
|
||||||
self._logger.debug(f"Использован кэш для команды: {command}")
|
|
||||||
return self._command_cache[command]
|
|
||||||
|
|
||||||
# Выполняем команду
|
|
||||||
response = self._serial.send_command(command, timeout)
|
|
||||||
|
|
||||||
# Сохраняем в кэш
|
|
||||||
if use_cache:
|
|
||||||
self._command_cache[command] = response
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
def execute_config(self, commands: List[str], timeout: Optional[int] = None) -> str:
|
|
||||||
"""
|
|
||||||
Выполнение конфигурационных команд на устройстве.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
commands: Список команд для выполнения
|
|
||||||
timeout: Таймаут ожидания ответа
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Ответ устройства
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValidationError: При ошибке валидации
|
|
||||||
ConnectionError: При ошибке соединения
|
|
||||||
"""
|
|
||||||
# Проверяем команды
|
|
||||||
self.validate_config_commands(commands)
|
|
||||||
|
|
||||||
# Выполняем команды
|
|
||||||
return self._serial.send_config(commands, timeout)
|
|
||||||
|
|
||||||
def clear_cache(self) -> None:
|
|
||||||
"""Очистка кэша команд."""
|
|
||||||
self._command_cache.clear()
|
|
||||||
self._logger.debug("Кэш команд очищен")
|
|
||||||
|
|
||||||
def get_device_info(self) -> Dict[str, str]:
|
|
||||||
"""
|
|
||||||
Получение информации об устройстве.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict[str, str]: Словарь с информацией об устройстве
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ConnectionError: При ошибке соединения
|
|
||||||
"""
|
|
||||||
info = {}
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Получаем версию ПО
|
|
||||||
version = self.execute_command("show version", use_cache=True)
|
|
||||||
info["version"] = self._parse_version(version)
|
|
||||||
|
|
||||||
# Получаем hostname
|
|
||||||
hostname = self.execute_command("show hostname", use_cache=True)
|
|
||||||
info["hostname"] = hostname.strip()
|
|
||||||
|
|
||||||
# Получаем модель
|
|
||||||
model = self.execute_command("show model", use_cache=True)
|
|
||||||
info["model"] = self._parse_model(model)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка получения информации об устройстве: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
return info
|
|
||||||
|
|
||||||
def _parse_version(self, version_output: str) -> str:
|
|
||||||
"""
|
|
||||||
Парсинг версии из вывода команды.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
version_output: Вывод команды show version
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Версия ПО
|
|
||||||
"""
|
|
||||||
# Здесь должна быть реализация парсинга версии
|
|
||||||
# в зависимости от формата вывода конкретного устройства
|
|
||||||
return version_output.strip()
|
|
||||||
|
|
||||||
def _parse_model(self, model_output: str) -> str:
|
|
||||||
"""
|
|
||||||
Парсинг модели из вывода команды.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
model_output: Вывод команды show model
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Модель устройства
|
|
||||||
"""
|
|
||||||
# Здесь должна быть реализация парсинга модели
|
|
||||||
# в зависимости от формата вывода конкретного устройства
|
|
||||||
return model_output.strip()
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
from typing import Any, Dict, Optional
|
|
||||||
from core.events import event_bus, Event, EventTypes
|
|
||||||
from core.exceptions import ConnectionError, AuthenticationError
|
|
||||||
|
|
||||||
class BaseProtocol(ABC):
|
|
||||||
"""Базовый класс для всех протоколов связи."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._connected = False
|
|
||||||
self._authenticated = False
|
|
||||||
self._config: Dict[str, Any] = {}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_connected(self) -> bool:
|
|
||||||
"""Проверка состояния подключения."""
|
|
||||||
return self._connected
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_authenticated(self) -> bool:
|
|
||||||
"""Проверка состояния аутентификации."""
|
|
||||||
return self._authenticated
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def connect(self) -> None:
|
|
||||||
"""
|
|
||||||
Установка соединения.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ConnectionError: При ошибке подключения
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def disconnect(self) -> None:
|
|
||||||
"""Разрыв соединения."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def authenticate(self, username: str, password: str) -> None:
|
|
||||||
"""
|
|
||||||
Аутентификация на устройстве.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
username: Имя пользователя
|
|
||||||
password: Пароль
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
AuthenticationError: При ошибке аутентификации
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def send_command(self, command: str, timeout: Optional[int] = None) -> str:
|
|
||||||
"""
|
|
||||||
Отправка команды на устройство.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
command: Команда для отправки
|
|
||||||
timeout: Таймаут ожидания ответа
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Ответ устройства
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ConnectionError: При ошибке отправки команды
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def send_config(self, config_commands: list[str], timeout: Optional[int] = None) -> str:
|
|
||||||
"""
|
|
||||||
Отправка конфигурационных команд на устройство.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
config_commands: Список команд конфигурации
|
|
||||||
timeout: Таймаут ожидания ответа
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Ответ устройства
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ConnectionError: При ошибке отправки команд
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def configure(self, **kwargs) -> None:
|
|
||||||
"""
|
|
||||||
Конфигурация протокола.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
**kwargs: Параметры конфигурации
|
|
||||||
"""
|
|
||||||
self._config.update(kwargs)
|
|
||||||
|
|
||||||
def _notify_connection_established(self) -> None:
|
|
||||||
"""Уведомление об установке соединения."""
|
|
||||||
self._connected = True
|
|
||||||
event_bus.publish(Event(EventTypes.CONNECTION_ESTABLISHED, None))
|
|
||||||
|
|
||||||
def _notify_connection_lost(self) -> None:
|
|
||||||
"""Уведомление о потере соединения."""
|
|
||||||
self._connected = False
|
|
||||||
self._authenticated = False
|
|
||||||
event_bus.publish(Event(EventTypes.CONNECTION_LOST, None))
|
|
||||||
|
|
||||||
def _notify_connection_error(self, error: Exception) -> None:
|
|
||||||
"""
|
|
||||||
Уведомление об ошибке соединения.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
error: Объект ошибки
|
|
||||||
"""
|
|
||||||
self._connected = False
|
|
||||||
self._authenticated = False
|
|
||||||
event_bus.publish(Event(EventTypes.CONNECTION_ERROR, str(error)))
|
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import time
|
|
||||||
import re
|
|
||||||
from typing import Optional, List
|
|
||||||
import serial
|
|
||||||
from serial.serialutil import SerialException
|
|
||||||
|
|
||||||
from core.config import AppConfig
|
|
||||||
from core.exceptions import ConnectionError, AuthenticationError
|
|
||||||
from .base import BaseProtocol
|
|
||||||
|
|
||||||
class SerialProtocol(BaseProtocol):
|
|
||||||
"""Протокол для работы с последовательным портом."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self._serial: Optional[serial.Serial] = None
|
|
||||||
self._prompt = AppConfig.DEFAULT_PROMPT
|
|
||||||
self._timeout = AppConfig.DEFAULT_TIMEOUT
|
|
||||||
self._baudrate = AppConfig.DEFAULT_BAUDRATE
|
|
||||||
self._port: Optional[str] = None
|
|
||||||
|
|
||||||
def configure(self, port: str, baudrate: int = None, timeout: int = None,
|
|
||||||
prompt: str = None) -> None:
|
|
||||||
"""
|
|
||||||
Конфигурация последовательного порта.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
port: COM-порт
|
|
||||||
baudrate: Скорость передачи
|
|
||||||
timeout: Таймаут операций
|
|
||||||
prompt: Приглашение командной строки
|
|
||||||
"""
|
|
||||||
self._port = port
|
|
||||||
if baudrate is not None:
|
|
||||||
self._baudrate = baudrate
|
|
||||||
if timeout is not None:
|
|
||||||
self._timeout = timeout
|
|
||||||
if prompt is not None:
|
|
||||||
self._prompt = prompt
|
|
||||||
|
|
||||||
def connect(self) -> None:
|
|
||||||
"""
|
|
||||||
Установка соединения с устройством.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ConnectionError: При ошибке подключения
|
|
||||||
"""
|
|
||||||
if not self._port:
|
|
||||||
raise ConnectionError("Не указан COM-порт")
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._serial = serial.Serial(
|
|
||||||
port=self._port,
|
|
||||||
baudrate=self._baudrate,
|
|
||||||
timeout=1
|
|
||||||
)
|
|
||||||
time.sleep(1) # Ждем инициализации порта
|
|
||||||
self._notify_connection_established()
|
|
||||||
|
|
||||||
except SerialException as e:
|
|
||||||
self._notify_connection_error(e)
|
|
||||||
raise ConnectionError(f"Ошибка подключения к порту {self._port}: {e}")
|
|
||||||
|
|
||||||
def disconnect(self) -> None:
|
|
||||||
"""Закрытие соединения."""
|
|
||||||
if self._serial and self._serial.is_open:
|
|
||||||
self._serial.close()
|
|
||||||
self._notify_connection_lost()
|
|
||||||
|
|
||||||
def authenticate(self, username: str, password: str) -> None:
|
|
||||||
"""
|
|
||||||
Аутентификация на устройстве.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
username: Имя пользователя
|
|
||||||
password: Пароль
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
AuthenticationError: При ошибке аутентификации
|
|
||||||
ConnectionError: При ошибке соединения
|
|
||||||
"""
|
|
||||||
if not self.is_connected:
|
|
||||||
raise ConnectionError("Нет подключения к устройству")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Очищаем буфер
|
|
||||||
self._serial.reset_input_buffer()
|
|
||||||
self._serial.reset_output_buffer()
|
|
||||||
|
|
||||||
# Отправляем Enter для получения приглашения
|
|
||||||
self._serial.write(b"\n")
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
# Ожидаем запрос логина или пароля
|
|
||||||
response = self._read_until(["login:", "username:", "password:", self._prompt])
|
|
||||||
|
|
||||||
if "login:" in response.lower() or "username:" in response.lower():
|
|
||||||
self._serial.write(f"{username}\n".encode())
|
|
||||||
time.sleep(0.5)
|
|
||||||
response = self._read_until(["password:", self._prompt])
|
|
||||||
|
|
||||||
if "password:" in response.lower():
|
|
||||||
self._serial.write(f"{password}\n".encode())
|
|
||||||
time.sleep(0.5)
|
|
||||||
response = self._read_until([self._prompt])
|
|
||||||
|
|
||||||
if self._prompt not in response:
|
|
||||||
raise AuthenticationError("Неверные учетные данные")
|
|
||||||
|
|
||||||
self._authenticated = True
|
|
||||||
|
|
||||||
except SerialException as e:
|
|
||||||
self._notify_connection_error(e)
|
|
||||||
raise ConnectionError(f"Ошибка при аутентификации: {e}")
|
|
||||||
|
|
||||||
def send_command(self, command: str, timeout: Optional[int] = None) -> str:
|
|
||||||
"""
|
|
||||||
Отправка команды на устройство.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
command: Команда для отправки
|
|
||||||
timeout: Таймаут ожидания ответа
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Ответ устройства
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ConnectionError: При ошибке отправки команды
|
|
||||||
"""
|
|
||||||
if not self.is_connected:
|
|
||||||
raise ConnectionError("Нет подключения к устройству")
|
|
||||||
|
|
||||||
if not self.is_authenticated:
|
|
||||||
raise ConnectionError("Требуется аутентификация")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Очищаем буфер перед отправкой
|
|
||||||
self._serial.reset_input_buffer()
|
|
||||||
|
|
||||||
# Отправляем команду
|
|
||||||
self._serial.write(f"{command}\n".encode())
|
|
||||||
|
|
||||||
# Ожидаем ответ
|
|
||||||
response = self._read_until([self._prompt], timeout or self._timeout)
|
|
||||||
|
|
||||||
# Удаляем отправленную команду из ответа
|
|
||||||
lines = response.splitlines()
|
|
||||||
if lines and command in lines[0]:
|
|
||||||
lines.pop(0)
|
|
||||||
|
|
||||||
return "\n".join(lines).strip()
|
|
||||||
|
|
||||||
except SerialException as e:
|
|
||||||
self._notify_connection_error(e)
|
|
||||||
raise ConnectionError(f"Ошибка при отправке команды: {e}")
|
|
||||||
|
|
||||||
def send_config(self, config_commands: List[str], timeout: Optional[int] = None) -> str:
|
|
||||||
"""
|
|
||||||
Отправка конфигурационных команд на устройство.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
config_commands: Список команд конфигурации
|
|
||||||
timeout: Таймаут ожидания ответа
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Ответ устройства
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ConnectionError: При ошибке отправки команд
|
|
||||||
"""
|
|
||||||
responses = []
|
|
||||||
for command in config_commands:
|
|
||||||
response = self.send_command(command, timeout)
|
|
||||||
responses.append(response)
|
|
||||||
return "\n".join(responses)
|
|
||||||
|
|
||||||
def _read_until(self, patterns: List[str], timeout: Optional[int] = None) -> str:
|
|
||||||
"""
|
|
||||||
Чтение данных из порта до появления одного из паттернов.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
patterns: Список паттернов для поиска
|
|
||||||
timeout: Таймаут операции
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Прочитанные данные
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ConnectionError: При ошибке чтения или таймауте
|
|
||||||
"""
|
|
||||||
if not patterns:
|
|
||||||
raise ValueError("Не указаны паттерны для поиска")
|
|
||||||
|
|
||||||
timeout = timeout or self._timeout
|
|
||||||
end_time = time.time() + timeout
|
|
||||||
buffer = ""
|
|
||||||
|
|
||||||
while time.time() < end_time:
|
|
||||||
if self._serial.in_waiting:
|
|
||||||
chunk = self._serial.read(self._serial.in_waiting).decode(errors="ignore")
|
|
||||||
buffer += chunk
|
|
||||||
|
|
||||||
# Проверяем наличие паттернов
|
|
||||||
for pattern in patterns:
|
|
||||||
if pattern in buffer:
|
|
||||||
return buffer
|
|
||||||
|
|
||||||
# Небольшая задержка для снижения нагрузки на CPU
|
|
||||||
time.sleep(0.1)
|
|
||||||
|
|
||||||
raise ConnectionError(f"Таймаут операции ({timeout} сек)")
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def list_ports() -> List[str]:
|
|
||||||
"""
|
|
||||||
Получение списка доступных COM-портов.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List[str]: Список доступных портов
|
|
||||||
"""
|
|
||||||
return [port.device for port in serial.tools.list_ports.comports()]
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import os
|
|
||||||
from typing import Optional, Callable
|
|
||||||
import tftpy
|
|
||||||
|
|
||||||
from core.config import AppConfig
|
|
||||||
from core.exceptions import TFTPError
|
|
||||||
from core.events import event_bus, Event, EventTypes
|
|
||||||
|
|
||||||
class TFTPProtocol:
|
|
||||||
"""Протокол для работы с TFTP сервером."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._server: Optional[tftpy.TftpServer] = None
|
|
||||||
self._client: Optional[tftpy.TftpClient] = None
|
|
||||||
self._server_running = False
|
|
||||||
self._root_dir = AppConfig.CONFIGS_DIR
|
|
||||||
self._port = AppConfig.TFTP_PORT
|
|
||||||
self._timeout = AppConfig.TFTP_TIMEOUT
|
|
||||||
self._retries = AppConfig.TFTP_RETRIES
|
|
||||||
|
|
||||||
def configure(self, root_dir: Optional[str] = None, port: Optional[int] = None,
|
|
||||||
timeout: Optional[int] = None, retries: Optional[int] = None) -> None:
|
|
||||||
"""
|
|
||||||
Конфигурация TFTP сервера/клиента.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
root_dir: Корневая директория для файлов
|
|
||||||
port: Порт TFTP сервера
|
|
||||||
timeout: Таймаут операций
|
|
||||||
retries: Количество попыток
|
|
||||||
"""
|
|
||||||
if root_dir is not None:
|
|
||||||
self._root_dir = root_dir
|
|
||||||
if port is not None:
|
|
||||||
self._port = port
|
|
||||||
if timeout is not None:
|
|
||||||
self._timeout = timeout
|
|
||||||
if retries is not None:
|
|
||||||
self._retries = retries
|
|
||||||
|
|
||||||
def start_server(self, host: str = "0.0.0.0") -> None:
|
|
||||||
"""
|
|
||||||
Запуск TFTP сервера.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
host: IP-адрес для прослушивания
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TFTPError: При ошибке запуска сервера
|
|
||||||
"""
|
|
||||||
if self._server_running:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Создаем серверный объект
|
|
||||||
self._server = tftpy.TftpServer(self._root_dir)
|
|
||||||
|
|
||||||
# Запускаем сервер в отдельном потоке
|
|
||||||
self._server.listen(host, self._port, timeout=self._timeout)
|
|
||||||
self._server_running = True
|
|
||||||
|
|
||||||
event_bus.publish(Event(EventTypes.TFTP_SERVER_STARTED, {
|
|
||||||
"host": host,
|
|
||||||
"port": self._port,
|
|
||||||
"root_dir": self._root_dir
|
|
||||||
}))
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
raise TFTPError(f"Ошибка запуска TFTP сервера: {e}")
|
|
||||||
|
|
||||||
def stop_server(self) -> None:
|
|
||||||
"""Остановка TFTP сервера."""
|
|
||||||
if self._server and self._server_running:
|
|
||||||
self._server.stop()
|
|
||||||
self._server = None
|
|
||||||
self._server_running = False
|
|
||||||
event_bus.publish(Event(EventTypes.TFTP_SERVER_STOPPED, None))
|
|
||||||
|
|
||||||
def upload_file(self, filename: str, host: str, remote_filename: Optional[str] = None,
|
|
||||||
progress_callback: Optional[Callable[[int], None]] = None) -> None:
|
|
||||||
"""
|
|
||||||
Загрузка файла на удаленное устройство.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
filename: Путь к локальному файлу
|
|
||||||
host: IP-адрес устройства
|
|
||||||
remote_filename: Имя файла на устройстве
|
|
||||||
progress_callback: Функция обратного вызова для отслеживания прогресса
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TFTPError: При ошибке загрузки файла
|
|
||||||
"""
|
|
||||||
if not os.path.exists(filename):
|
|
||||||
raise TFTPError(f"Файл не найден: {filename}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Создаем клиентский объект
|
|
||||||
self._client = tftpy.TftpClient(
|
|
||||||
host,
|
|
||||||
self._port,
|
|
||||||
options={"timeout": self._timeout, "retries": self._retries}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Определяем имя удаленного файла
|
|
||||||
if not remote_filename:
|
|
||||||
remote_filename = os.path.basename(filename)
|
|
||||||
|
|
||||||
event_bus.publish(Event(EventTypes.TFTP_TRANSFER_STARTED, {
|
|
||||||
"operation": "upload",
|
|
||||||
"local_file": filename,
|
|
||||||
"remote_file": remote_filename,
|
|
||||||
"host": host
|
|
||||||
}))
|
|
||||||
|
|
||||||
# Загружаем файл
|
|
||||||
self._client.upload(
|
|
||||||
remote_filename,
|
|
||||||
filename,
|
|
||||||
progress_callback
|
|
||||||
)
|
|
||||||
|
|
||||||
event_bus.publish(Event(EventTypes.TFTP_TRANSFER_COMPLETED, {
|
|
||||||
"operation": "upload",
|
|
||||||
"local_file": filename,
|
|
||||||
"remote_file": remote_filename,
|
|
||||||
"host": host
|
|
||||||
}))
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
event_bus.publish(Event(EventTypes.TFTP_ERROR, str(e)))
|
|
||||||
raise TFTPError(f"Ошибка загрузки файла: {e}")
|
|
||||||
|
|
||||||
def download_file(self, remote_filename: str, host: str, local_filename: Optional[str] = None,
|
|
||||||
progress_callback: Optional[Callable[[int], None]] = None) -> None:
|
|
||||||
"""
|
|
||||||
Загрузка файла с удаленного устройства.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
remote_filename: Имя файла на устройстве
|
|
||||||
host: IP-адрес устройства
|
|
||||||
local_filename: Путь для сохранения файла
|
|
||||||
progress_callback: Функция обратного вызова для отслеживания прогресса
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TFTPError: При ошибке загрузки файла
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Создаем клиентский объект
|
|
||||||
self._client = tftpy.TftpClient(
|
|
||||||
host,
|
|
||||||
self._port,
|
|
||||||
options={"timeout": self._timeout, "retries": self._retries}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Определяем имя локального файла
|
|
||||||
if not local_filename:
|
|
||||||
local_filename = os.path.join(self._root_dir, remote_filename)
|
|
||||||
|
|
||||||
event_bus.publish(Event(EventTypes.TFTP_TRANSFER_STARTED, {
|
|
||||||
"operation": "download",
|
|
||||||
"local_file": local_filename,
|
|
||||||
"remote_file": remote_filename,
|
|
||||||
"host": host
|
|
||||||
}))
|
|
||||||
|
|
||||||
# Загружаем файл
|
|
||||||
self._client.download(
|
|
||||||
remote_filename,
|
|
||||||
local_filename,
|
|
||||||
progress_callback
|
|
||||||
)
|
|
||||||
|
|
||||||
event_bus.publish(Event(EventTypes.TFTP_TRANSFER_COMPLETED, {
|
|
||||||
"operation": "download",
|
|
||||||
"local_file": local_filename,
|
|
||||||
"remote_file": remote_filename,
|
|
||||||
"host": host
|
|
||||||
}))
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
event_bus.publish(Event(EventTypes.TFTP_ERROR, str(e)))
|
|
||||||
raise TFTPError(f"Ошибка загрузки файла: {e}")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_server_running(self) -> bool:
|
|
||||||
"""Проверка состояния сервера."""
|
|
||||||
return self._server_running
|
|
||||||
@@ -1,223 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import re
|
|
||||||
from typing import List, Dict, Any, Optional
|
|
||||||
from dataclasses import dataclass
|
|
||||||
import logging
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ParsedResponse:
|
|
||||||
"""Структура разобранного ответа."""
|
|
||||||
success: bool
|
|
||||||
data: Any
|
|
||||||
error: Optional[str] = None
|
|
||||||
raw_response: Optional[str] = None
|
|
||||||
|
|
||||||
class ResponseParser:
|
|
||||||
"""Парсер ответов от сетевого устройства."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# Регулярные выражения для парсинга
|
|
||||||
self._patterns = {
|
|
||||||
"error": r"(?i)error|invalid|failed|denied|rejected",
|
|
||||||
"success": r"(?i)success|completed|done|ok",
|
|
||||||
"version": r"(?i)version\s+(\S+)",
|
|
||||||
"model": r"(?i)model\s*:\s*(\S+)",
|
|
||||||
"hostname": r"(?i)hostname\s*:\s*(\S+)",
|
|
||||||
"interface": r"(?i)interface\s+(\S+)",
|
|
||||||
"ip_address": r"(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})",
|
|
||||||
"mac_address": r"([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})",
|
|
||||||
}
|
|
||||||
|
|
||||||
def parse_command_response(self, response: str) -> ParsedResponse:
|
|
||||||
"""
|
|
||||||
Парсинг ответа на команду.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
response: Ответ устройства
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
ParsedResponse: Структура с результатами парсинга
|
|
||||||
"""
|
|
||||||
# Проверяем на наличие ошибок
|
|
||||||
if re.search(self._patterns["error"], response, re.MULTILINE):
|
|
||||||
error_msg = self._extract_error_message(response)
|
|
||||||
return ParsedResponse(
|
|
||||||
success=False,
|
|
||||||
data=None,
|
|
||||||
error=error_msg,
|
|
||||||
raw_response=response
|
|
||||||
)
|
|
||||||
|
|
||||||
# Проверяем на успешное выполнение
|
|
||||||
success = bool(re.search(self._patterns["success"], response, re.MULTILINE))
|
|
||||||
|
|
||||||
return ParsedResponse(
|
|
||||||
success=success,
|
|
||||||
data=response.strip(),
|
|
||||||
raw_response=response
|
|
||||||
)
|
|
||||||
|
|
||||||
def parse_version(self, response: str) -> ParsedResponse:
|
|
||||||
"""
|
|
||||||
Парсинг версии ПО.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
response: Ответ на команду show version
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
ParsedResponse: Структура с результатами парсинга
|
|
||||||
"""
|
|
||||||
match = re.search(self._patterns["version"], response)
|
|
||||||
if match:
|
|
||||||
return ParsedResponse(
|
|
||||||
success=True,
|
|
||||||
data=match.group(1),
|
|
||||||
raw_response=response
|
|
||||||
)
|
|
||||||
return ParsedResponse(
|
|
||||||
success=False,
|
|
||||||
data=None,
|
|
||||||
error="Версия не найдена",
|
|
||||||
raw_response=response
|
|
||||||
)
|
|
||||||
|
|
||||||
def parse_model(self, response: str) -> ParsedResponse:
|
|
||||||
"""
|
|
||||||
Парсинг модели устройства.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
response: Ответ на команду show model
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
ParsedResponse: Структура с результатами парсинга
|
|
||||||
"""
|
|
||||||
match = re.search(self._patterns["model"], response)
|
|
||||||
if match:
|
|
||||||
return ParsedResponse(
|
|
||||||
success=True,
|
|
||||||
data=match.group(1),
|
|
||||||
raw_response=response
|
|
||||||
)
|
|
||||||
return ParsedResponse(
|
|
||||||
success=False,
|
|
||||||
data=None,
|
|
||||||
error="Модель не найдена",
|
|
||||||
raw_response=response
|
|
||||||
)
|
|
||||||
|
|
||||||
def parse_interfaces(self, response: str) -> ParsedResponse:
|
|
||||||
"""
|
|
||||||
Парсинг информации об интерфейсах.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
response: Ответ на команду show interfaces
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
ParsedResponse: Структура с результатами парсинга
|
|
||||||
"""
|
|
||||||
interfaces = []
|
|
||||||
|
|
||||||
# Ищем все интерфейсы
|
|
||||||
for line in response.splitlines():
|
|
||||||
if_match = re.search(self._patterns["interface"], line)
|
|
||||||
if if_match:
|
|
||||||
interface = {
|
|
||||||
"name": if_match.group(1),
|
|
||||||
"ip": None,
|
|
||||||
"mac": None
|
|
||||||
}
|
|
||||||
|
|
||||||
# Ищем IP-адрес
|
|
||||||
ip_match = re.search(self._patterns["ip_address"], line)
|
|
||||||
if ip_match:
|
|
||||||
interface["ip"] = ip_match.group(1)
|
|
||||||
|
|
||||||
# Ищем MAC-адрес
|
|
||||||
mac_match = re.search(self._patterns["mac_address"], line)
|
|
||||||
if mac_match:
|
|
||||||
interface["mac"] = mac_match.group(0)
|
|
||||||
|
|
||||||
interfaces.append(interface)
|
|
||||||
|
|
||||||
if interfaces:
|
|
||||||
return ParsedResponse(
|
|
||||||
success=True,
|
|
||||||
data=interfaces,
|
|
||||||
raw_response=response
|
|
||||||
)
|
|
||||||
return ParsedResponse(
|
|
||||||
success=False,
|
|
||||||
data=None,
|
|
||||||
error="Интерфейсы не найдены",
|
|
||||||
raw_response=response
|
|
||||||
)
|
|
||||||
|
|
||||||
def parse_config_result(self, response: str) -> ParsedResponse:
|
|
||||||
"""
|
|
||||||
Парсинг результата применения конфигурации.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
response: Ответ на команды конфигурации
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
ParsedResponse: Структура с результатами парсинга
|
|
||||||
"""
|
|
||||||
# Проверяем на наличие ошибок
|
|
||||||
if re.search(self._patterns["error"], response, re.MULTILINE):
|
|
||||||
error_msg = self._extract_error_message(response)
|
|
||||||
return ParsedResponse(
|
|
||||||
success=False,
|
|
||||||
data=None,
|
|
||||||
error=error_msg,
|
|
||||||
raw_response=response
|
|
||||||
)
|
|
||||||
|
|
||||||
# Если нет ошибок, считаем что конфигурация применена успешно
|
|
||||||
return ParsedResponse(
|
|
||||||
success=True,
|
|
||||||
data=response.strip(),
|
|
||||||
raw_response=response
|
|
||||||
)
|
|
||||||
|
|
||||||
def _extract_error_message(self, response: str) -> str:
|
|
||||||
"""
|
|
||||||
Извлечение сообщения об ошибке из ответа.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
response: Ответ устройства
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Сообщение об ошибке
|
|
||||||
"""
|
|
||||||
# Ищем строку с ошибкой
|
|
||||||
for line in response.splitlines():
|
|
||||||
if re.search(self._patterns["error"], line, re.IGNORECASE):
|
|
||||||
return line.strip()
|
|
||||||
return "Неизвестная ошибка"
|
|
||||||
|
|
||||||
def add_pattern(self, name: str, pattern: str) -> None:
|
|
||||||
"""
|
|
||||||
Добавление нового шаблона для парсинга.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: Имя шаблона
|
|
||||||
pattern: Регулярное выражение
|
|
||||||
"""
|
|
||||||
self._patterns[name] = pattern
|
|
||||||
self._logger.debug(f"Добавлен новый шаблон: {name} = {pattern}")
|
|
||||||
|
|
||||||
def get_pattern(self, name: str) -> Optional[str]:
|
|
||||||
"""
|
|
||||||
Получение шаблона по имени.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: Имя шаблона
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Optional[str]: Регулярное выражение или None
|
|
||||||
"""
|
|
||||||
return self._patterns.get(name)
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from typing import Optional, List, Dict, Any
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from core.events import event_bus, Event, EventTypes
|
|
||||||
from core.exceptions import ConnectionError, AuthenticationError
|
|
||||||
from core.config import AppConfig
|
|
||||||
from .protocols.serial import SerialProtocol
|
|
||||||
|
|
||||||
class SerialManager:
|
|
||||||
"""Менеджер для работы с последовательным портом."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._protocol = SerialProtocol()
|
|
||||||
self._logger = logging.getLogger(__name__)
|
|
||||||
self._connected = False
|
|
||||||
self._authenticated = False
|
|
||||||
|
|
||||||
def configure(self, settings: Dict[str, Any]) -> None:
|
|
||||||
"""
|
|
||||||
Конфигурация последовательного порта.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
settings: Словарь с настройками
|
|
||||||
"""
|
|
||||||
self._protocol.configure(
|
|
||||||
port=settings.get("port"),
|
|
||||||
baudrate=settings.get("baudrate", AppConfig.DEFAULT_BAUDRATE),
|
|
||||||
timeout=settings.get("timeout", AppConfig.DEFAULT_TIMEOUT),
|
|
||||||
prompt=settings.get("prompt", AppConfig.DEFAULT_PROMPT)
|
|
||||||
)
|
|
||||||
|
|
||||||
def connect(self) -> None:
|
|
||||||
"""
|
|
||||||
Установка соединения с устройством.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ConnectionError: При ошибке подключения
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
self._protocol.connect()
|
|
||||||
self._connected = True
|
|
||||||
self._logger.info("Соединение установлено")
|
|
||||||
|
|
||||||
except ConnectionError as e:
|
|
||||||
self._connected = False
|
|
||||||
self._authenticated = False
|
|
||||||
self._logger.error(f"Ошибка подключения: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
def disconnect(self) -> None:
|
|
||||||
"""Разрыв соединения."""
|
|
||||||
if self._connected:
|
|
||||||
self._protocol.disconnect()
|
|
||||||
self._connected = False
|
|
||||||
self._authenticated = False
|
|
||||||
self._logger.info("Соединение разорвано")
|
|
||||||
|
|
||||||
def authenticate(self, username: str, password: str) -> None:
|
|
||||||
"""
|
|
||||||
Аутентификация на устройстве.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
username: Имя пользователя
|
|
||||||
password: Пароль
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ConnectionError: При ошибке соединения
|
|
||||||
AuthenticationError: При ошибке аутентификации
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
self._protocol.authenticate(username, password)
|
|
||||||
self._authenticated = True
|
|
||||||
self._logger.info("Аутентификация успешна")
|
|
||||||
|
|
||||||
except (ConnectionError, AuthenticationError) as e:
|
|
||||||
self._authenticated = False
|
|
||||||
self._logger.error(f"Ошибка аутентификации: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
def send_command(self, command: str, timeout: Optional[int] = None) -> str:
|
|
||||||
"""
|
|
||||||
Отправка команды на устройство.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
command: Команда для отправки
|
|
||||||
timeout: Таймаут ожидания ответа
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Ответ устройства
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ConnectionError: При ошибке отправки команды
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
response = self._protocol.send_command(command, timeout)
|
|
||||||
self._logger.debug(f"Отправлена команда: {command}")
|
|
||||||
self._logger.debug(f"Получен ответ: {response}")
|
|
||||||
return response
|
|
||||||
|
|
||||||
except ConnectionError as e:
|
|
||||||
self._logger.error(f"Ошибка отправки команды: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
def send_config(self, config_commands: List[str], timeout: Optional[int] = None) -> str:
|
|
||||||
"""
|
|
||||||
Отправка конфигурационных команд на устройство.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
config_commands: Список команд конфигурации
|
|
||||||
timeout: Таймаут ожидания ответа
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Ответ устройства
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ConnectionError: При ошибке отправки команд
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
response = self._protocol.send_config(config_commands, timeout)
|
|
||||||
self._logger.info(f"Отправлено {len(config_commands)} команд конфигурации")
|
|
||||||
return response
|
|
||||||
|
|
||||||
except ConnectionError as e:
|
|
||||||
self._logger.error(f"Ошибка отправки конфигурации: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_connected(self) -> bool:
|
|
||||||
"""Проверка состояния подключения."""
|
|
||||||
return self._connected
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_authenticated(self) -> bool:
|
|
||||||
"""Проверка состояния аутентификации."""
|
|
||||||
return self._authenticated
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def list_ports() -> List[str]:
|
|
||||||
"""
|
|
||||||
Получение списка доступных COM-портов.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List[str]: Список доступных портов
|
|
||||||
"""
|
|
||||||
return SerialProtocol.list_ports()
|
|
||||||
298
src/core/app.py
298
src/core/app.py
@@ -1,298 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import logging
|
|
||||||
from typing import Optional, Dict, Any
|
|
||||||
|
|
||||||
# Добавляем корневую директорию в PYTHONPATH
|
|
||||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
|
||||||
|
|
||||||
from ui.main_window import MainWindow
|
|
||||||
from core.config import AppConfig
|
|
||||||
from core.state import StateManager
|
|
||||||
from core.events import event_bus, Event, EventTypes
|
|
||||||
from filesystem.logger import setup_logging
|
|
||||||
from filesystem.settings import Settings
|
|
||||||
from filesystem.config_manager import ConfigManager
|
|
||||||
from filesystem.watchers.config_watcher import ConfigWatcher
|
|
||||||
from communication.serial_manager import SerialManager
|
|
||||||
from communication.command_handler import CommandHandler
|
|
||||||
from network.servers.tftp_server import TFTPServer
|
|
||||||
from network.transfer.transfer_manager import TransferManager
|
|
||||||
|
|
||||||
class App:
|
|
||||||
"""Основной класс приложения."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._logger = logging.getLogger(__name__)
|
|
||||||
self._window: Optional[MainWindow] = None
|
|
||||||
|
|
||||||
# Инициализируем компоненты
|
|
||||||
self._init_components()
|
|
||||||
|
|
||||||
# Подписываемся на события
|
|
||||||
self._setup_event_handlers()
|
|
||||||
|
|
||||||
def _init_components(self) -> None:
|
|
||||||
"""Инициализация компонентов приложения."""
|
|
||||||
try:
|
|
||||||
# Создаем необходимые директории
|
|
||||||
AppConfig.create_directories()
|
|
||||||
|
|
||||||
# Инициализируем логирование
|
|
||||||
setup_logging()
|
|
||||||
|
|
||||||
# Загружаем настройки
|
|
||||||
self._settings = Settings()
|
|
||||||
self._settings.load()
|
|
||||||
|
|
||||||
# Создаем менеджеры
|
|
||||||
self._state = StateManager()
|
|
||||||
self._config_manager = ConfigManager()
|
|
||||||
self._config_watcher = ConfigWatcher()
|
|
||||||
self._serial = SerialManager()
|
|
||||||
self._command_handler = CommandHandler(self._serial)
|
|
||||||
self._tftp = TFTPServer()
|
|
||||||
self._transfer = TransferManager(self._serial, self._tftp)
|
|
||||||
|
|
||||||
self._logger.info("Компоненты приложения инициализированы")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка инициализации компонентов: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
def _setup_event_handlers(self) -> None:
|
|
||||||
"""Настройка обработчиков событий."""
|
|
||||||
# Обработка изменения настроек
|
|
||||||
event_bus.subscribe(
|
|
||||||
EventTypes.UI_SETTINGS_CHANGED,
|
|
||||||
self._handle_settings_changed
|
|
||||||
)
|
|
||||||
|
|
||||||
# Обработка событий подключения
|
|
||||||
event_bus.subscribe(
|
|
||||||
EventTypes.CONNECTION_ESTABLISHED,
|
|
||||||
self._handle_connection_established
|
|
||||||
)
|
|
||||||
event_bus.subscribe(
|
|
||||||
EventTypes.CONNECTION_LOST,
|
|
||||||
self._handle_connection_lost
|
|
||||||
)
|
|
||||||
event_bus.subscribe(
|
|
||||||
EventTypes.CONNECTION_ERROR,
|
|
||||||
self._handle_connection_error
|
|
||||||
)
|
|
||||||
|
|
||||||
# Обработка событий передачи
|
|
||||||
event_bus.subscribe(
|
|
||||||
EventTypes.TRANSFER_COMPLETED,
|
|
||||||
self._handle_transfer_completed
|
|
||||||
)
|
|
||||||
event_bus.subscribe(
|
|
||||||
EventTypes.TRANSFER_ERROR,
|
|
||||||
self._handle_transfer_error
|
|
||||||
)
|
|
||||||
|
|
||||||
# Обработка событий конфигурации
|
|
||||||
event_bus.subscribe(
|
|
||||||
EventTypes.CONFIG_MODIFIED,
|
|
||||||
self._handle_config_modified
|
|
||||||
)
|
|
||||||
event_bus.subscribe(
|
|
||||||
EventTypes.CONFIG_DELETED,
|
|
||||||
self._handle_config_deleted
|
|
||||||
)
|
|
||||||
|
|
||||||
def _handle_settings_changed(self, event: Event) -> None:
|
|
||||||
"""
|
|
||||||
Обработка изменения настроек.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Событие изменения настроек
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
settings = event.data
|
|
||||||
|
|
||||||
# Применяем настройки к компонентам
|
|
||||||
self._serial.configure(settings)
|
|
||||||
self._tftp.configure(
|
|
||||||
port=settings.get("tftp_port"),
|
|
||||||
root_dir=AppConfig.CONFIGS_DIR
|
|
||||||
)
|
|
||||||
|
|
||||||
# Сохраняем настройки
|
|
||||||
self._settings.update(settings)
|
|
||||||
|
|
||||||
self._logger.info("Настройки обновлены")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка применения настроек: {e}")
|
|
||||||
|
|
||||||
def _handle_connection_established(self, event: Event) -> None:
|
|
||||||
"""
|
|
||||||
Обработка установки соединения.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Событие установки соединения
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Получаем информацию об устройстве
|
|
||||||
device_info = self._command_handler.get_device_info()
|
|
||||||
|
|
||||||
# Обновляем состояние
|
|
||||||
self._state.update_device_info(device_info)
|
|
||||||
|
|
||||||
self._logger.info("Соединение установлено")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка получения информации об устройстве: {e}")
|
|
||||||
|
|
||||||
def _handle_connection_lost(self, event: Event) -> None:
|
|
||||||
"""
|
|
||||||
Обработка потери соединения.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Событие потери соединения
|
|
||||||
"""
|
|
||||||
# Отменяем все активные передачи
|
|
||||||
self._transfer.cancel_all_transfers()
|
|
||||||
|
|
||||||
# Обновляем состояние
|
|
||||||
self._state.clear_device_info()
|
|
||||||
|
|
||||||
self._logger.info("Соединение потеряно")
|
|
||||||
|
|
||||||
def _handle_connection_error(self, event: Event) -> None:
|
|
||||||
"""
|
|
||||||
Обработка ошибки соединения.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Событие ошибки соединения
|
|
||||||
"""
|
|
||||||
error = event.data
|
|
||||||
self._logger.error(f"Ошибка соединения: {error}")
|
|
||||||
|
|
||||||
def _handle_transfer_completed(self, event: Event) -> None:
|
|
||||||
"""
|
|
||||||
Обработка завершения передачи.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Событие завершения передачи
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
transfer_info = event.data
|
|
||||||
|
|
||||||
# Очищаем завершенные передачи
|
|
||||||
self._transfer.cleanup_transfers()
|
|
||||||
|
|
||||||
self._logger.info(f"Передача завершена: {transfer_info}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка обработки завершения передачи: {e}")
|
|
||||||
|
|
||||||
def _handle_transfer_error(self, event: Event) -> None:
|
|
||||||
"""
|
|
||||||
Обработка ошибки передачи.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Событие ошибки передачи
|
|
||||||
"""
|
|
||||||
error = event.data
|
|
||||||
self._logger.error(f"Ошибка передачи: {error}")
|
|
||||||
|
|
||||||
def _handle_config_modified(self, event: Event) -> None:
|
|
||||||
"""
|
|
||||||
Обработка изменения конфигурации.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Событие изменения конфигурации
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
file_info = event.data
|
|
||||||
|
|
||||||
# Обновляем информацию о файле
|
|
||||||
self._state.update_config_info(file_info)
|
|
||||||
|
|
||||||
self._logger.info(f"Конфигурация изменена: {file_info['name']}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка обработки изменения конфигурации: {e}")
|
|
||||||
|
|
||||||
def _handle_config_deleted(self, event: Event) -> None:
|
|
||||||
"""
|
|
||||||
Обработка удаления конфигурации.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Событие удаления конфигурации
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
file_info = event.data
|
|
||||||
|
|
||||||
# Удаляем информацию о файле
|
|
||||||
self._state.remove_config_info(file_info["name"])
|
|
||||||
|
|
||||||
# Удаляем файл из отслеживания
|
|
||||||
self._config_watcher.remove_watch(file_info["name"])
|
|
||||||
|
|
||||||
self._logger.info(f"Конфигурация удалена: {file_info['name']}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка обработки удаления конфигурации: {e}")
|
|
||||||
|
|
||||||
def start(self) -> None:
|
|
||||||
"""Запуск приложения."""
|
|
||||||
try:
|
|
||||||
# Запускаем наблюдатель за конфигурациями
|
|
||||||
self._config_watcher.start()
|
|
||||||
self._config_watcher.watch_all_configs()
|
|
||||||
|
|
||||||
# Создаем и запускаем главное окно
|
|
||||||
self._window = MainWindow(
|
|
||||||
self._settings,
|
|
||||||
self._state,
|
|
||||||
self._serial,
|
|
||||||
self._transfer,
|
|
||||||
self._config_manager
|
|
||||||
)
|
|
||||||
self._window.mainloop()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка запуска приложения: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
finally:
|
|
||||||
self.cleanup()
|
|
||||||
|
|
||||||
def cleanup(self) -> None:
|
|
||||||
"""Очистка ресурсов приложения."""
|
|
||||||
try:
|
|
||||||
# Останавливаем компоненты
|
|
||||||
if self._serial.is_connected:
|
|
||||||
self._serial.disconnect()
|
|
||||||
|
|
||||||
if self._tftp.is_running:
|
|
||||||
self._tftp.stop()
|
|
||||||
|
|
||||||
if self._config_watcher.is_running:
|
|
||||||
self._config_watcher.stop()
|
|
||||||
|
|
||||||
# Отменяем все передачи
|
|
||||||
self._transfer.cancel_all_transfers()
|
|
||||||
|
|
||||||
# Сохраняем настройки
|
|
||||||
self._settings.save()
|
|
||||||
|
|
||||||
self._logger.info("Ресурсы приложения освобождены")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка очистки ресурсов: {e}")
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Точка входа в приложение."""
|
|
||||||
app = App()
|
|
||||||
app.start()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import os
|
|
||||||
from typing import Dict, List, Optional
|
|
||||||
|
|
||||||
class AppConfig:
|
|
||||||
"""Конфигурация приложения."""
|
|
||||||
|
|
||||||
# Версия приложения
|
|
||||||
VERSION: str = "1.0.0"
|
|
||||||
|
|
||||||
# Директории
|
|
||||||
LOGS_DIR: str = "Logs"
|
|
||||||
CONFIGS_DIR: str = "Configs"
|
|
||||||
SETTINGS_DIR: str = "Settings"
|
|
||||||
FIRMWARE_DIR: str = "Firmware"
|
|
||||||
DOCS_DIR: str = "docs"
|
|
||||||
|
|
||||||
# Файлы
|
|
||||||
LOG_FILE: str = "app.log"
|
|
||||||
SETTINGS_FILE: str = "settings.json"
|
|
||||||
|
|
||||||
# Настройки логирования
|
|
||||||
LOG_MAX_BYTES: int = 5 * 1024 * 1024 # 5 MB
|
|
||||||
LOG_BACKUP_COUNT: int = 3
|
|
||||||
|
|
||||||
# Настройки TFTP
|
|
||||||
TFTP_PORT: int = 69
|
|
||||||
TFTP_TIMEOUT: int = 5
|
|
||||||
TFTP_RETRIES: int = 3
|
|
||||||
|
|
||||||
# Настройки последовательного порта по умолчанию
|
|
||||||
DEFAULT_BAUDRATE: int = 9600
|
|
||||||
DEFAULT_TIMEOUT: int = 10
|
|
||||||
DEFAULT_PROMPT: str = ">"
|
|
||||||
|
|
||||||
# Настройки передачи конфигурации
|
|
||||||
DEFAULT_BLOCK_SIZE: int = 15
|
|
||||||
DEFAULT_COPY_MODE: str = "line"
|
|
||||||
|
|
||||||
# Поддерживаемые скорости передачи
|
|
||||||
SUPPORTED_BAUDRATES: List[int] = [
|
|
||||||
1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200
|
|
||||||
]
|
|
||||||
|
|
||||||
# Поддерживаемые режимы копирования
|
|
||||||
SUPPORTED_COPY_MODES: List[str] = [
|
|
||||||
"line", # Построчный режим
|
|
||||||
"block", # Блочный режим
|
|
||||||
"tftp" # Через TFTP
|
|
||||||
]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def create_directories(cls) -> None:
|
|
||||||
"""Создание необходимых директорий."""
|
|
||||||
directories = [
|
|
||||||
cls.LOGS_DIR,
|
|
||||||
cls.CONFIGS_DIR,
|
|
||||||
cls.SETTINGS_DIR,
|
|
||||||
cls.FIRMWARE_DIR,
|
|
||||||
cls.DOCS_DIR
|
|
||||||
]
|
|
||||||
|
|
||||||
for directory in directories:
|
|
||||||
os.makedirs(directory, exist_ok=True)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_log_path(cls) -> str:
|
|
||||||
"""Получение пути к файлу лога."""
|
|
||||||
return os.path.join(cls.LOGS_DIR, cls.LOG_FILE)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_settings_path(cls) -> str:
|
|
||||||
"""Получение пути к файлу настроек."""
|
|
||||||
return os.path.join(cls.SETTINGS_DIR, cls.SETTINGS_FILE)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_config_path(cls, config_name: str) -> str:
|
|
||||||
"""
|
|
||||||
Получение пути к файлу конфигурации.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
config_name: Имя файла конфигурации
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Полный путь к файлу конфигурации
|
|
||||||
"""
|
|
||||||
return os.path.join(cls.CONFIGS_DIR, config_name)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_firmware_path(cls, firmware_name: str) -> str:
|
|
||||||
"""
|
|
||||||
Получение пути к файлу прошивки.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
firmware_name: Имя файла прошивки
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Полный путь к файлу прошивки
|
|
||||||
"""
|
|
||||||
return os.path.join(cls.FIRMWARE_DIR, firmware_name)
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from typing import Any, Callable, Dict, List, Optional
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Event:
|
|
||||||
"""Базовый класс для событий."""
|
|
||||||
type: str
|
|
||||||
data: Any
|
|
||||||
timestamp: datetime = datetime.now()
|
|
||||||
|
|
||||||
class EventBus:
|
|
||||||
"""Шина событий для асинхронного взаимодействия компонентов."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._subscribers: Dict[str, List[Callable[[Event], None]]] = {}
|
|
||||||
|
|
||||||
def subscribe(self, event_type: str, callback: Callable[[Event], None]) -> None:
|
|
||||||
"""
|
|
||||||
Подписка на событие.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event_type: Тип события
|
|
||||||
callback: Функция обратного вызова
|
|
||||||
"""
|
|
||||||
if event_type not in self._subscribers:
|
|
||||||
self._subscribers[event_type] = []
|
|
||||||
self._subscribers[event_type].append(callback)
|
|
||||||
|
|
||||||
def unsubscribe(self, event_type: str, callback: Callable[[Event], None]) -> None:
|
|
||||||
"""
|
|
||||||
Отписка от события.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event_type: Тип события
|
|
||||||
callback: Функция обратного вызова
|
|
||||||
"""
|
|
||||||
if event_type in self._subscribers:
|
|
||||||
self._subscribers[event_type].remove(callback)
|
|
||||||
|
|
||||||
def publish(self, event: Event) -> None:
|
|
||||||
"""
|
|
||||||
Публикация события.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Объект события
|
|
||||||
"""
|
|
||||||
if event.type in self._subscribers:
|
|
||||||
for callback in self._subscribers[event.type]:
|
|
||||||
callback(event)
|
|
||||||
|
|
||||||
# Создаем глобальный экземпляр шины событий
|
|
||||||
event_bus = EventBus()
|
|
||||||
|
|
||||||
# Определяем типы событий
|
|
||||||
class EventTypes:
|
|
||||||
"""Константы для типов событий."""
|
|
||||||
|
|
||||||
# События подключения
|
|
||||||
CONNECTION_ESTABLISHED = "connection_established"
|
|
||||||
CONNECTION_LOST = "connection_lost"
|
|
||||||
CONNECTION_ERROR = "connection_error"
|
|
||||||
|
|
||||||
# События передачи данных
|
|
||||||
TRANSFER_STARTED = "transfer_started"
|
|
||||||
TRANSFER_PROGRESS = "transfer_progress"
|
|
||||||
TRANSFER_COMPLETED = "transfer_completed"
|
|
||||||
TRANSFER_ERROR = "transfer_error"
|
|
||||||
|
|
||||||
# События конфигурации
|
|
||||||
CONFIG_LOADED = "config_loaded"
|
|
||||||
CONFIG_SAVED = "config_saved"
|
|
||||||
CONFIG_ERROR = "config_error"
|
|
||||||
CONFIG_MODIFIED = "config_modified"
|
|
||||||
CONFIG_DELETED = "config_deleted"
|
|
||||||
CONFIG_CREATED = "config_created"
|
|
||||||
|
|
||||||
# События TFTP
|
|
||||||
TFTP_SERVER_STARTED = "tftp_server_started"
|
|
||||||
TFTP_SERVER_STOPPED = "tftp_server_stopped"
|
|
||||||
TFTP_TRANSFER_STARTED = "tftp_transfer_started"
|
|
||||||
TFTP_TRANSFER_COMPLETED = "tftp_transfer_completed"
|
|
||||||
TFTP_ERROR = "tftp_error"
|
|
||||||
|
|
||||||
# События UI
|
|
||||||
UI_SETTINGS_CHANGED = "ui_settings_changed"
|
|
||||||
UI_STATUS_CHANGED = "ui_status_changed"
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
class ComConfigError(Exception):
|
|
||||||
"""Базовый класс для всех исключений приложения."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
class ConnectionError(ComConfigError):
|
|
||||||
"""Ошибка подключения к устройству."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
class AuthenticationError(ConnectionError):
|
|
||||||
"""Ошибка аутентификации."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
class ConfigError(ComConfigError):
|
|
||||||
"""Ошибка работы с конфигурацией."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
class TransferError(ComConfigError):
|
|
||||||
"""Ошибка передачи данных."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
class TFTPError(ComConfigError):
|
|
||||||
"""Ошибка TFTP сервера."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
class ValidationError(ComConfigError):
|
|
||||||
"""Ошибка валидации данных."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
class SettingsError(ComConfigError):
|
|
||||||
"""Ошибка работы с настройками."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
class FileSystemError(ComConfigError):
|
|
||||||
"""Ошибка работы с файловой системой."""
|
|
||||||
pass
|
|
||||||
@@ -1,412 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from typing import Any, Dict, Optional, List
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from enum import Enum, auto
|
|
||||||
import logging
|
|
||||||
from datetime import datetime
|
|
||||||
from .events import event_bus, Event, EventTypes
|
|
||||||
|
|
||||||
class ConnectionState(Enum):
|
|
||||||
"""Состояния подключения."""
|
|
||||||
DISCONNECTED = auto()
|
|
||||||
CONNECTING = auto()
|
|
||||||
CONNECTED = auto()
|
|
||||||
ERROR = auto()
|
|
||||||
|
|
||||||
class TransferState(Enum):
|
|
||||||
"""Состояния передачи данных."""
|
|
||||||
IDLE = auto()
|
|
||||||
PREPARING = auto()
|
|
||||||
TRANSFERRING = auto()
|
|
||||||
COMPLETED = auto()
|
|
||||||
ERROR = auto()
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class DeviceInfo:
|
|
||||||
"""Информация об устройстве."""
|
|
||||||
version: Optional[str] = None
|
|
||||||
model: Optional[str] = None
|
|
||||||
hostname: Optional[str] = None
|
|
||||||
interfaces: List[Dict[str, Any]] = field(default_factory=list)
|
|
||||||
last_update: Optional[datetime] = None
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ConfigInfo:
|
|
||||||
"""Информация о файле конфигурации."""
|
|
||||||
name: str
|
|
||||||
path: str
|
|
||||||
size: int
|
|
||||||
modified: datetime
|
|
||||||
created: datetime
|
|
||||||
is_watched: bool = False
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ApplicationState:
|
|
||||||
"""Состояние приложения."""
|
|
||||||
connection_state: ConnectionState = ConnectionState.DISCONNECTED
|
|
||||||
transfer_state: TransferState = TransferState.IDLE
|
|
||||||
device_info: DeviceInfo = field(default_factory=DeviceInfo)
|
|
||||||
configs: Dict[str, ConfigInfo] = field(default_factory=dict)
|
|
||||||
current_config: Optional[str] = None
|
|
||||||
transfer_progress: float = 0.0
|
|
||||||
status_message: str = ""
|
|
||||||
error_message: Optional[str] = None
|
|
||||||
is_tftp_server_running: bool = False
|
|
||||||
custom_data: Dict[str, Any] = field(default_factory=dict)
|
|
||||||
|
|
||||||
class StateManager:
|
|
||||||
"""Менеджер состояния приложения."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._state = ApplicationState()
|
|
||||||
self._logger = logging.getLogger(__name__)
|
|
||||||
self._setup_event_handlers()
|
|
||||||
|
|
||||||
def _setup_event_handlers(self) -> None:
|
|
||||||
"""Настройка обработчиков событий."""
|
|
||||||
event_bus.subscribe(EventTypes.CONNECTION_ESTABLISHED, self._handle_connection_established)
|
|
||||||
event_bus.subscribe(EventTypes.CONNECTION_LOST, self._handle_connection_lost)
|
|
||||||
event_bus.subscribe(EventTypes.CONNECTION_ERROR, self._handle_connection_error)
|
|
||||||
event_bus.subscribe(EventTypes.TRANSFER_STARTED, self._handle_transfer_started)
|
|
||||||
event_bus.subscribe(EventTypes.TRANSFER_PROGRESS, self._handle_transfer_progress)
|
|
||||||
event_bus.subscribe(EventTypes.TRANSFER_COMPLETED, self._handle_transfer_completed)
|
|
||||||
event_bus.subscribe(EventTypes.TRANSFER_ERROR, self._handle_transfer_error)
|
|
||||||
event_bus.subscribe(EventTypes.TFTP_SERVER_STARTED, self._handle_tftp_server_started)
|
|
||||||
event_bus.subscribe(EventTypes.TFTP_SERVER_STOPPED, self._handle_tftp_server_stopped)
|
|
||||||
|
|
||||||
# Подписка на события конфигурации
|
|
||||||
event_bus.subscribe(EventTypes.CONFIG_CREATED, self._handle_config_created)
|
|
||||||
event_bus.subscribe(EventTypes.CONFIG_MODIFIED, self._handle_config_modified)
|
|
||||||
event_bus.subscribe(EventTypes.CONFIG_DELETED, self._handle_config_deleted)
|
|
||||||
|
|
||||||
def _handle_connection_established(self, event: Event) -> None:
|
|
||||||
self._state.connection_state = ConnectionState.CONNECTED
|
|
||||||
self._state.error_message = None
|
|
||||||
self._update_status("Подключение установлено")
|
|
||||||
|
|
||||||
def _handle_connection_lost(self, event: Event) -> None:
|
|
||||||
self._state.connection_state = ConnectionState.DISCONNECTED
|
|
||||||
self._update_status("Подключение потеряно")
|
|
||||||
|
|
||||||
def _handle_connection_error(self, event: Event) -> None:
|
|
||||||
self._state.connection_state = ConnectionState.ERROR
|
|
||||||
self._state.error_message = str(event.data)
|
|
||||||
self._update_status(f"Ошибка подключения: {event.data}")
|
|
||||||
|
|
||||||
def _handle_transfer_started(self, event: Event) -> None:
|
|
||||||
self._state.transfer_state = TransferState.TRANSFERRING
|
|
||||||
self._state.transfer_progress = 0.0
|
|
||||||
self._update_status("Передача данных начата")
|
|
||||||
|
|
||||||
def _handle_transfer_progress(self, event: Event) -> None:
|
|
||||||
self._state.transfer_progress = float(event.data)
|
|
||||||
self._update_status(f"Прогресс передачи: {self._state.transfer_progress:.1f}%")
|
|
||||||
|
|
||||||
def _handle_transfer_completed(self, event: Event) -> None:
|
|
||||||
self._state.transfer_state = TransferState.COMPLETED
|
|
||||||
self._state.transfer_progress = 100.0
|
|
||||||
self._update_status("Передача данных завершена")
|
|
||||||
|
|
||||||
def _handle_transfer_error(self, event: Event) -> None:
|
|
||||||
self._state.transfer_state = TransferState.ERROR
|
|
||||||
self._state.error_message = str(event.data)
|
|
||||||
self._update_status(f"Ошибка передачи: {event.data}")
|
|
||||||
|
|
||||||
def _handle_tftp_server_started(self, event: Event) -> None:
|
|
||||||
self._state.is_tftp_server_running = True
|
|
||||||
self._update_status("TFTP сервер запущен")
|
|
||||||
|
|
||||||
def _handle_tftp_server_stopped(self, event: Event) -> None:
|
|
||||||
self._state.is_tftp_server_running = False
|
|
||||||
self._update_status("TFTP сервер остановлен")
|
|
||||||
|
|
||||||
def _handle_config_created(self, event: Event) -> None:
|
|
||||||
"""
|
|
||||||
Обработка создания конфигурации.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Событие создания конфигурации
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
self.update_config_info(event.data)
|
|
||||||
self._update_status(f"Конфигурация создана: {event.data['name']}")
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка обработки создания конфигурации: {e}")
|
|
||||||
|
|
||||||
def _handle_config_modified(self, event: Event) -> None:
|
|
||||||
"""
|
|
||||||
Обработка изменения конфигурации.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Событие изменения конфигурации
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
self.update_config_info(event.data)
|
|
||||||
self._update_status(f"Конфигурация изменена: {event.data['name']}")
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка обработки изменения конфигурации: {e}")
|
|
||||||
|
|
||||||
def _handle_config_deleted(self, event: Event) -> None:
|
|
||||||
"""
|
|
||||||
Обработка удаления конфигурации.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Событие удаления конфигурации
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
config_name = event.data['name']
|
|
||||||
self.remove_config_info(config_name)
|
|
||||||
self._update_status(f"Конфигурация удалена: {config_name}")
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка обработки удаления конфигурации: {e}")
|
|
||||||
|
|
||||||
def _update_status(self, message: str) -> None:
|
|
||||||
"""Обновление статусного сообщения."""
|
|
||||||
self._state.status_message = message
|
|
||||||
event_bus.publish(Event(EventTypes.UI_STATUS_CHANGED, message))
|
|
||||||
|
|
||||||
def update_device_info(self, info: Dict[str, Any]) -> None:
|
|
||||||
"""
|
|
||||||
Обновление информации об устройстве.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
info: Словарь с информацией об устройстве
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
self._state.device_info = DeviceInfo(
|
|
||||||
version=info.get("version"),
|
|
||||||
model=info.get("model"),
|
|
||||||
hostname=info.get("hostname"),
|
|
||||||
interfaces=info.get("interfaces", []),
|
|
||||||
last_update=datetime.now()
|
|
||||||
)
|
|
||||||
|
|
||||||
# Обновляем состояние подключения
|
|
||||||
self._state.connection_state = ConnectionState.CONNECTED
|
|
||||||
|
|
||||||
# Уведомляем об изменении состояния
|
|
||||||
event_bus.publish(Event(EventTypes.UI_STATUS_CHANGED, {
|
|
||||||
"message": f"Подключено к {self._state.device_info.hostname}"
|
|
||||||
}))
|
|
||||||
|
|
||||||
self._logger.info("Информация об устройстве обновлена")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка обновления информации об устройстве: {e}")
|
|
||||||
self._state.connection_state = ConnectionState.ERROR
|
|
||||||
self._state.error_message = str(e)
|
|
||||||
|
|
||||||
def clear_device_info(self) -> None:
|
|
||||||
"""Очистка информации об устройстве."""
|
|
||||||
self._state.device_info = DeviceInfo()
|
|
||||||
self._state.connection_state = ConnectionState.DISCONNECTED
|
|
||||||
self._state.error_message = None
|
|
||||||
|
|
||||||
# Уведомляем об изменении состояния
|
|
||||||
event_bus.publish(Event(EventTypes.UI_STATUS_CHANGED, {
|
|
||||||
"message": "Отключено от устройства"
|
|
||||||
}))
|
|
||||||
|
|
||||||
self._logger.info("Информация об устройстве очищена")
|
|
||||||
|
|
||||||
def update_config_info(self, info: Dict[str, Any]) -> None:
|
|
||||||
"""
|
|
||||||
Обновление информации о файле конфигурации.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
info: Словарь с информацией о файле
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
config = ConfigInfo(
|
|
||||||
name=info["name"],
|
|
||||||
path=info["path"],
|
|
||||||
size=info["size"],
|
|
||||||
modified=info["modified"],
|
|
||||||
created=info["created"],
|
|
||||||
is_watched=info.get("is_watched", False)
|
|
||||||
)
|
|
||||||
|
|
||||||
self._state.configs[config.name] = config
|
|
||||||
|
|
||||||
# Уведомляем об изменении состояния
|
|
||||||
event_bus.publish(Event(EventTypes.UI_STATUS_CHANGED, {
|
|
||||||
"message": f"Конфигурация обновлена: {config.name}"
|
|
||||||
}))
|
|
||||||
|
|
||||||
self._logger.debug(f"Обновлена информация о конфигурации: {config.name}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка обновления информации о конфигурации: {e}")
|
|
||||||
|
|
||||||
def remove_config_info(self, config_name: str) -> None:
|
|
||||||
"""
|
|
||||||
Удаление информации о файле конфигурации.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
config_name: Имя файла конфигурации
|
|
||||||
"""
|
|
||||||
if config_name in self._state.configs:
|
|
||||||
self._state.configs.pop(config_name)
|
|
||||||
|
|
||||||
# Если удаляется текущая конфигурация, очищаем её
|
|
||||||
if self._state.current_config == config_name:
|
|
||||||
self._state.current_config = None
|
|
||||||
|
|
||||||
# Уведомляем об изменении состояния
|
|
||||||
event_bus.publish(Event(EventTypes.UI_STATUS_CHANGED, {
|
|
||||||
"message": f"Конфигурация удалена: {config_name}"
|
|
||||||
}))
|
|
||||||
|
|
||||||
self._logger.debug(f"Удалена информация о конфигурации: {config_name}")
|
|
||||||
|
|
||||||
def set_current_config(self, config_name: Optional[str]) -> None:
|
|
||||||
"""
|
|
||||||
Установка текущей конфигурации.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
config_name: Имя файла конфигурации
|
|
||||||
"""
|
|
||||||
if config_name is None or config_name in self._state.configs:
|
|
||||||
self._state.current_config = config_name
|
|
||||||
|
|
||||||
if config_name:
|
|
||||||
self._logger.debug(f"Установлена текущая конфигурация: {config_name}")
|
|
||||||
else:
|
|
||||||
self._logger.debug("Текущая конфигурация очищена")
|
|
||||||
|
|
||||||
def update_transfer_state(self, state: TransferState, progress: float = 0.0,
|
|
||||||
error: Optional[str] = None) -> None:
|
|
||||||
"""
|
|
||||||
Обновление состояния передачи.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
state: Новое состояние
|
|
||||||
progress: Прогресс передачи
|
|
||||||
error: Сообщение об ошибке
|
|
||||||
"""
|
|
||||||
self._state.transfer_state = state
|
|
||||||
self._state.transfer_progress = progress
|
|
||||||
self._state.error_message = error
|
|
||||||
|
|
||||||
# Формируем сообщение о состоянии
|
|
||||||
if state == TransferState.IDLE:
|
|
||||||
message = "Готов к передаче"
|
|
||||||
elif state == TransferState.PREPARING:
|
|
||||||
message = "Подготовка к передаче..."
|
|
||||||
elif state == TransferState.TRANSFERRING:
|
|
||||||
message = f"Передача данных: {progress:.1f}%"
|
|
||||||
elif state == TransferState.COMPLETED:
|
|
||||||
message = "Передача завершена"
|
|
||||||
elif state == TransferState.ERROR:
|
|
||||||
message = f"Ошибка передачи: {error}"
|
|
||||||
else:
|
|
||||||
message = "Неизвестное состояние"
|
|
||||||
|
|
||||||
# Уведомляем об изменении состояния
|
|
||||||
event_bus.publish(Event(EventTypes.UI_STATUS_CHANGED, {
|
|
||||||
"message": message
|
|
||||||
}))
|
|
||||||
|
|
||||||
self._logger.debug(f"Состояние передачи обновлено: {state.name}")
|
|
||||||
|
|
||||||
def set_tftp_server_state(self, is_running: bool) -> None:
|
|
||||||
"""
|
|
||||||
Установка состояния TFTP сервера.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
is_running: Флаг работы сервера
|
|
||||||
"""
|
|
||||||
self._state.is_tftp_server_running = is_running
|
|
||||||
|
|
||||||
# Уведомляем об изменении состояния
|
|
||||||
event_bus.publish(Event(EventTypes.UI_STATUS_CHANGED, {
|
|
||||||
"message": "TFTP сервер запущен" if is_running else "TFTP сервер остановлен"
|
|
||||||
}))
|
|
||||||
|
|
||||||
self._logger.debug(f"Состояние TFTP сервера: {'запущен' if is_running else 'остановлен'}")
|
|
||||||
|
|
||||||
def set_status_message(self, message: str) -> None:
|
|
||||||
"""
|
|
||||||
Установка сообщения о состоянии.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
message: Сообщение
|
|
||||||
"""
|
|
||||||
self._state.status_message = message
|
|
||||||
|
|
||||||
# Уведомляем об изменении состояния
|
|
||||||
event_bus.publish(Event(EventTypes.UI_STATUS_CHANGED, {
|
|
||||||
"message": message
|
|
||||||
}))
|
|
||||||
|
|
||||||
def set_error_message(self, error: Optional[str]) -> None:
|
|
||||||
"""
|
|
||||||
Установка сообщения об ошибке.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
error: Сообщение об ошибке
|
|
||||||
"""
|
|
||||||
self._state.error_message = error
|
|
||||||
|
|
||||||
if error:
|
|
||||||
# Уведомляем об ошибке
|
|
||||||
event_bus.publish(Event(EventTypes.UI_STATUS_CHANGED, {
|
|
||||||
"message": f"Ошибка: {error}"
|
|
||||||
}))
|
|
||||||
|
|
||||||
def get_device_info(self) -> DeviceInfo:
|
|
||||||
"""
|
|
||||||
Получение информации об устройстве.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
DeviceInfo: Информация об устройстве
|
|
||||||
"""
|
|
||||||
return self._state.device_info
|
|
||||||
|
|
||||||
def get_config_info(self, config_name: str) -> Optional[ConfigInfo]:
|
|
||||||
"""
|
|
||||||
Получение информации о конфигурации.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
config_name: Имя файла конфигурации
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Optional[ConfigInfo]: Информация о конфигурации или None
|
|
||||||
"""
|
|
||||||
return self._state.configs.get(config_name)
|
|
||||||
|
|
||||||
def get_configs(self) -> Dict[str, ConfigInfo]:
|
|
||||||
"""
|
|
||||||
Получение списка всех конфигураций.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict[str, ConfigInfo]: Словарь с информацией о конфигурациях
|
|
||||||
"""
|
|
||||||
return self._state.configs.copy()
|
|
||||||
|
|
||||||
def get_current_config(self) -> Optional[str]:
|
|
||||||
"""
|
|
||||||
Получение текущей конфигурации.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Optional[str]: Имя текущей конфигурации или None
|
|
||||||
"""
|
|
||||||
return self._state.current_config
|
|
||||||
|
|
||||||
@property
|
|
||||||
def state(self) -> ApplicationState:
|
|
||||||
"""Получение текущего состояния."""
|
|
||||||
return self._state
|
|
||||||
|
|
||||||
def set_custom_data(self, key: str, value: Any) -> None:
|
|
||||||
"""Установка пользовательских данных."""
|
|
||||||
self._state.custom_data[key] = value
|
|
||||||
|
|
||||||
def get_custom_data(self, key: str, default: Any = None) -> Any:
|
|
||||||
"""Получение пользовательских данных."""
|
|
||||||
return self._state.custom_data.get(key, default)
|
|
||||||
|
|
||||||
# Создаем глобальный экземпляр менеджера состояния
|
|
||||||
state_manager = StateManager()
|
|
||||||
@@ -1,211 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
from typing import List, Optional, Dict, Any
|
|
||||||
import logging
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from core.config import AppConfig
|
|
||||||
from core.exceptions import FileSystemError
|
|
||||||
from core.events import event_bus, Event, EventTypes
|
|
||||||
|
|
||||||
class ConfigManager:
|
|
||||||
"""Менеджер для работы с файлами конфигураций."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._logger = logging.getLogger(__name__)
|
|
||||||
self._config_dir = AppConfig.CONFIGS_DIR
|
|
||||||
|
|
||||||
# Создаем директорию, если её нет
|
|
||||||
os.makedirs(self._config_dir, exist_ok=True)
|
|
||||||
|
|
||||||
def save_config(self, config_name: str, content: str,
|
|
||||||
make_backup: bool = True) -> str:
|
|
||||||
"""
|
|
||||||
Сохранение конфигурации в файл.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
config_name: Имя файла конфигурации
|
|
||||||
content: Содержимое конфигурации
|
|
||||||
make_backup: Создавать ли резервную копию
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Путь к сохраненному файлу
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
FileSystemError: При ошибке сохранения
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Формируем путь к файлу
|
|
||||||
config_path = os.path.join(self._config_dir, config_name)
|
|
||||||
|
|
||||||
# Создаем резервную копию, если файл существует
|
|
||||||
if make_backup and os.path.exists(config_path):
|
|
||||||
self._create_backup(config_path)
|
|
||||||
|
|
||||||
# Сохраняем конфигурацию
|
|
||||||
with open(config_path, "w", encoding="utf-8") as f:
|
|
||||||
f.write(content)
|
|
||||||
|
|
||||||
event_bus.publish(Event(EventTypes.CONFIG_SAVED, {
|
|
||||||
"file": config_path,
|
|
||||||
"size": len(content)
|
|
||||||
}))
|
|
||||||
|
|
||||||
self._logger.info(f"Конфигурация сохранена: {config_name}")
|
|
||||||
return config_path
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка сохранения конфигурации: {e}")
|
|
||||||
raise FileSystemError(f"Ошибка сохранения конфигурации: {e}")
|
|
||||||
|
|
||||||
def load_config(self, config_name: str) -> str:
|
|
||||||
"""
|
|
||||||
Загрузка конфигурации из файла.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
config_name: Имя файла конфигурации
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Содержимое конфигурации
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
FileSystemError: При ошибке загрузки
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
config_path = os.path.join(self._config_dir, config_name)
|
|
||||||
|
|
||||||
if not os.path.exists(config_path):
|
|
||||||
raise FileSystemError(f"Файл не найден: {config_name}")
|
|
||||||
|
|
||||||
with open(config_path, "r", encoding="utf-8") as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
event_bus.publish(Event(EventTypes.CONFIG_LOADED, {
|
|
||||||
"file": config_path,
|
|
||||||
"size": len(content)
|
|
||||||
}))
|
|
||||||
|
|
||||||
self._logger.info(f"Конфигурация загружена: {config_name}")
|
|
||||||
return content
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка загрузки конфигурации: {e}")
|
|
||||||
raise FileSystemError(f"Ошибка загрузки конфигурации: {e}")
|
|
||||||
|
|
||||||
def delete_config(self, config_name: str, keep_backup: bool = True) -> None:
|
|
||||||
"""
|
|
||||||
Удаление файла конфигурации.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
config_name: Имя файла конфигурации
|
|
||||||
keep_backup: Сохранить ли резервную копию
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
FileSystemError: При ошибке удаления
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
config_path = os.path.join(self._config_dir, config_name)
|
|
||||||
|
|
||||||
if not os.path.exists(config_path):
|
|
||||||
return
|
|
||||||
|
|
||||||
# Создаем резервную копию перед удалением
|
|
||||||
if keep_backup:
|
|
||||||
self._create_backup(config_path)
|
|
||||||
|
|
||||||
# Удаляем файл
|
|
||||||
os.remove(config_path)
|
|
||||||
|
|
||||||
event_bus.publish(Event(EventTypes.CONFIG_DELETED, {
|
|
||||||
"file": config_path
|
|
||||||
}))
|
|
||||||
|
|
||||||
self._logger.info(f"Конфигурация удалена: {config_name}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка удаления конфигурации: {e}")
|
|
||||||
raise FileSystemError(f"Ошибка удаления конфигурации: {e}")
|
|
||||||
|
|
||||||
def list_configs(self) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
Получение списка файлов конфигураций.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List[Dict[str, Any]]: Список с информацией о файлах
|
|
||||||
"""
|
|
||||||
configs = []
|
|
||||||
|
|
||||||
try:
|
|
||||||
for filename in os.listdir(self._config_dir):
|
|
||||||
file_path = os.path.join(self._config_dir, filename)
|
|
||||||
if os.path.isfile(file_path):
|
|
||||||
stat = os.stat(file_path)
|
|
||||||
configs.append({
|
|
||||||
"name": filename,
|
|
||||||
"size": stat.st_size,
|
|
||||||
"modified": datetime.fromtimestamp(stat.st_mtime),
|
|
||||||
"created": datetime.fromtimestamp(stat.st_ctime)
|
|
||||||
})
|
|
||||||
|
|
||||||
return sorted(configs, key=lambda x: x["modified"], reverse=True)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка получения списка конфигураций: {e}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
def get_config_info(self, config_name: str) -> Optional[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
Получение информации о файле конфигурации.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
config_name: Имя файла конфигурации
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Optional[Dict[str, Any]]: Информация о файле или None
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
config_path = os.path.join(self._config_dir, config_name)
|
|
||||||
|
|
||||||
if not os.path.exists(config_path):
|
|
||||||
return None
|
|
||||||
|
|
||||||
stat = os.stat(config_path)
|
|
||||||
return {
|
|
||||||
"name": config_name,
|
|
||||||
"size": stat.st_size,
|
|
||||||
"modified": datetime.fromtimestamp(stat.st_mtime),
|
|
||||||
"created": datetime.fromtimestamp(stat.st_ctime),
|
|
||||||
"path": config_path
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка получения информации о конфигурации: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _create_backup(self, file_path: str) -> None:
|
|
||||||
"""
|
|
||||||
Создание резервной копии файла.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
file_path: Путь к файлу
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
FileSystemError: При ошибке создания копии
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Формируем имя для резервной копии
|
|
||||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
||||||
backup_name = f"{os.path.basename(file_path)}.{timestamp}.bak"
|
|
||||||
backup_path = os.path.join(self._config_dir, backup_name)
|
|
||||||
|
|
||||||
# Копируем файл
|
|
||||||
shutil.copy2(file_path, backup_path)
|
|
||||||
|
|
||||||
self._logger.info(f"Создана резервная копия: {backup_name}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка создания резервной копии: {e}")
|
|
||||||
raise FileSystemError(f"Ошибка создания резервной копии: {e}")
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import os
|
|
||||||
import logging
|
|
||||||
from logging.handlers import RotatingFileHandler
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
def setup_logging(
|
|
||||||
log_dir: str = "Logs",
|
|
||||||
log_file: str = "app.log",
|
|
||||||
max_bytes: int = 5 * 1024 * 1024, # 5 MB
|
|
||||||
backup_count: int = 3,
|
|
||||||
log_level: int = logging.DEBUG,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Настройка системы логирования.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
log_dir: Директория для хранения логов
|
|
||||||
log_file: Имя файла лога
|
|
||||||
max_bytes: Максимальный размер файла лога
|
|
||||||
backup_count: Количество файлов ротации
|
|
||||||
log_level: Уровень логирования
|
|
||||||
"""
|
|
||||||
# Создаем директорию для логов, если её нет
|
|
||||||
os.makedirs(log_dir, exist_ok=True)
|
|
||||||
|
|
||||||
# Настраиваем корневой логгер
|
|
||||||
logger = logging.getLogger()
|
|
||||||
logger.setLevel(log_level)
|
|
||||||
|
|
||||||
# Очищаем существующие обработчики
|
|
||||||
logger.handlers = []
|
|
||||||
|
|
||||||
# Создаем форматтер
|
|
||||||
formatter = logging.Formatter(
|
|
||||||
"%(asctime)s - %(levelname)s - %(message)s",
|
|
||||||
datefmt="%Y-%m-%d %H:%M:%S"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Добавляем обработчик файла с ротацией
|
|
||||||
log_path = os.path.join(log_dir, log_file)
|
|
||||||
file_handler = RotatingFileHandler(
|
|
||||||
log_path,
|
|
||||||
maxBytes=max_bytes,
|
|
||||||
backupCount=backup_count,
|
|
||||||
encoding="utf-8"
|
|
||||||
)
|
|
||||||
file_handler.setFormatter(formatter)
|
|
||||||
logger.addHandler(file_handler)
|
|
||||||
|
|
||||||
# Добавляем обработчик консоли для отладки
|
|
||||||
console_handler = logging.StreamHandler()
|
|
||||||
console_handler.setFormatter(formatter)
|
|
||||||
logger.addHandler(console_handler)
|
|
||||||
|
|
||||||
logging.info("Система логирования инициализирована")
|
|
||||||
|
|
||||||
def get_logger(name: Optional[str] = None) -> logging.Logger:
|
|
||||||
"""
|
|
||||||
Получение логгера с указанным именем.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: Имя логгера
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
logging.Logger: Настроенный логгер
|
|
||||||
"""
|
|
||||||
return logging.getLogger(name)
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import logging
|
|
||||||
from typing import Dict, Any
|
|
||||||
|
|
||||||
class Settings:
|
|
||||||
"""Класс для работы с настройками приложения."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.settings_dir = "Settings"
|
|
||||||
self.settings_file = os.path.join(self.settings_dir, "settings.json")
|
|
||||||
self._settings = self._get_default_settings()
|
|
||||||
|
|
||||||
# Создаем директорию для настроек, если её нет
|
|
||||||
os.makedirs(self.settings_dir, exist_ok=True)
|
|
||||||
|
|
||||||
def _get_default_settings(self) -> Dict[str, Any]:
|
|
||||||
"""Возвращает настройки по умолчанию."""
|
|
||||||
return {
|
|
||||||
"port": None, # Порт для подключения
|
|
||||||
"baudrate": 9600, # Скорость передачи данных
|
|
||||||
"config_file": None, # Файл конфигурации
|
|
||||||
"login": None, # Логин для подключения
|
|
||||||
"password": None, # Пароль для подключения
|
|
||||||
"timeout": 10, # Таймаут подключения
|
|
||||||
"copy_mode": "line", # Режим копирования
|
|
||||||
"block_size": 15, # Размер блока команд
|
|
||||||
"prompt": ">", # Используется для определения приглашения
|
|
||||||
}
|
|
||||||
|
|
||||||
def load(self) -> None:
|
|
||||||
"""Загружает настройки из файла."""
|
|
||||||
if not os.path.exists(self.settings_file):
|
|
||||||
self.save()
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(self.settings_file, "r", encoding="utf-8") as f:
|
|
||||||
loaded_settings = json.load(f)
|
|
||||||
|
|
||||||
# Проверяем наличие всех необходимых параметров
|
|
||||||
default_settings = self._get_default_settings()
|
|
||||||
for key, value in default_settings.items():
|
|
||||||
if key not in loaded_settings:
|
|
||||||
loaded_settings[key] = value
|
|
||||||
|
|
||||||
self._settings = loaded_settings
|
|
||||||
self.save() # Сохраняем обновленные настройки
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Ошибка при загрузке настроек: {e}", exc_info=True)
|
|
||||||
self._settings = self._get_default_settings()
|
|
||||||
self.save()
|
|
||||||
|
|
||||||
def save(self) -> None:
|
|
||||||
"""Сохраняет настройки в файл."""
|
|
||||||
try:
|
|
||||||
with open(self.settings_file, "w", encoding="utf-8") as f:
|
|
||||||
json.dump(self._settings, f, indent=4, ensure_ascii=False)
|
|
||||||
logging.info("Настройки успешно сохранены")
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Ошибка при сохранении настроек: {e}", exc_info=True)
|
|
||||||
|
|
||||||
def get(self, key: str, default: Any = None) -> Any:
|
|
||||||
"""Возвращает значение настройки по ключу."""
|
|
||||||
return self._settings.get(key, default)
|
|
||||||
|
|
||||||
def set(self, key: str, value: Any) -> None:
|
|
||||||
"""Устанавливает значение настройки."""
|
|
||||||
self._settings[key] = value
|
|
||||||
|
|
||||||
def update(self, settings: Dict[str, Any]) -> None:
|
|
||||||
"""Обновляет несколько настроек одновременно."""
|
|
||||||
self._settings.update(settings)
|
|
||||||
self.save()
|
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
from typing import Optional, Dict, Set, Callable, Any
|
|
||||||
import logging
|
|
||||||
from datetime import datetime
|
|
||||||
from watchdog.observers import Observer
|
|
||||||
from watchdog.events import FileSystemEventHandler, FileSystemEvent
|
|
||||||
|
|
||||||
from core.config import AppConfig
|
|
||||||
from core.exceptions import FileSystemError
|
|
||||||
from core.events import event_bus, Event, EventTypes
|
|
||||||
|
|
||||||
class ConfigEventHandler(FileSystemEventHandler):
|
|
||||||
"""Обработчик событий файловой системы для конфигураций."""
|
|
||||||
|
|
||||||
def __init__(self, callback: Callable[[str, str], None]):
|
|
||||||
self._callback = callback
|
|
||||||
|
|
||||||
def on_created(self, event: FileSystemEvent) -> None:
|
|
||||||
"""
|
|
||||||
Обработка создания файла.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Событие файловой системы
|
|
||||||
"""
|
|
||||||
if not event.is_directory:
|
|
||||||
self._callback(event.src_path, "created")
|
|
||||||
|
|
||||||
def on_modified(self, event: FileSystemEvent) -> None:
|
|
||||||
"""
|
|
||||||
Обработка изменения файла.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Событие файловой системы
|
|
||||||
"""
|
|
||||||
if not event.is_directory:
|
|
||||||
self._callback(event.src_path, "modified")
|
|
||||||
|
|
||||||
def on_deleted(self, event: FileSystemEvent) -> None:
|
|
||||||
"""
|
|
||||||
Обработка удаления файла.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Событие файловой системы
|
|
||||||
"""
|
|
||||||
if not event.is_directory:
|
|
||||||
self._callback(event.src_path, "deleted")
|
|
||||||
|
|
||||||
class ConfigWatcher:
|
|
||||||
"""Наблюдатель за изменениями файлов конфигурации."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._logger = logging.getLogger(__name__)
|
|
||||||
self._observer: Optional[Observer] = None
|
|
||||||
self._watched_files: Set[str] = set()
|
|
||||||
self._config_dir = AppConfig.CONFIGS_DIR
|
|
||||||
|
|
||||||
# Создаем директорию, если её нет
|
|
||||||
os.makedirs(self._config_dir, exist_ok=True)
|
|
||||||
|
|
||||||
def get_file_info(self, filename: str) -> Optional[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
Получение информации о файле.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
filename: Имя файла
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Optional[Dict[str, Any]]: Информация о файле или None
|
|
||||||
"""
|
|
||||||
file_path = os.path.join(self._config_dir, filename)
|
|
||||||
|
|
||||||
if not os.path.exists(file_path):
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
stat = os.stat(file_path)
|
|
||||||
return {
|
|
||||||
"name": filename,
|
|
||||||
"path": file_path,
|
|
||||||
"size": stat.st_size,
|
|
||||||
"modified": datetime.fromtimestamp(stat.st_mtime),
|
|
||||||
"created": datetime.fromtimestamp(stat.st_ctime),
|
|
||||||
"is_watched": file_path in self._watched_files
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка получения информации о файле: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def start(self) -> None:
|
|
||||||
"""
|
|
||||||
Запуск наблюдателя.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
FileSystemError: При ошибке запуска
|
|
||||||
"""
|
|
||||||
if self._observer is not None:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._observer = Observer()
|
|
||||||
handler = ConfigEventHandler(self._handle_event)
|
|
||||||
|
|
||||||
# Начинаем наблюдение за директорией
|
|
||||||
self._observer.schedule(handler, self._config_dir, recursive=False)
|
|
||||||
self._observer.start()
|
|
||||||
|
|
||||||
self._logger.info(f"Наблюдатель запущен: {self._config_dir}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Неожиданная ошибка: {e}")
|
|
||||||
raise FileSystemError(f"Неожиданная ошибка: {e}")
|
|
||||||
|
|
||||||
def stop(self) -> None:
|
|
||||||
"""
|
|
||||||
Остановка наблюдателя.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
FileSystemError: При ошибке остановки
|
|
||||||
"""
|
|
||||||
if self._observer is not None:
|
|
||||||
try:
|
|
||||||
self._observer.stop()
|
|
||||||
self._observer.join()
|
|
||||||
self._observer = None
|
|
||||||
self._watched_files.clear()
|
|
||||||
|
|
||||||
self._logger.info("Наблюдатель остановлен")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка остановки наблюдателя: {e}")
|
|
||||||
raise FileSystemError(f"Ошибка остановки наблюдателя: {e}")
|
|
||||||
|
|
||||||
def add_watch(self, filename: str) -> None:
|
|
||||||
"""
|
|
||||||
Добавление файла для отслеживания.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
filename: Имя файла
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
FileSystemError: При ошибке добавления файла
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
file_path = os.path.join(self._config_dir, filename)
|
|
||||||
|
|
||||||
if not os.path.exists(file_path):
|
|
||||||
raise FileSystemError(f"Файл не найден: {filename}")
|
|
||||||
|
|
||||||
self._watched_files.add(file_path)
|
|
||||||
self._logger.debug(f"Добавлен файл для отслеживания: {filename}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка добавления файла для отслеживания: {e}")
|
|
||||||
raise FileSystemError(f"Ошибка добавления файла для отслеживания: {e}")
|
|
||||||
|
|
||||||
def remove_watch(self, filename: str) -> None:
|
|
||||||
"""
|
|
||||||
Удаление файла из отслеживания.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
filename: Имя файла
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
FileSystemError: При ошибке удаления файла
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
file_path = os.path.join(self._config_dir, filename)
|
|
||||||
self._watched_files.discard(file_path)
|
|
||||||
self._logger.debug(f"Удален файл из отслеживания: {filename}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка удаления файла из отслеживания: {e}")
|
|
||||||
raise FileSystemError(f"Ошибка удаления файла из отслеживания: {e}")
|
|
||||||
|
|
||||||
def _handle_event(self, file_path: str, event_type: str) -> None:
|
|
||||||
"""
|
|
||||||
Обработка события файловой системы.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
file_path: Путь к файлу
|
|
||||||
event_type: Тип события
|
|
||||||
"""
|
|
||||||
# Проверяем, отслеживается ли файл
|
|
||||||
if file_path not in self._watched_files:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Получаем информацию о файле
|
|
||||||
filename = os.path.basename(file_path)
|
|
||||||
file_info = self.get_file_info(filename)
|
|
||||||
|
|
||||||
if not file_info:
|
|
||||||
return
|
|
||||||
|
|
||||||
if event_type == "created":
|
|
||||||
event_bus.publish(Event(EventTypes.CONFIG_CREATED, file_info))
|
|
||||||
self._logger.info(f"Создан файл конфигурации: {filename}")
|
|
||||||
|
|
||||||
elif event_type == "modified":
|
|
||||||
event_bus.publish(Event(EventTypes.CONFIG_MODIFIED, file_info))
|
|
||||||
self._logger.info(f"Изменен файл конфигурации: {filename}")
|
|
||||||
|
|
||||||
elif event_type == "deleted":
|
|
||||||
event_bus.publish(Event(EventTypes.CONFIG_DELETED, file_info))
|
|
||||||
self._logger.info(f"Удален файл конфигурации: {filename}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка обработки события: {e}")
|
|
||||||
|
|
||||||
def watch_all_configs(self) -> None:
|
|
||||||
"""
|
|
||||||
Добавление всех существующих конфигураций для отслеживания.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
FileSystemError: При ошибке добавления файлов
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
for filename in os.listdir(self._config_dir):
|
|
||||||
file_path = os.path.join(self._config_dir, filename)
|
|
||||||
if os.path.isfile(file_path):
|
|
||||||
self._watched_files.add(file_path)
|
|
||||||
|
|
||||||
self._logger.info(f"Добавлено {len(self._watched_files)} файлов для отслеживания")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка добавления файлов для отслеживания: {e}")
|
|
||||||
raise FileSystemError(f"Ошибка добавления файлов для отслеживания: {e}")
|
|
||||||
|
|
||||||
def clear_watches(self) -> None:
|
|
||||||
"""Удаление всех файлов из отслеживания."""
|
|
||||||
self._watched_files.clear()
|
|
||||||
self._logger.info("Очищен список отслеживаемых файлов")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_running(self) -> bool:
|
|
||||||
"""Проверка состояния наблюдателя."""
|
|
||||||
return self._observer is not None and self._observer.is_alive()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def watched_files(self) -> Set[str]:
|
|
||||||
"""Получение списка отслеживаемых файлов."""
|
|
||||||
return {os.path.basename(path) for path in self._watched_files}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
import threading
|
|
||||||
from typing import Optional, Dict, Any
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from core.events import event_bus, Event, EventTypes
|
|
||||||
|
|
||||||
class BaseServer(ABC):
|
|
||||||
"""Базовый класс для всех серверов."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._running = False
|
|
||||||
self._thread: Optional[threading.Thread] = None
|
|
||||||
self._logger = logging.getLogger(__name__)
|
|
||||||
self._config: Dict[str, Any] = {}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_running(self) -> bool:
|
|
||||||
"""Проверка состояния сервера."""
|
|
||||||
return self._running
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def start(self) -> None:
|
|
||||||
"""
|
|
||||||
Запуск сервера.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
Exception: При ошибке запуска
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def stop(self) -> None:
|
|
||||||
"""Остановка сервера."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def configure(self, **kwargs) -> None:
|
|
||||||
"""
|
|
||||||
Конфигурация сервера.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
**kwargs: Параметры конфигурации
|
|
||||||
"""
|
|
||||||
self._config.update(kwargs)
|
|
||||||
|
|
||||||
def _start_in_thread(self, target) -> None:
|
|
||||||
"""
|
|
||||||
Запуск сервера в отдельном потоке.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
target: Функция для выполнения в потоке
|
|
||||||
"""
|
|
||||||
if self._running:
|
|
||||||
self._logger.warning("Сервер уже запущен")
|
|
||||||
return
|
|
||||||
|
|
||||||
self._thread = threading.Thread(target=target)
|
|
||||||
self._thread.daemon = True
|
|
||||||
self._thread.start()
|
|
||||||
self._running = True
|
|
||||||
|
|
||||||
def _stop_thread(self) -> None:
|
|
||||||
"""Остановка потока сервера."""
|
|
||||||
self._running = False
|
|
||||||
if self._thread and self._thread.is_alive():
|
|
||||||
self._thread.join()
|
|
||||||
self._thread = None
|
|
||||||
|
|
||||||
def _notify_started(self, data: Optional[Dict[str, Any]] = None) -> None:
|
|
||||||
"""
|
|
||||||
Уведомление о запуске сервера.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data: Дополнительные данные
|
|
||||||
"""
|
|
||||||
event_bus.publish(Event(EventTypes.TFTP_SERVER_STARTED, data))
|
|
||||||
|
|
||||||
def _notify_stopped(self) -> None:
|
|
||||||
"""Уведомление об остановке сервера."""
|
|
||||||
event_bus.publish(Event(EventTypes.TFTP_SERVER_STOPPED, None))
|
|
||||||
|
|
||||||
def _notify_error(self, error: Exception) -> None:
|
|
||||||
"""
|
|
||||||
Уведомление об ошибке сервера.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
error: Объект ошибки
|
|
||||||
"""
|
|
||||||
event_bus.publish(Event(EventTypes.TFTP_ERROR, str(error)))
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import os
|
|
||||||
import socket
|
|
||||||
import threading
|
|
||||||
from typing import Optional, Dict, Any, Callable
|
|
||||||
import tftpy
|
|
||||||
|
|
||||||
from core.config import AppConfig
|
|
||||||
from core.exceptions import TFTPError
|
|
||||||
from .base_server import BaseServer
|
|
||||||
|
|
||||||
class TFTPServer(BaseServer):
|
|
||||||
"""TFTP сервер для передачи файлов."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self._server: Optional[tftpy.TftpServer] = None
|
|
||||||
self._root_dir = AppConfig.CONFIGS_DIR
|
|
||||||
self._host = "0.0.0.0"
|
|
||||||
self._port = AppConfig.TFTP_PORT
|
|
||||||
self._timeout = AppConfig.TFTP_TIMEOUT
|
|
||||||
|
|
||||||
# Создаем директорию, если её нет
|
|
||||||
os.makedirs(self._root_dir, exist_ok=True)
|
|
||||||
|
|
||||||
def configure(self, root_dir: Optional[str] = None, host: Optional[str] = None,
|
|
||||||
port: Optional[int] = None, timeout: Optional[int] = None) -> None:
|
|
||||||
"""
|
|
||||||
Конфигурация TFTP сервера.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
root_dir: Корневая директория для файлов
|
|
||||||
host: IP-адрес для прослушивания
|
|
||||||
port: Порт сервера
|
|
||||||
timeout: Таймаут операций
|
|
||||||
"""
|
|
||||||
if root_dir is not None:
|
|
||||||
self._root_dir = root_dir
|
|
||||||
os.makedirs(self._root_dir, exist_ok=True)
|
|
||||||
if host is not None:
|
|
||||||
self._host = host
|
|
||||||
if port is not None:
|
|
||||||
self._port = port
|
|
||||||
if timeout is not None:
|
|
||||||
self._timeout = timeout
|
|
||||||
|
|
||||||
def start(self) -> None:
|
|
||||||
"""
|
|
||||||
Запуск TFTP сервера.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TFTPError: При ошибке запуска сервера
|
|
||||||
"""
|
|
||||||
if self.is_running:
|
|
||||||
self._logger.warning("TFTP сервер уже запущен")
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Создаем серверный объект
|
|
||||||
self._server = tftpy.TftpServer(self._root_dir)
|
|
||||||
|
|
||||||
# Запускаем сервер в отдельном потоке
|
|
||||||
self._start_in_thread(self._serve)
|
|
||||||
|
|
||||||
self._notify_started({
|
|
||||||
"host": self._host,
|
|
||||||
"port": self._port,
|
|
||||||
"root_dir": self._root_dir
|
|
||||||
})
|
|
||||||
|
|
||||||
self._logger.info(f"TFTP сервер запущен на {self._host}:{self._port}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._notify_error(e)
|
|
||||||
raise TFTPError(f"Ошибка запуска TFTP сервера: {e}")
|
|
||||||
|
|
||||||
def stop(self) -> None:
|
|
||||||
"""Остановка TFTP сервера."""
|
|
||||||
if not self.is_running:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
if self._server:
|
|
||||||
self._server.stop()
|
|
||||||
self._server = None
|
|
||||||
|
|
||||||
self._stop_thread()
|
|
||||||
self._notify_stopped()
|
|
||||||
|
|
||||||
self._logger.info("TFTP сервер остановлен")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка при остановке TFTP сервера: {e}")
|
|
||||||
|
|
||||||
def _serve(self) -> None:
|
|
||||||
"""Основной цикл сервера."""
|
|
||||||
try:
|
|
||||||
self._server.listen(self._host, self._port, timeout=self._timeout)
|
|
||||||
except Exception as e:
|
|
||||||
self._notify_error(e)
|
|
||||||
self._logger.error(f"Ошибка в работе TFTP сервера: {e}")
|
|
||||||
self.stop()
|
|
||||||
|
|
||||||
def get_server_info(self) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Получение информации о сервере.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict[str, Any]: Информация о сервере
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
"running": self.is_running,
|
|
||||||
"host": self._host,
|
|
||||||
"port": self._port,
|
|
||||||
"root_dir": self._root_dir,
|
|
||||||
"timeout": self._timeout
|
|
||||||
}
|
|
||||||
|
|
||||||
def list_files(self) -> list[str]:
|
|
||||||
"""
|
|
||||||
Получение списка файлов в корневой директории.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list[str]: Список файлов
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return os.listdir(self._root_dir)
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка при получении списка файлов: {e}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
def get_file_info(self, filename: str) -> Optional[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
Получение информации о файле.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
filename: Имя файла
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Optional[Dict[str, Any]]: Информация о файле или None
|
|
||||||
"""
|
|
||||||
file_path = os.path.join(self._root_dir, filename)
|
|
||||||
if not os.path.exists(file_path):
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
stat = os.stat(file_path)
|
|
||||||
return {
|
|
||||||
"name": filename,
|
|
||||||
"size": stat.st_size,
|
|
||||||
"modified": stat.st_mtime,
|
|
||||||
"created": stat.st_ctime
|
|
||||||
}
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка при получении информации о файле: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def delete_file(self, filename: str) -> bool:
|
|
||||||
"""
|
|
||||||
Удаление файла из корневой директории.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
filename: Имя файла
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True если файл удален успешно
|
|
||||||
"""
|
|
||||||
file_path = os.path.join(self._root_dir, filename)
|
|
||||||
if not os.path.exists(file_path):
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
os.remove(file_path)
|
|
||||||
self._logger.info(f"Файл удален: {filename}")
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка при удалении файла: {e}")
|
|
||||||
return False
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import time
|
|
||||||
from typing import Optional, Dict, Any, Callable
|
|
||||||
from dataclasses import dataclass
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from core.events import event_bus, Event, EventTypes
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class TransferProgress:
|
|
||||||
"""Информация о прогрессе передачи."""
|
|
||||||
total_bytes: int
|
|
||||||
transferred_bytes: int = 0
|
|
||||||
start_time: float = 0.0
|
|
||||||
end_time: float = 0.0
|
|
||||||
status: str = "pending"
|
|
||||||
error: Optional[str] = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def progress(self) -> float:
|
|
||||||
"""Процент выполнения."""
|
|
||||||
if self.total_bytes == 0:
|
|
||||||
return 0.0
|
|
||||||
return (self.transferred_bytes / self.total_bytes) * 100
|
|
||||||
|
|
||||||
@property
|
|
||||||
def speed(self) -> float:
|
|
||||||
"""Скорость передачи в байтах в секунду."""
|
|
||||||
if self.start_time == 0 or self.transferred_bytes == 0:
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
duration = (self.end_time or time.time()) - self.start_time
|
|
||||||
if duration == 0:
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
return self.transferred_bytes / duration
|
|
||||||
|
|
||||||
@property
|
|
||||||
def elapsed_time(self) -> float:
|
|
||||||
"""Прошедшее время в секундах."""
|
|
||||||
if self.start_time == 0:
|
|
||||||
return 0.0
|
|
||||||
return (self.end_time or time.time()) - self.start_time
|
|
||||||
|
|
||||||
@property
|
|
||||||
def estimated_time(self) -> float:
|
|
||||||
"""Оценка оставшегося времени в секундах."""
|
|
||||||
if self.speed == 0 or self.transferred_bytes == 0:
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
remaining_bytes = self.total_bytes - self.transferred_bytes
|
|
||||||
return remaining_bytes / self.speed
|
|
||||||
|
|
||||||
class ProgressTracker:
|
|
||||||
"""Трекер прогресса передачи файлов."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._logger = logging.getLogger(__name__)
|
|
||||||
self._transfers: Dict[str, TransferProgress] = {}
|
|
||||||
self._callbacks: Dict[str, Callable[[TransferProgress], None]] = {}
|
|
||||||
|
|
||||||
def start_transfer(self, transfer_id: str, total_bytes: int,
|
|
||||||
callback: Optional[Callable[[TransferProgress], None]] = None) -> None:
|
|
||||||
"""
|
|
||||||
Начало отслеживания передачи.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
transfer_id: Идентификатор передачи
|
|
||||||
total_bytes: Общий размер в байтах
|
|
||||||
callback: Функция обратного вызова
|
|
||||||
"""
|
|
||||||
progress = TransferProgress(
|
|
||||||
total_bytes=total_bytes,
|
|
||||||
start_time=time.time()
|
|
||||||
)
|
|
||||||
|
|
||||||
self._transfers[transfer_id] = progress
|
|
||||||
if callback:
|
|
||||||
self._callbacks[transfer_id] = callback
|
|
||||||
|
|
||||||
event_bus.publish(Event(EventTypes.TRANSFER_STARTED, {
|
|
||||||
"transfer_id": transfer_id,
|
|
||||||
"total_bytes": total_bytes
|
|
||||||
}))
|
|
||||||
|
|
||||||
self._logger.info(f"Начата передача {transfer_id}: {total_bytes} байт")
|
|
||||||
|
|
||||||
def update_progress(self, transfer_id: str, transferred_bytes: int) -> None:
|
|
||||||
"""
|
|
||||||
Обновление прогресса передачи.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
transfer_id: Идентификатор передачи
|
|
||||||
transferred_bytes: Количество переданных байт
|
|
||||||
"""
|
|
||||||
if transfer_id not in self._transfers:
|
|
||||||
self._logger.warning(f"Передача {transfer_id} не найдена")
|
|
||||||
return
|
|
||||||
|
|
||||||
progress = self._transfers[transfer_id]
|
|
||||||
progress.transferred_bytes = transferred_bytes
|
|
||||||
|
|
||||||
# Вызываем callback, если есть
|
|
||||||
if transfer_id in self._callbacks:
|
|
||||||
self._callbacks[transfer_id](progress)
|
|
||||||
|
|
||||||
event_bus.publish(Event(EventTypes.TRANSFER_PROGRESS, {
|
|
||||||
"transfer_id": transfer_id,
|
|
||||||
"progress": progress.progress,
|
|
||||||
"speed": progress.speed,
|
|
||||||
"elapsed_time": progress.elapsed_time,
|
|
||||||
"estimated_time": progress.estimated_time
|
|
||||||
}))
|
|
||||||
|
|
||||||
def complete_transfer(self, transfer_id: str) -> None:
|
|
||||||
"""
|
|
||||||
Завершение передачи.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
transfer_id: Идентификатор передачи
|
|
||||||
"""
|
|
||||||
if transfer_id not in self._transfers:
|
|
||||||
return
|
|
||||||
|
|
||||||
progress = self._transfers[transfer_id]
|
|
||||||
progress.end_time = time.time()
|
|
||||||
progress.status = "completed"
|
|
||||||
|
|
||||||
event_bus.publish(Event(EventTypes.TRANSFER_COMPLETED, {
|
|
||||||
"transfer_id": transfer_id,
|
|
||||||
"total_time": progress.elapsed_time,
|
|
||||||
"average_speed": progress.speed
|
|
||||||
}))
|
|
||||||
|
|
||||||
self._logger.info(
|
|
||||||
f"Завершена передача {transfer_id}: "
|
|
||||||
f"{progress.transferred_bytes}/{progress.total_bytes} байт "
|
|
||||||
f"за {progress.elapsed_time:.1f} сек "
|
|
||||||
f"({progress.speed:.1f} байт/сек)"
|
|
||||||
)
|
|
||||||
|
|
||||||
self._cleanup_transfer(transfer_id)
|
|
||||||
|
|
||||||
def fail_transfer(self, transfer_id: str, error: str) -> None:
|
|
||||||
"""
|
|
||||||
Отметка передачи как неудачной.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
transfer_id: Идентификатор передачи
|
|
||||||
error: Сообщение об ошибке
|
|
||||||
"""
|
|
||||||
if transfer_id not in self._transfers:
|
|
||||||
return
|
|
||||||
|
|
||||||
progress = self._transfers[transfer_id]
|
|
||||||
progress.end_time = time.time()
|
|
||||||
progress.status = "failed"
|
|
||||||
progress.error = error
|
|
||||||
|
|
||||||
event_bus.publish(Event(EventTypes.TRANSFER_ERROR, {
|
|
||||||
"transfer_id": transfer_id,
|
|
||||||
"error": error
|
|
||||||
}))
|
|
||||||
|
|
||||||
self._logger.error(f"Ошибка передачи {transfer_id}: {error}")
|
|
||||||
|
|
||||||
self._cleanup_transfer(transfer_id)
|
|
||||||
|
|
||||||
def get_progress(self, transfer_id: str) -> Optional[TransferProgress]:
|
|
||||||
"""
|
|
||||||
Получение информации о прогрессе передачи.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
transfer_id: Идентификатор передачи
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Optional[TransferProgress]: Информация о прогрессе или None
|
|
||||||
"""
|
|
||||||
return self._transfers.get(transfer_id)
|
|
||||||
|
|
||||||
def list_transfers(self) -> Dict[str, TransferProgress]:
|
|
||||||
"""
|
|
||||||
Получение списка всех передач.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict[str, TransferProgress]: Словарь с информацией о передачах
|
|
||||||
"""
|
|
||||||
return self._transfers.copy()
|
|
||||||
|
|
||||||
def _cleanup_transfer(self, transfer_id: str) -> None:
|
|
||||||
"""
|
|
||||||
Очистка данных завершенной передачи.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
transfer_id: Идентификатор передачи
|
|
||||||
"""
|
|
||||||
self._transfers.pop(transfer_id, None)
|
|
||||||
self._callbacks.pop(transfer_id, None)
|
|
||||||
@@ -1,335 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import os
|
|
||||||
import uuid
|
|
||||||
from typing import Optional, Dict, Any, List, Callable
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from core.config import AppConfig
|
|
||||||
from core.exceptions import TransferError, ValidationError
|
|
||||||
from core.events import event_bus, Event, EventTypes
|
|
||||||
from communication.serial_manager import SerialManager
|
|
||||||
from network.servers.tftp_server import TFTPServer
|
|
||||||
from .progress_tracker import ProgressTracker
|
|
||||||
|
|
||||||
class TransferManager:
|
|
||||||
"""Менеджер передачи файлов."""
|
|
||||||
|
|
||||||
def __init__(self, serial_manager: SerialManager, tftp_server: TFTPServer):
|
|
||||||
self._serial = serial_manager
|
|
||||||
self._tftp = tftp_server
|
|
||||||
self._progress = ProgressTracker()
|
|
||||||
self._logger = logging.getLogger(__name__)
|
|
||||||
self._active_transfers: Dict[str, Dict[str, Any]] = {}
|
|
||||||
|
|
||||||
def transfer_config(self, config_path: str, mode: str = "line",
|
|
||||||
block_size: int = AppConfig.DEFAULT_BLOCK_SIZE,
|
|
||||||
callback: Optional[Callable[[float], None]] = None) -> str:
|
|
||||||
"""
|
|
||||||
Передача конфигурации на устройство.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
config_path: Путь к файлу конфигурации
|
|
||||||
mode: Режим передачи (line/block/tftp)
|
|
||||||
block_size: Размер блока команд
|
|
||||||
callback: Функция обратного вызова для отслеживания прогресса
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Идентификатор передачи
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValidationError: При ошибке валидации параметров
|
|
||||||
TransferError: При ошибке передачи
|
|
||||||
"""
|
|
||||||
# Проверяем файл
|
|
||||||
if not os.path.exists(config_path):
|
|
||||||
raise ValidationError(f"Файл не найден: {config_path}")
|
|
||||||
|
|
||||||
# Проверяем режим передачи
|
|
||||||
if mode not in AppConfig.SUPPORTED_COPY_MODES:
|
|
||||||
raise ValidationError(f"Неподдерживаемый режим передачи: {mode}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Создаем идентификатор передачи
|
|
||||||
transfer_id = str(uuid.uuid4())
|
|
||||||
|
|
||||||
# Читаем конфигурацию
|
|
||||||
with open(config_path, "r", encoding="utf-8") as f:
|
|
||||||
config_data = f.read()
|
|
||||||
|
|
||||||
# Разбиваем на команды
|
|
||||||
commands = [cmd.strip() for cmd in config_data.splitlines() if cmd.strip()]
|
|
||||||
total_commands = len(commands)
|
|
||||||
|
|
||||||
if not commands:
|
|
||||||
raise ValidationError("Пустой файл конфигурации")
|
|
||||||
|
|
||||||
# Сохраняем информацию о передаче
|
|
||||||
self._active_transfers[transfer_id] = {
|
|
||||||
"mode": mode,
|
|
||||||
"file": config_path,
|
|
||||||
"total_commands": total_commands,
|
|
||||||
"current_command": 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Запускаем передачу в зависимости от режима
|
|
||||||
if mode == "line":
|
|
||||||
self._transfer_line_by_line(transfer_id, commands, callback)
|
|
||||||
elif mode == "block":
|
|
||||||
self._transfer_by_blocks(transfer_id, commands, block_size, callback)
|
|
||||||
elif mode == "tftp":
|
|
||||||
self._transfer_by_tftp(transfer_id, config_path, callback)
|
|
||||||
|
|
||||||
return transfer_id
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка передачи конфигурации: {e}")
|
|
||||||
raise TransferError(f"Ошибка передачи конфигурации: {e}")
|
|
||||||
|
|
||||||
def _transfer_line_by_line(self, transfer_id: str, commands: List[str],
|
|
||||||
callback: Optional[Callable[[float], None]] = None) -> None:
|
|
||||||
"""
|
|
||||||
Построчная передача команд.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
transfer_id: Идентификатор передачи
|
|
||||||
commands: Список команд
|
|
||||||
callback: Функция обратного вызова
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TransferError: При ошибке передачи
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
total_commands = len(commands)
|
|
||||||
|
|
||||||
# Начинаем отслеживание прогресса
|
|
||||||
self._progress.start_transfer(transfer_id, total_commands, callback)
|
|
||||||
|
|
||||||
# Отправляем команды по одной
|
|
||||||
for i, command in enumerate(commands, 1):
|
|
||||||
response = self._serial.send_command(command)
|
|
||||||
|
|
||||||
# Проверяем ответ на ошибки
|
|
||||||
if "error" in response.lower() or "invalid" in response.lower():
|
|
||||||
raise TransferError(f"Ошибка выполнения команды: {response}")
|
|
||||||
|
|
||||||
# Обновляем прогресс
|
|
||||||
self._progress.update_progress(transfer_id, i)
|
|
||||||
|
|
||||||
# Обновляем информацию о передаче
|
|
||||||
self._active_transfers[transfer_id]["current_command"] = i
|
|
||||||
|
|
||||||
# Завершаем передачу
|
|
||||||
self._progress.complete_transfer(transfer_id)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._progress.fail_transfer(transfer_id, str(e))
|
|
||||||
raise TransferError(f"Ошибка построчной передачи: {e}")
|
|
||||||
|
|
||||||
def _transfer_by_blocks(self, transfer_id: str, commands: List[str],
|
|
||||||
block_size: int,
|
|
||||||
callback: Optional[Callable[[float], None]] = None) -> None:
|
|
||||||
"""
|
|
||||||
Блочная передача команд.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
transfer_id: Идентификатор передачи
|
|
||||||
commands: Список команд
|
|
||||||
block_size: Размер блока
|
|
||||||
callback: Функция обратного вызова
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TransferError: При ошибке передачи
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
total_commands = len(commands)
|
|
||||||
|
|
||||||
# Начинаем отслеживание прогресса
|
|
||||||
self._progress.start_transfer(transfer_id, total_commands, callback)
|
|
||||||
|
|
||||||
# Разбиваем команды на блоки
|
|
||||||
for i in range(0, total_commands, block_size):
|
|
||||||
block = commands[i:i + block_size]
|
|
||||||
|
|
||||||
# Отправляем блок команд
|
|
||||||
response = self._serial.send_config(block)
|
|
||||||
|
|
||||||
# Проверяем ответ на ошибки
|
|
||||||
if "error" in response.lower() or "invalid" in response.lower():
|
|
||||||
raise TransferError(f"Ошибка выполнения блока команд: {response}")
|
|
||||||
|
|
||||||
# Обновляем прогресс
|
|
||||||
self._progress.update_progress(transfer_id, i + len(block))
|
|
||||||
|
|
||||||
# Обновляем информацию о передаче
|
|
||||||
self._active_transfers[transfer_id]["current_command"] = i + len(block)
|
|
||||||
|
|
||||||
# Завершаем передачу
|
|
||||||
self._progress.complete_transfer(transfer_id)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._progress.fail_transfer(transfer_id, str(e))
|
|
||||||
raise TransferError(f"Ошибка блочной передачи: {e}")
|
|
||||||
|
|
||||||
def _transfer_by_tftp(self, transfer_id: str, config_path: str,
|
|
||||||
callback: Optional[Callable[[float], None]] = None) -> None:
|
|
||||||
"""
|
|
||||||
Передача через TFTP.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
transfer_id: Идентификатор передачи
|
|
||||||
config_path: Путь к файлу конфигурации
|
|
||||||
callback: Функция обратного вызова
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TransferError: При ошибке передачи
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Получаем размер файла
|
|
||||||
file_size = os.path.getsize(config_path)
|
|
||||||
|
|
||||||
# Начинаем отслеживание прогресса
|
|
||||||
self._progress.start_transfer(transfer_id, file_size, callback)
|
|
||||||
|
|
||||||
# Запускаем TFTP сервер, если не запущен
|
|
||||||
if not self._tftp.is_running:
|
|
||||||
self._tftp.start()
|
|
||||||
|
|
||||||
# Копируем файл в директорию TFTP сервера
|
|
||||||
filename = os.path.basename(config_path)
|
|
||||||
tftp_path = os.path.join(self._tftp.root_dir, filename)
|
|
||||||
|
|
||||||
with open(config_path, "rb") as src, open(tftp_path, "wb") as dst:
|
|
||||||
dst.write(src.read())
|
|
||||||
|
|
||||||
# Отправляем команду на загрузку файла через TFTP
|
|
||||||
command = f"copy tftp://{self._tftp.host}/{filename} startup-config"
|
|
||||||
response = self._serial.send_command(command)
|
|
||||||
|
|
||||||
# Проверяем ответ на ошибки
|
|
||||||
if "error" in response.lower() or "invalid" in response.lower():
|
|
||||||
raise TransferError(f"Ошибка загрузки через TFTP: {response}")
|
|
||||||
|
|
||||||
# Завершаем передачу
|
|
||||||
self._progress.complete_transfer(transfer_id)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._progress.fail_transfer(transfer_id, str(e))
|
|
||||||
raise TransferError(f"Ошибка передачи через TFTP: {e}")
|
|
||||||
|
|
||||||
finally:
|
|
||||||
# Удаляем временный файл
|
|
||||||
if os.path.exists(tftp_path):
|
|
||||||
os.remove(tftp_path)
|
|
||||||
|
|
||||||
def get_transfer_info(self, transfer_id: str) -> Optional[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
Получение информации о передаче.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
transfer_id: Идентификатор передачи
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Optional[Dict[str, Any]]: Информация о передаче или None
|
|
||||||
"""
|
|
||||||
if transfer_id not in self._active_transfers:
|
|
||||||
return None
|
|
||||||
|
|
||||||
transfer = self._active_transfers[transfer_id]
|
|
||||||
progress = self._progress.get_progress(transfer_id)
|
|
||||||
|
|
||||||
if not progress:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return {
|
|
||||||
**transfer,
|
|
||||||
"progress": progress.progress,
|
|
||||||
"speed": progress.speed,
|
|
||||||
"elapsed_time": progress.elapsed_time,
|
|
||||||
"estimated_time": progress.estimated_time,
|
|
||||||
"status": progress.status,
|
|
||||||
"error": progress.error
|
|
||||||
}
|
|
||||||
|
|
||||||
def list_transfers(self) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
Получение списка активных передач.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List[Dict[str, Any]]: Список с информацией о передачах
|
|
||||||
"""
|
|
||||||
transfers = []
|
|
||||||
|
|
||||||
for transfer_id, transfer in self._active_transfers.items():
|
|
||||||
info = self.get_transfer_info(transfer_id)
|
|
||||||
if info:
|
|
||||||
transfers.append(info)
|
|
||||||
|
|
||||||
return transfers
|
|
||||||
|
|
||||||
def cancel_transfer(self, transfer_id: str) -> None:
|
|
||||||
"""
|
|
||||||
Отмена передачи.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
transfer_id: Идентификатор передачи
|
|
||||||
"""
|
|
||||||
if transfer_id in self._active_transfers:
|
|
||||||
self._progress.fail_transfer(transfer_id, "Передача отменена пользователем")
|
|
||||||
self._active_transfers.pop(transfer_id)
|
|
||||||
|
|
||||||
def is_transfer_active(self, transfer_id: str) -> bool:
|
|
||||||
"""
|
|
||||||
Проверка активности передачи.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
transfer_id: Идентификатор передачи
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True если передача активна
|
|
||||||
"""
|
|
||||||
if transfer_id not in self._active_transfers:
|
|
||||||
return False
|
|
||||||
|
|
||||||
progress = self._progress.get_progress(transfer_id)
|
|
||||||
if not progress:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return progress.status not in ["completed", "failed"]
|
|
||||||
|
|
||||||
def cleanup_transfers(self) -> None:
|
|
||||||
"""Очистка завершенных передач."""
|
|
||||||
completed_transfers = [
|
|
||||||
transfer_id for transfer_id in self._active_transfers
|
|
||||||
if not self.is_transfer_active(transfer_id)
|
|
||||||
]
|
|
||||||
|
|
||||||
for transfer_id in completed_transfers:
|
|
||||||
self._active_transfers.pop(transfer_id)
|
|
||||||
|
|
||||||
self._logger.debug(f"Очищено {len(completed_transfers)} завершенных передач")
|
|
||||||
|
|
||||||
def get_active_transfer_count(self) -> int:
|
|
||||||
"""
|
|
||||||
Получение количества активных передач.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
int: Количество активных передач
|
|
||||||
"""
|
|
||||||
return len([
|
|
||||||
transfer_id for transfer_id in self._active_transfers
|
|
||||||
if self.is_transfer_active(transfer_id)
|
|
||||||
])
|
|
||||||
|
|
||||||
def cancel_all_transfers(self) -> None:
|
|
||||||
"""Отмена всех активных передач."""
|
|
||||||
active_transfers = [
|
|
||||||
transfer_id for transfer_id in self._active_transfers
|
|
||||||
if self.is_transfer_active(transfer_id)
|
|
||||||
]
|
|
||||||
|
|
||||||
for transfer_id in active_transfers:
|
|
||||||
self.cancel_transfer(transfer_id)
|
|
||||||
|
|
||||||
self._logger.info(f"Отменено {len(active_transfers)} активных передач")
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
pyserial>=3.5
|
|
||||||
tftpy>=0.8.0
|
|
||||||
requests>=2.31.0
|
|
||||||
watchdog>=3.0.0
|
|
||||||
@@ -1,250 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import tkinter as tk
|
|
||||||
from tkinter import ttk
|
|
||||||
import webbrowser
|
|
||||||
from typing import Optional
|
|
||||||
import platform
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from core.config import AppConfig
|
|
||||||
|
|
||||||
class AboutDialog(tk.Toplevel):
|
|
||||||
"""Диалог с информацией о программе."""
|
|
||||||
|
|
||||||
def __init__(self, parent):
|
|
||||||
super().__init__(parent)
|
|
||||||
|
|
||||||
self.title("О программе")
|
|
||||||
self.resizable(False, False)
|
|
||||||
self.transient(parent)
|
|
||||||
|
|
||||||
# Создаем элементы интерфейса
|
|
||||||
self._create_widgets()
|
|
||||||
|
|
||||||
# Делаем диалог модальным
|
|
||||||
self.grab_set()
|
|
||||||
self.focus_set()
|
|
||||||
|
|
||||||
# Центрируем окно
|
|
||||||
self._center_window()
|
|
||||||
|
|
||||||
def _create_widgets(self) -> None:
|
|
||||||
"""Создание элементов интерфейса."""
|
|
||||||
# Основной контейнер
|
|
||||||
main_frame = ttk.Frame(self, padding="20")
|
|
||||||
main_frame.pack(fill=tk.BOTH, expand=True)
|
|
||||||
|
|
||||||
# Логотип
|
|
||||||
try:
|
|
||||||
logo = tk.PhotoImage(file="Icon.ico")
|
|
||||||
logo_label = ttk.Label(main_frame, image=logo)
|
|
||||||
logo_label.image = logo # Сохраняем ссылку
|
|
||||||
logo_label.pack(pady=(0, 10))
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Название программы
|
|
||||||
ttk.Label(
|
|
||||||
main_frame,
|
|
||||||
text="ComConfigCopy",
|
|
||||||
font=("Arial", 14, "bold")
|
|
||||||
).pack()
|
|
||||||
|
|
||||||
# Версия
|
|
||||||
ttk.Label(
|
|
||||||
main_frame,
|
|
||||||
text=f"Версия {AppConfig.VERSION}",
|
|
||||||
font=("Arial", 10)
|
|
||||||
).pack()
|
|
||||||
|
|
||||||
# Описание
|
|
||||||
description = (
|
|
||||||
"Программа для копирования конфигураций\n"
|
|
||||||
"на сетевое оборудование через последовательный порт и TFTP"
|
|
||||||
)
|
|
||||||
ttk.Label(
|
|
||||||
main_frame,
|
|
||||||
text=description,
|
|
||||||
justify=tk.CENTER,
|
|
||||||
wraplength=300
|
|
||||||
).pack(pady=10)
|
|
||||||
|
|
||||||
# Системная информация
|
|
||||||
system_frame = ttk.LabelFrame(main_frame, text="Системная информация", padding="5")
|
|
||||||
system_frame.pack(fill=tk.X, pady=10)
|
|
||||||
|
|
||||||
# Python
|
|
||||||
ttk.Label(
|
|
||||||
system_frame,
|
|
||||||
text=f"Python: {sys.version.split()[0]}"
|
|
||||||
).pack(anchor=tk.W)
|
|
||||||
|
|
||||||
# ОС
|
|
||||||
ttk.Label(
|
|
||||||
system_frame,
|
|
||||||
text=f"ОС: {platform.system()} {platform.release()}"
|
|
||||||
).pack(anchor=tk.W)
|
|
||||||
|
|
||||||
# Процессор
|
|
||||||
ttk.Label(
|
|
||||||
system_frame,
|
|
||||||
text=f"Процессор: {platform.processor()}"
|
|
||||||
).pack(anchor=tk.W)
|
|
||||||
|
|
||||||
# Зависимости
|
|
||||||
deps_frame = ttk.LabelFrame(main_frame, text="Зависимости", padding="5")
|
|
||||||
deps_frame.pack(fill=tk.X, pady=10)
|
|
||||||
|
|
||||||
# Создаем список зависимостей
|
|
||||||
self._add_dependency(deps_frame, "pyserial", ">=3.5")
|
|
||||||
self._add_dependency(deps_frame, "tftpy", ">=0.8.0")
|
|
||||||
self._add_dependency(deps_frame, "requests", ">=2.31.0")
|
|
||||||
self._add_dependency(deps_frame, "watchdog", ">=3.0.0")
|
|
||||||
|
|
||||||
# Ссылки
|
|
||||||
links_frame = ttk.Frame(main_frame)
|
|
||||||
links_frame.pack(fill=tk.X, pady=10)
|
|
||||||
|
|
||||||
# GitHub
|
|
||||||
github_link = ttk.Label(
|
|
||||||
links_frame,
|
|
||||||
text="GitHub",
|
|
||||||
cursor="hand2",
|
|
||||||
foreground="blue"
|
|
||||||
)
|
|
||||||
github_link.pack(side=tk.LEFT, padx=5)
|
|
||||||
github_link.bind(
|
|
||||||
"<Button-1>",
|
|
||||||
lambda e: webbrowser.open("https://github.com/yourusername/ComConfigCopy")
|
|
||||||
)
|
|
||||||
|
|
||||||
# Документация
|
|
||||||
docs_link = ttk.Label(
|
|
||||||
links_frame,
|
|
||||||
text="Документация",
|
|
||||||
cursor="hand2",
|
|
||||||
foreground="blue"
|
|
||||||
)
|
|
||||||
docs_link.pack(side=tk.LEFT, padx=5)
|
|
||||||
docs_link.bind(
|
|
||||||
"<Button-1>",
|
|
||||||
lambda e: webbrowser.open("https://yourusername.github.io/ComConfigCopy")
|
|
||||||
)
|
|
||||||
|
|
||||||
# Лицензия
|
|
||||||
license_link = ttk.Label(
|
|
||||||
links_frame,
|
|
||||||
text="Лицензия",
|
|
||||||
cursor="hand2",
|
|
||||||
foreground="blue"
|
|
||||||
)
|
|
||||||
license_link.pack(side=tk.LEFT, padx=5)
|
|
||||||
license_link.bind(
|
|
||||||
"<Button-1>",
|
|
||||||
lambda e: self._show_license()
|
|
||||||
)
|
|
||||||
|
|
||||||
# Кнопка закрытия
|
|
||||||
ttk.Button(
|
|
||||||
main_frame,
|
|
||||||
text="Закрыть",
|
|
||||||
command=self.destroy
|
|
||||||
).pack(pady=(10, 0))
|
|
||||||
|
|
||||||
def _add_dependency(self, parent: ttk.Frame, name: str, version: str) -> None:
|
|
||||||
"""
|
|
||||||
Добавление зависимости в список.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
parent: Родительский виджет
|
|
||||||
name: Название пакета
|
|
||||||
version: Версия пакета
|
|
||||||
"""
|
|
||||||
frame = ttk.Frame(parent)
|
|
||||||
frame.pack(fill=tk.X)
|
|
||||||
|
|
||||||
ttk.Label(frame, text=name).pack(side=tk.LEFT)
|
|
||||||
ttk.Label(frame, text=version).pack(side=tk.RIGHT)
|
|
||||||
|
|
||||||
def _show_license(self) -> None:
|
|
||||||
"""Отображение текста лицензии."""
|
|
||||||
license_text = """
|
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2024 Your Name
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
"""
|
|
||||||
# Создаем диалог с текстом лицензии
|
|
||||||
dialog = tk.Toplevel(self)
|
|
||||||
dialog.title("Лицензия")
|
|
||||||
dialog.transient(self)
|
|
||||||
dialog.grab_set()
|
|
||||||
|
|
||||||
# Добавляем текстовое поле
|
|
||||||
text = tk.Text(
|
|
||||||
dialog,
|
|
||||||
wrap=tk.WORD,
|
|
||||||
width=60,
|
|
||||||
height=20,
|
|
||||||
padx=10,
|
|
||||||
pady=10
|
|
||||||
)
|
|
||||||
text.pack(padx=10, pady=10)
|
|
||||||
|
|
||||||
# Вставляем текст лицензии
|
|
||||||
text.insert("1.0", license_text.strip())
|
|
||||||
text.configure(state=tk.DISABLED)
|
|
||||||
|
|
||||||
# Добавляем кнопку закрытия
|
|
||||||
ttk.Button(
|
|
||||||
dialog,
|
|
||||||
text="Закрыть",
|
|
||||||
command=dialog.destroy
|
|
||||||
).pack(pady=(0, 10))
|
|
||||||
|
|
||||||
# Центрируем диалог
|
|
||||||
self._center_window(dialog)
|
|
||||||
|
|
||||||
def _center_window(self, window: Optional[tk.Toplevel] = None) -> None:
|
|
||||||
"""
|
|
||||||
Центрирование окна на экране.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
window: Окно для центрирования
|
|
||||||
"""
|
|
||||||
window = window or self
|
|
||||||
window.update_idletasks()
|
|
||||||
|
|
||||||
# Получаем размеры экрана
|
|
||||||
screen_width = window.winfo_screenwidth()
|
|
||||||
screen_height = window.winfo_screenheight()
|
|
||||||
|
|
||||||
# Получаем размеры окна
|
|
||||||
window_width = window.winfo_width()
|
|
||||||
window_height = window.winfo_height()
|
|
||||||
|
|
||||||
# Вычисляем координаты
|
|
||||||
x = (screen_width - window_width) // 2
|
|
||||||
y = (screen_height - window_height) // 2
|
|
||||||
|
|
||||||
# Устанавливаем позицию
|
|
||||||
window.geometry(f"+{x}+{y}")
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import tkinter as tk
|
|
||||||
from tkinter import ttk
|
|
||||||
from typing import Dict, Any, Optional
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from core.config import AppConfig
|
|
||||||
from core.events import event_bus, Event, EventTypes
|
|
||||||
from ..widgets.custom_entry import CustomEntry
|
|
||||||
|
|
||||||
class SettingsDialog(tk.Toplevel):
|
|
||||||
"""Диалог настроек приложения."""
|
|
||||||
|
|
||||||
def __init__(self, parent, settings: Dict[str, Any]):
|
|
||||||
super().__init__(parent)
|
|
||||||
|
|
||||||
self.title("Настройки")
|
|
||||||
self.resizable(False, False)
|
|
||||||
self.transient(parent)
|
|
||||||
|
|
||||||
# Сохраняем текущие настройки
|
|
||||||
self._settings = settings.copy()
|
|
||||||
self._logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# Создаем элементы интерфейса
|
|
||||||
self._create_widgets()
|
|
||||||
|
|
||||||
# Заполняем поля текущими значениями
|
|
||||||
self._load_settings()
|
|
||||||
|
|
||||||
# Делаем диалог модальным
|
|
||||||
self.grab_set()
|
|
||||||
self.focus_set()
|
|
||||||
|
|
||||||
def _create_widgets(self) -> None:
|
|
||||||
"""Создание элементов интерфейса."""
|
|
||||||
# Основной контейнер
|
|
||||||
main_frame = ttk.Frame(self, padding="10")
|
|
||||||
main_frame.pack(fill=tk.BOTH, expand=True)
|
|
||||||
|
|
||||||
# Настройки последовательного порта
|
|
||||||
port_frame = ttk.LabelFrame(main_frame, text="Последовательный порт", padding="5")
|
|
||||||
port_frame.pack(fill=tk.X, padx=5, pady=5)
|
|
||||||
|
|
||||||
# COM-порт
|
|
||||||
ttk.Label(port_frame, text="COM-порт:").grid(row=0, column=0, sticky=tk.W)
|
|
||||||
self.port_entry = CustomEntry(port_frame)
|
|
||||||
self.port_entry.grid(row=0, column=1, sticky=tk.EW, padx=5)
|
|
||||||
|
|
||||||
# Скорость
|
|
||||||
ttk.Label(port_frame, text="Скорость:").grid(row=1, column=0, sticky=tk.W)
|
|
||||||
self.baudrate_combo = ttk.Combobox(
|
|
||||||
port_frame,
|
|
||||||
values=AppConfig.SUPPORTED_BAUDRATES,
|
|
||||||
state="readonly"
|
|
||||||
)
|
|
||||||
self.baudrate_combo.grid(row=1, column=1, sticky=tk.EW, padx=5, pady=5)
|
|
||||||
|
|
||||||
# Таймаут
|
|
||||||
ttk.Label(port_frame, text="Таймаут (сек):").grid(row=2, column=0, sticky=tk.W)
|
|
||||||
self.timeout_entry = CustomEntry(
|
|
||||||
port_frame,
|
|
||||||
validator=lambda x: x.isdigit() and 1 <= int(x) <= 60
|
|
||||||
)
|
|
||||||
self.timeout_entry.grid(row=2, column=1, sticky=tk.EW, padx=5)
|
|
||||||
|
|
||||||
# Настройки передачи
|
|
||||||
transfer_frame = ttk.LabelFrame(main_frame, text="Передача данных", padding="5")
|
|
||||||
transfer_frame.pack(fill=tk.X, padx=5, pady=5)
|
|
||||||
|
|
||||||
# Режим копирования
|
|
||||||
ttk.Label(transfer_frame, text="Режим:").grid(row=0, column=0, sticky=tk.W)
|
|
||||||
self.copy_mode_combo = ttk.Combobox(
|
|
||||||
transfer_frame,
|
|
||||||
values=AppConfig.SUPPORTED_COPY_MODES,
|
|
||||||
state="readonly"
|
|
||||||
)
|
|
||||||
self.copy_mode_combo.grid(row=0, column=1, sticky=tk.EW, padx=5)
|
|
||||||
|
|
||||||
# Размер блока
|
|
||||||
ttk.Label(transfer_frame, text="Размер блока:").grid(row=1, column=0, sticky=tk.W)
|
|
||||||
self.block_size_entry = CustomEntry(
|
|
||||||
transfer_frame,
|
|
||||||
validator=lambda x: x.isdigit() and 1 <= int(x) <= 100
|
|
||||||
)
|
|
||||||
self.block_size_entry.grid(row=1, column=1, sticky=tk.EW, padx=5, pady=5)
|
|
||||||
|
|
||||||
# Приглашение командной строки
|
|
||||||
ttk.Label(transfer_frame, text="Приглашение:").grid(row=2, column=0, sticky=tk.W)
|
|
||||||
self.prompt_entry = CustomEntry(transfer_frame)
|
|
||||||
self.prompt_entry.grid(row=2, column=1, sticky=tk.EW, padx=5)
|
|
||||||
|
|
||||||
# Настройки TFTP
|
|
||||||
tftp_frame = ttk.LabelFrame(main_frame, text="TFTP сервер", padding="5")
|
|
||||||
tftp_frame.pack(fill=tk.X, padx=5, pady=5)
|
|
||||||
|
|
||||||
# Порт TFTP
|
|
||||||
ttk.Label(tftp_frame, text="Порт:").grid(row=0, column=0, sticky=tk.W)
|
|
||||||
self.tftp_port_entry = CustomEntry(
|
|
||||||
tftp_frame,
|
|
||||||
validator=lambda x: x.isdigit() and 1 <= int(x) <= 65535
|
|
||||||
)
|
|
||||||
self.tftp_port_entry.grid(row=0, column=1, sticky=tk.EW, padx=5)
|
|
||||||
|
|
||||||
# Кнопки
|
|
||||||
button_frame = ttk.Frame(main_frame)
|
|
||||||
button_frame.pack(fill=tk.X, padx=5, pady=10)
|
|
||||||
|
|
||||||
ttk.Button(
|
|
||||||
button_frame,
|
|
||||||
text="Сохранить",
|
|
||||||
command=self._save_settings
|
|
||||||
).pack(side=tk.RIGHT, padx=5)
|
|
||||||
|
|
||||||
ttk.Button(
|
|
||||||
button_frame,
|
|
||||||
text="Отмена",
|
|
||||||
command=self.destroy
|
|
||||||
).pack(side=tk.RIGHT)
|
|
||||||
|
|
||||||
# Настраиваем растяжение колонок
|
|
||||||
port_frame.columnconfigure(1, weight=1)
|
|
||||||
transfer_frame.columnconfigure(1, weight=1)
|
|
||||||
tftp_frame.columnconfigure(1, weight=1)
|
|
||||||
|
|
||||||
def _load_settings(self) -> None:
|
|
||||||
"""Загрузка текущих настроек в поля ввода."""
|
|
||||||
# COM-порт
|
|
||||||
self.port_entry.set(self._settings.get("port", ""))
|
|
||||||
|
|
||||||
# Скорость
|
|
||||||
baudrate = str(self._settings.get("baudrate", AppConfig.DEFAULT_BAUDRATE))
|
|
||||||
self.baudrate_combo.set(baudrate)
|
|
||||||
|
|
||||||
# Таймаут
|
|
||||||
timeout = str(self._settings.get("timeout", AppConfig.DEFAULT_TIMEOUT))
|
|
||||||
self.timeout_entry.set(timeout)
|
|
||||||
|
|
||||||
# Режим копирования
|
|
||||||
copy_mode = self._settings.get("copy_mode", AppConfig.DEFAULT_COPY_MODE)
|
|
||||||
self.copy_mode_combo.set(copy_mode)
|
|
||||||
|
|
||||||
# Размер блока
|
|
||||||
block_size = str(self._settings.get("block_size", AppConfig.DEFAULT_BLOCK_SIZE))
|
|
||||||
self.block_size_entry.set(block_size)
|
|
||||||
|
|
||||||
# Приглашение
|
|
||||||
prompt = self._settings.get("prompt", AppConfig.DEFAULT_PROMPT)
|
|
||||||
self.prompt_entry.set(prompt)
|
|
||||||
|
|
||||||
# Порт TFTP
|
|
||||||
tftp_port = str(self._settings.get("tftp_port", AppConfig.TFTP_PORT))
|
|
||||||
self.tftp_port_entry.set(tftp_port)
|
|
||||||
|
|
||||||
def _validate_settings(self) -> Optional[str]:
|
|
||||||
"""
|
|
||||||
Валидация введенных настроек.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Optional[str]: Сообщение об ошибке или None
|
|
||||||
"""
|
|
||||||
# Проверяем COM-порт
|
|
||||||
if not self.port_entry.get():
|
|
||||||
return "Не указан COM-порт"
|
|
||||||
|
|
||||||
# Проверяем таймаут
|
|
||||||
try:
|
|
||||||
timeout = int(self.timeout_entry.get())
|
|
||||||
if not 1 <= timeout <= 60:
|
|
||||||
return "Таймаут должен быть от 1 до 60 секунд"
|
|
||||||
except ValueError:
|
|
||||||
return "Некорректное значение таймаута"
|
|
||||||
|
|
||||||
# Проверяем размер блока
|
|
||||||
try:
|
|
||||||
block_size = int(self.block_size_entry.get())
|
|
||||||
if not 1 <= block_size <= 100:
|
|
||||||
return "Размер блока должен быть от 1 до 100"
|
|
||||||
except ValueError:
|
|
||||||
return "Некорректное значение размера блока"
|
|
||||||
|
|
||||||
# Проверяем порт TFTP
|
|
||||||
try:
|
|
||||||
tftp_port = int(self.tftp_port_entry.get())
|
|
||||||
if not 1 <= tftp_port <= 65535:
|
|
||||||
return "Порт TFTP должен быть от 1 до 65535"
|
|
||||||
except ValueError:
|
|
||||||
return "Некорректное значение порта TFTP"
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _save_settings(self) -> None:
|
|
||||||
"""Сохранение настроек."""
|
|
||||||
# Проверяем настройки
|
|
||||||
error = self._validate_settings()
|
|
||||||
if error:
|
|
||||||
tk.messagebox.showerror("Ошибка", error)
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Обновляем настройки
|
|
||||||
self._settings.update({
|
|
||||||
"port": self.port_entry.get(),
|
|
||||||
"baudrate": int(self.baudrate_combo.get()),
|
|
||||||
"timeout": int(self.timeout_entry.get()),
|
|
||||||
"copy_mode": self.copy_mode_combo.get(),
|
|
||||||
"block_size": int(self.block_size_entry.get()),
|
|
||||||
"prompt": self.prompt_entry.get(),
|
|
||||||
"tftp_port": int(self.tftp_port_entry.get())
|
|
||||||
})
|
|
||||||
|
|
||||||
# Уведомляем об изменении настроек
|
|
||||||
event_bus.publish(Event(EventTypes.UI_SETTINGS_CHANGED, self._settings))
|
|
||||||
|
|
||||||
self._logger.info("Настройки сохранены")
|
|
||||||
self.destroy()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка сохранения настроек: {e}")
|
|
||||||
tk.messagebox.showerror("Ошибка", "Не удалось сохранить настройки")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def settings(self) -> Dict[str, Any]:
|
|
||||||
"""Получение текущих настроек."""
|
|
||||||
return self._settings
|
|
||||||
@@ -1,741 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Добавляем директорию src в путь поиска модулей
|
|
||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
|
|
||||||
import tkinter as tk
|
|
||||||
from tkinter import ttk, messagebox
|
|
||||||
from typing import Optional, Dict, Any
|
|
||||||
import logging
|
|
||||||
import serial.tools.list_ports
|
|
||||||
import os
|
|
||||||
|
|
||||||
from core.config import AppConfig
|
|
||||||
from core.events import event_bus, Event, EventTypes
|
|
||||||
from core.state import ConnectionState, TransferState, StateManager
|
|
||||||
from communication.serial_manager import SerialManager
|
|
||||||
from network.servers.tftp_server import TFTPServer
|
|
||||||
from network.transfer.transfer_manager import TransferManager
|
|
||||||
from filesystem.config_manager import ConfigManager
|
|
||||||
from filesystem.watchers.config_watcher import ConfigWatcher
|
|
||||||
from .widgets.custom_text import CustomText
|
|
||||||
from .widgets.custom_entry import CustomEntry
|
|
||||||
from .widgets.transfer_view import TransferView
|
|
||||||
from .dialogs.settings_dialog import SettingsDialog
|
|
||||||
from .dialogs.about_dialog import AboutDialog
|
|
||||||
|
|
||||||
class MainWindow(tk.Tk):
|
|
||||||
"""Главное окно приложения."""
|
|
||||||
|
|
||||||
def __init__(self, settings, state, serial, transfer, config_manager):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
# Настройка окна
|
|
||||||
self.title(f"ComConfigCopy v{AppConfig.VERSION}")
|
|
||||||
self.geometry("800x600")
|
|
||||||
self.minsize(800, 600)
|
|
||||||
|
|
||||||
# Флаг несохраненных изменений
|
|
||||||
self._has_unsaved_changes = False
|
|
||||||
|
|
||||||
# Обработка закрытия окна
|
|
||||||
self.protocol("WM_DELETE_WINDOW", self._on_close)
|
|
||||||
|
|
||||||
# Инициализация компонентов
|
|
||||||
self._logger = logging.getLogger(__name__)
|
|
||||||
self._settings = settings
|
|
||||||
self._state_manager = state
|
|
||||||
self._serial_manager = serial
|
|
||||||
self._transfer_manager = transfer
|
|
||||||
self._config_manager = config_manager
|
|
||||||
self._tftp_server = TFTPServer()
|
|
||||||
self._config_watcher = ConfigWatcher()
|
|
||||||
|
|
||||||
# Создаем интерфейс
|
|
||||||
self._create_widgets()
|
|
||||||
self._setup_event_handlers()
|
|
||||||
|
|
||||||
# Загружаем конфигурации
|
|
||||||
self._load_configs()
|
|
||||||
|
|
||||||
# Запускаем наблюдатель за конфигурациями
|
|
||||||
self._config_watcher.start()
|
|
||||||
self._config_watcher.watch_all_configs()
|
|
||||||
|
|
||||||
def _create_widgets(self) -> None:
|
|
||||||
"""Создание элементов интерфейса."""
|
|
||||||
# Создаем главное меню
|
|
||||||
self._create_menu()
|
|
||||||
|
|
||||||
# Создаем панель инструментов
|
|
||||||
self._create_toolbar()
|
|
||||||
|
|
||||||
# Создаем основной контейнер
|
|
||||||
main_frame = ttk.Frame(self)
|
|
||||||
main_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
||||||
|
|
||||||
# Создаем разделитель для левой и правой панели
|
|
||||||
paned = ttk.PanedWindow(main_frame, orient=tk.HORIZONTAL)
|
|
||||||
paned.pack(fill=tk.BOTH, expand=True)
|
|
||||||
|
|
||||||
# Левая панель
|
|
||||||
left_frame = ttk.Frame(paned)
|
|
||||||
paned.add(left_frame, weight=1)
|
|
||||||
|
|
||||||
# Список конфигураций
|
|
||||||
configs_frame = ttk.LabelFrame(left_frame, text="Конфигурации")
|
|
||||||
configs_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
||||||
|
|
||||||
# Создаем список с прокруткой
|
|
||||||
self._configs_list = tk.Listbox(
|
|
||||||
configs_frame,
|
|
||||||
selectmode=tk.SINGLE,
|
|
||||||
activestyle=tk.NONE
|
|
||||||
)
|
|
||||||
self._configs_list.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
||||||
|
|
||||||
scrollbar = ttk.Scrollbar(configs_frame, orient=tk.VERTICAL)
|
|
||||||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
|
||||||
|
|
||||||
self._configs_list.configure(yscrollcommand=scrollbar.set)
|
|
||||||
scrollbar.configure(command=self._configs_list.yview)
|
|
||||||
|
|
||||||
# Кнопки управления конфигурациями
|
|
||||||
config_buttons = ttk.Frame(left_frame)
|
|
||||||
config_buttons.pack(fill=tk.X, padx=5, pady=5)
|
|
||||||
|
|
||||||
ttk.Button(
|
|
||||||
config_buttons,
|
|
||||||
text="Добавить",
|
|
||||||
command=self._add_config
|
|
||||||
).pack(side=tk.LEFT, padx=2)
|
|
||||||
|
|
||||||
ttk.Button(
|
|
||||||
config_buttons,
|
|
||||||
text="Удалить",
|
|
||||||
command=self._remove_config
|
|
||||||
).pack(side=tk.LEFT, padx=2)
|
|
||||||
|
|
||||||
ttk.Button(
|
|
||||||
config_buttons,
|
|
||||||
text="Обновить",
|
|
||||||
command=self._load_configs
|
|
||||||
).pack(side=tk.LEFT, padx=2)
|
|
||||||
|
|
||||||
# Правая панель
|
|
||||||
right_frame = ttk.Frame(paned)
|
|
||||||
paned.add(right_frame, weight=2)
|
|
||||||
|
|
||||||
# Информация об устройстве
|
|
||||||
device_frame = ttk.LabelFrame(right_frame, text="Информация об устройстве")
|
|
||||||
device_frame.pack(fill=tk.X, padx=5, pady=5)
|
|
||||||
|
|
||||||
# Создаем сетку для информации
|
|
||||||
self._device_info = {}
|
|
||||||
|
|
||||||
info_grid = ttk.Frame(device_frame)
|
|
||||||
info_grid.pack(fill=tk.X, padx=5, pady=5)
|
|
||||||
|
|
||||||
# Hostname
|
|
||||||
ttk.Label(info_grid, text="Hostname:").grid(row=0, column=0, sticky=tk.W)
|
|
||||||
self._device_info["hostname"] = ttk.Label(info_grid, text="-")
|
|
||||||
self._device_info["hostname"].grid(row=0, column=1, sticky=tk.W, padx=5)
|
|
||||||
|
|
||||||
# Модель
|
|
||||||
ttk.Label(info_grid, text="Модель:").grid(row=1, column=0, sticky=tk.W)
|
|
||||||
self._device_info["model"] = ttk.Label(info_grid, text="-")
|
|
||||||
self._device_info["model"].grid(row=1, column=1, sticky=tk.W, padx=5)
|
|
||||||
|
|
||||||
# Версия
|
|
||||||
ttk.Label(info_grid, text="Версия:").grid(row=2, column=0, sticky=tk.W)
|
|
||||||
self._device_info["version"] = ttk.Label(info_grid, text="-")
|
|
||||||
self._device_info["version"].grid(row=2, column=1, sticky=tk.W, padx=5)
|
|
||||||
|
|
||||||
# Редактор конфигурации
|
|
||||||
editor_frame = ttk.LabelFrame(right_frame, text="Конфигурация")
|
|
||||||
editor_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
||||||
|
|
||||||
self._editor = CustomText(editor_frame)
|
|
||||||
self._editor.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
||||||
|
|
||||||
# Виджет прогресса передачи
|
|
||||||
self._transfer_view = TransferView(right_frame)
|
|
||||||
self._transfer_view.pack(fill=tk.X, padx=5, pady=5)
|
|
||||||
|
|
||||||
# Статусная строка
|
|
||||||
self._status_bar = ttk.Label(
|
|
||||||
self,
|
|
||||||
text="Готов к работе",
|
|
||||||
anchor=tk.W,
|
|
||||||
padding=5
|
|
||||||
)
|
|
||||||
self._status_bar.pack(fill=tk.X)
|
|
||||||
|
|
||||||
def _create_menu(self) -> None:
|
|
||||||
"""Создание главного меню."""
|
|
||||||
menubar = tk.Menu(self)
|
|
||||||
|
|
||||||
# Меню "Файл"
|
|
||||||
file_menu = tk.Menu(menubar, tearoff=0)
|
|
||||||
file_menu.add_command(label="Сохранить", command=self._save_config_changes, accelerator="Ctrl+S")
|
|
||||||
file_menu.add_separator()
|
|
||||||
file_menu.add_command(label="Настройки", command=self._show_settings)
|
|
||||||
file_menu.add_separator()
|
|
||||||
file_menu.add_command(label="Выход", command=self.quit)
|
|
||||||
menubar.add_cascade(label="Файл", menu=file_menu)
|
|
||||||
|
|
||||||
# Меню "Устройство"
|
|
||||||
device_menu = tk.Menu(menubar, tearoff=0)
|
|
||||||
device_menu.add_command(label="Подключить", command=self._connect_device)
|
|
||||||
device_menu.add_command(label="Отключить", command=self._disconnect_device)
|
|
||||||
device_menu.add_separator()
|
|
||||||
device_menu.add_command(label="Обновить информацию", command=self._update_device_info)
|
|
||||||
menubar.add_cascade(label="Устройство", menu=device_menu)
|
|
||||||
|
|
||||||
# Меню "Передача"
|
|
||||||
transfer_menu = tk.Menu(menubar, tearoff=0)
|
|
||||||
transfer_menu.add_command(label="Передать конфигурацию", command=self._transfer_config)
|
|
||||||
transfer_menu.add_command(label="Отменить передачу", command=self._cancel_transfer)
|
|
||||||
transfer_menu.add_separator()
|
|
||||||
transfer_menu.add_command(label="Запустить TFTP сервер", command=self._start_tftp_server)
|
|
||||||
transfer_menu.add_command(label="Остановить TFTP сервер", command=self._stop_tftp_server)
|
|
||||||
menubar.add_cascade(label="Передача", menu=transfer_menu)
|
|
||||||
|
|
||||||
# Меню "Справка"
|
|
||||||
help_menu = tk.Menu(menubar, tearoff=0)
|
|
||||||
help_menu.add_command(label="О программе", command=self._show_about)
|
|
||||||
menubar.add_cascade(label="Справка", menu=help_menu)
|
|
||||||
|
|
||||||
self.config(menu=menubar)
|
|
||||||
|
|
||||||
def _create_toolbar(self) -> None:
|
|
||||||
"""Создание панели инструментов."""
|
|
||||||
toolbar = ttk.Frame(self)
|
|
||||||
toolbar.pack(fill=tk.X, padx=5, pady=2)
|
|
||||||
|
|
||||||
# Кнопка сохранения
|
|
||||||
ttk.Button(
|
|
||||||
toolbar,
|
|
||||||
text="Сохранить",
|
|
||||||
command=self._save_config_changes
|
|
||||||
).pack(side=tk.LEFT, padx=2)
|
|
||||||
|
|
||||||
ttk.Separator(toolbar, orient=tk.VERTICAL).pack(side=tk.LEFT, padx=5, fill=tk.Y)
|
|
||||||
|
|
||||||
# Кнопки подключения
|
|
||||||
ttk.Button(
|
|
||||||
toolbar,
|
|
||||||
text="Подключить",
|
|
||||||
command=self._connect_device
|
|
||||||
).pack(side=tk.LEFT, padx=2)
|
|
||||||
|
|
||||||
ttk.Button(
|
|
||||||
toolbar,
|
|
||||||
text="Отключить",
|
|
||||||
command=self._disconnect_device
|
|
||||||
).pack(side=tk.LEFT, padx=2)
|
|
||||||
|
|
||||||
ttk.Separator(toolbar, orient=tk.VERTICAL).pack(side=tk.LEFT, padx=5, fill=tk.Y)
|
|
||||||
|
|
||||||
# Кнопки передачи
|
|
||||||
ttk.Button(
|
|
||||||
toolbar,
|
|
||||||
text="Передать",
|
|
||||||
command=self._transfer_config
|
|
||||||
).pack(side=tk.LEFT, padx=2)
|
|
||||||
|
|
||||||
ttk.Button(
|
|
||||||
toolbar,
|
|
||||||
text="Отменить",
|
|
||||||
command=self._cancel_transfer
|
|
||||||
).pack(side=tk.LEFT, padx=2)
|
|
||||||
|
|
||||||
ttk.Separator(toolbar, orient=tk.VERTICAL).pack(side=tk.LEFT, padx=5, fill=tk.Y)
|
|
||||||
|
|
||||||
# Кнопки TFTP сервера
|
|
||||||
ttk.Button(
|
|
||||||
toolbar,
|
|
||||||
text="Запустить TFTP",
|
|
||||||
command=self._start_tftp_server
|
|
||||||
).pack(side=tk.LEFT, padx=2)
|
|
||||||
|
|
||||||
ttk.Button(
|
|
||||||
toolbar,
|
|
||||||
text="Остановить TFTP",
|
|
||||||
command=self._stop_tftp_server
|
|
||||||
).pack(side=tk.LEFT, padx=2)
|
|
||||||
|
|
||||||
def _setup_event_handlers(self) -> None:
|
|
||||||
"""Настройка обработчиков событий."""
|
|
||||||
# Привязка сохранения к Ctrl+S
|
|
||||||
self.bind("<Control-s>", lambda e: self._save_config_changes())
|
|
||||||
|
|
||||||
# Привязка отслеживания изменений в редакторе
|
|
||||||
self._editor.bind("<<Modified>>", self._on_editor_change)
|
|
||||||
|
|
||||||
# События подключения
|
|
||||||
event_bus.subscribe(EventTypes.CONNECTION_ESTABLISHED, self._on_connection_established)
|
|
||||||
event_bus.subscribe(EventTypes.CONNECTION_LOST, self._on_connection_lost)
|
|
||||||
event_bus.subscribe(EventTypes.CONNECTION_ERROR, self._on_connection_error)
|
|
||||||
|
|
||||||
# События передачи
|
|
||||||
event_bus.subscribe(EventTypes.TRANSFER_STARTED, self._on_transfer_started)
|
|
||||||
event_bus.subscribe(EventTypes.TRANSFER_COMPLETED, self._on_transfer_completed)
|
|
||||||
event_bus.subscribe(EventTypes.TRANSFER_ERROR, self._on_transfer_error)
|
|
||||||
|
|
||||||
# События конфигурации
|
|
||||||
event_bus.subscribe(EventTypes.CONFIG_LOADED, self._on_config_loaded)
|
|
||||||
event_bus.subscribe(EventTypes.CONFIG_SAVED, self._on_config_saved)
|
|
||||||
event_bus.subscribe(EventTypes.CONFIG_DELETED, self._on_config_deleted)
|
|
||||||
|
|
||||||
# События UI
|
|
||||||
event_bus.subscribe(EventTypes.UI_STATUS_CHANGED, self._on_status_changed)
|
|
||||||
|
|
||||||
# События списка конфигураций
|
|
||||||
self._configs_list.bind("<<ListboxSelect>>", self._on_config_selected)
|
|
||||||
|
|
||||||
def _load_configs(self) -> None:
|
|
||||||
"""Загрузка списка конфигураций."""
|
|
||||||
try:
|
|
||||||
# Получаем список конфигураций
|
|
||||||
configs = self._config_manager.list_configs()
|
|
||||||
|
|
||||||
# Очищаем список
|
|
||||||
self._configs_list.delete(0, tk.END)
|
|
||||||
|
|
||||||
# Добавляем конфигурации в список
|
|
||||||
for config in configs:
|
|
||||||
self._configs_list.insert(tk.END, config["name"])
|
|
||||||
|
|
||||||
self._logger.debug(f"Загружено {len(configs)} конфигураций")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка загрузки конфигураций: {e}")
|
|
||||||
messagebox.showerror("Ошибка", "Не удалось загрузить список конфигураций")
|
|
||||||
|
|
||||||
def _add_config(self) -> None:
|
|
||||||
"""Добавление новой конфигурации."""
|
|
||||||
try:
|
|
||||||
# Показываем диалог выбора файла
|
|
||||||
file_path = tk.filedialog.askopenfilename(
|
|
||||||
title="Выберите файл конфигурации",
|
|
||||||
filetypes=[
|
|
||||||
("Текстовые файлы", "*.txt"),
|
|
||||||
("Все файлы", "*.*")
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
if not file_path:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Получаем содержимое файла
|
|
||||||
with open(file_path, "r", encoding="utf-8") as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
# Сохраняем в директорию конфигураций
|
|
||||||
config_name = os.path.basename(file_path)
|
|
||||||
self._config_manager.save_config(config_name, content)
|
|
||||||
|
|
||||||
# Обновляем список конфигураций
|
|
||||||
self._load_configs()
|
|
||||||
|
|
||||||
# Выбираем добавленную конфигурацию
|
|
||||||
configs = self._configs_list.get(0, tk.END)
|
|
||||||
if config_name in configs:
|
|
||||||
index = configs.index(config_name)
|
|
||||||
self._configs_list.selection_set(index)
|
|
||||||
self._configs_list.see(index)
|
|
||||||
|
|
||||||
self._logger.info(f"Добавлена конфигурация: {config_name}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка добавления конфигурации: {e}")
|
|
||||||
messagebox.showerror(
|
|
||||||
"Ошибка",
|
|
||||||
f"Не удалось добавить конфигурацию:\n{str(e)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def _remove_config(self) -> None:
|
|
||||||
"""Удаление выбранной конфигурации."""
|
|
||||||
selection = self._configs_list.curselection()
|
|
||||||
if not selection:
|
|
||||||
return
|
|
||||||
|
|
||||||
config_name = self._configs_list.get(selection[0])
|
|
||||||
|
|
||||||
if messagebox.askyesno(
|
|
||||||
"Подтверждение",
|
|
||||||
f"Удалить конфигурацию {config_name}?"
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
self._config_manager.delete_config(config_name)
|
|
||||||
self._load_configs()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка удаления конфигурации: {e}")
|
|
||||||
messagebox.showerror("Ошибка", "Не удалось удалить конфигурацию")
|
|
||||||
|
|
||||||
def _show_settings(self) -> None:
|
|
||||||
"""Отображение диалога настроек."""
|
|
||||||
try:
|
|
||||||
# Получаем текущие настройки
|
|
||||||
settings = {
|
|
||||||
"port": self._serial_manager._protocol._port,
|
|
||||||
"baudrate": self._serial_manager._protocol._baudrate,
|
|
||||||
"timeout": self._serial_manager._protocol._timeout,
|
|
||||||
"prompt": self._serial_manager._protocol._prompt,
|
|
||||||
"copy_mode": self._state_manager.get_custom_data("copy_mode", AppConfig.DEFAULT_COPY_MODE),
|
|
||||||
"block_size": self._state_manager.get_custom_data("block_size", AppConfig.DEFAULT_BLOCK_SIZE),
|
|
||||||
"tftp_port": self._tftp_server._port
|
|
||||||
}
|
|
||||||
|
|
||||||
# Создаем и показываем диалог
|
|
||||||
dialog = SettingsDialog(self, settings)
|
|
||||||
self.wait_window(dialog)
|
|
||||||
|
|
||||||
# Если настройки были изменены, применяем их
|
|
||||||
if dialog.settings != settings:
|
|
||||||
self._apply_settings(dialog.settings)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка отображения настроек: {e}")
|
|
||||||
messagebox.showerror(
|
|
||||||
"Ошибка",
|
|
||||||
f"Не удалось открыть настройки:\n{str(e)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def _show_about(self) -> None:
|
|
||||||
"""Отображение диалога 'О программе'."""
|
|
||||||
AboutDialog(self)
|
|
||||||
|
|
||||||
def _connect_device(self) -> None:
|
|
||||||
"""Подключение к устройству."""
|
|
||||||
try:
|
|
||||||
self._serial_manager.connect()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка подключения: {e}")
|
|
||||||
messagebox.showerror("Ошибка", f"Не удалось подключиться к устройству: {e}")
|
|
||||||
|
|
||||||
def _disconnect_device(self) -> None:
|
|
||||||
"""Отключение от устройства."""
|
|
||||||
try:
|
|
||||||
self._serial_manager.disconnect()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка отключения: {e}")
|
|
||||||
messagebox.showerror("Ошибка", f"Не удалось отключиться от устройства: {e}")
|
|
||||||
|
|
||||||
def _update_device_info(self) -> None:
|
|
||||||
"""Обновление информации об устройстве."""
|
|
||||||
if not self._serial_manager.is_connected:
|
|
||||||
messagebox.showwarning("Предупреждение", "Нет подключения к устройству")
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Получаем информацию об устройстве
|
|
||||||
device_info = self._serial_manager.get_device_info()
|
|
||||||
|
|
||||||
# Обновляем состояние
|
|
||||||
self._state_manager.update_device_info(device_info)
|
|
||||||
|
|
||||||
# Обновляем отображение
|
|
||||||
self._device_info["hostname"].configure(text=device_info.get("hostname", "-"))
|
|
||||||
self._device_info["model"].configure(text=device_info.get("model", "-"))
|
|
||||||
self._device_info["version"].configure(text=device_info.get("version", "-"))
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка получения информации об устройстве: {e}")
|
|
||||||
messagebox.showerror("Ошибка", "Не удалось получить информацию об устройстве")
|
|
||||||
|
|
||||||
def _transfer_config(self) -> None:
|
|
||||||
"""Передача конфигурации на устройство."""
|
|
||||||
if not self._serial_manager.is_connected:
|
|
||||||
messagebox.showwarning("Предупреждение", "Нет подключения к устройству")
|
|
||||||
return
|
|
||||||
|
|
||||||
selection = self._configs_list.curselection()
|
|
||||||
if not selection:
|
|
||||||
messagebox.showwarning("Предупреждение", "Не выбрана конфигурация")
|
|
||||||
return
|
|
||||||
|
|
||||||
config_name = self._configs_list.get(selection[0])
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Получаем путь к файлу конфигурации
|
|
||||||
config_info = self._config_manager.get_config_info(config_name)
|
|
||||||
if not config_info:
|
|
||||||
raise ValueError("Конфигурация не найдена")
|
|
||||||
|
|
||||||
# Запускаем передачу
|
|
||||||
self._transfer_manager.transfer_config(config_info["path"])
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка передачи конфигурации: {e}")
|
|
||||||
messagebox.showerror("Ошибка", "Не удалось передать конфигурацию")
|
|
||||||
|
|
||||||
def _cancel_transfer(self) -> None:
|
|
||||||
"""Отмена передачи конфигурации."""
|
|
||||||
try:
|
|
||||||
self._transfer_manager.cancel_all_transfers()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка отмены передачи: {e}")
|
|
||||||
messagebox.showerror("Ошибка", "Не удалось отменить передачу")
|
|
||||||
|
|
||||||
def _start_tftp_server(self) -> None:
|
|
||||||
"""Запуск TFTP сервера."""
|
|
||||||
try:
|
|
||||||
self._tftp_server.start()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка запуска TFTP сервера: {e}")
|
|
||||||
messagebox.showerror("Ошибка", "Не удалось запустить TFTP сервер")
|
|
||||||
|
|
||||||
def _stop_tftp_server(self) -> None:
|
|
||||||
"""Остановка TFTP сервера."""
|
|
||||||
try:
|
|
||||||
self._tftp_server.stop()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка остановки TFTP сервера: {e}")
|
|
||||||
messagebox.showerror("Ошибка", "Не удалось остановить TFTP сервер")
|
|
||||||
|
|
||||||
def _on_config_selected(self, event) -> None:
|
|
||||||
"""
|
|
||||||
Обработка выбора конфигурации в списке.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Событие выбора
|
|
||||||
"""
|
|
||||||
if self._has_unsaved_changes:
|
|
||||||
if messagebox.askyesno(
|
|
||||||
"Несохраненные изменения",
|
|
||||||
"Есть несохраненные изменения. Сохранить?"
|
|
||||||
):
|
|
||||||
self._save_config_changes()
|
|
||||||
|
|
||||||
selection = self._configs_list.curselection()
|
|
||||||
if not selection:
|
|
||||||
return
|
|
||||||
|
|
||||||
config_name = self._configs_list.get(selection[0])
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Загружаем содержимое конфигурации
|
|
||||||
content = self._config_manager.load_config(config_name)
|
|
||||||
|
|
||||||
# Обновляем редактор
|
|
||||||
self._editor.set_text(content)
|
|
||||||
|
|
||||||
# Обновляем состояние
|
|
||||||
self._state_manager.set_current_config(config_name)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка загрузки конфигурации: {e}")
|
|
||||||
messagebox.showerror("Ошибка", "Не удалось загрузить конфигурацию")
|
|
||||||
|
|
||||||
def _on_connection_established(self, event: Event) -> None:
|
|
||||||
"""
|
|
||||||
Обработка установки соединения.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Событие подключения
|
|
||||||
"""
|
|
||||||
self._update_device_info()
|
|
||||||
|
|
||||||
def _on_connection_lost(self, event: Event) -> None:
|
|
||||||
"""
|
|
||||||
Обработка потери соединения.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Событие отключения
|
|
||||||
"""
|
|
||||||
# Очищаем информацию об устройстве
|
|
||||||
self._device_info["hostname"].configure(text="-")
|
|
||||||
self._device_info["model"].configure(text="-")
|
|
||||||
self._device_info["version"].configure(text="-")
|
|
||||||
|
|
||||||
# Очищаем состояние
|
|
||||||
self._state_manager.clear_device_info()
|
|
||||||
|
|
||||||
def _on_connection_error(self, event: Event) -> None:
|
|
||||||
"""
|
|
||||||
Обработка ошибки соединения.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Событие ошибки
|
|
||||||
"""
|
|
||||||
messagebox.showerror("Ошибка", str(event.data))
|
|
||||||
|
|
||||||
def _on_transfer_started(self, event: Event) -> None:
|
|
||||||
"""
|
|
||||||
Обработка начала передачи.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Событие начала передачи
|
|
||||||
"""
|
|
||||||
# Блокируем редактор
|
|
||||||
self._editor.set_readonly(True)
|
|
||||||
|
|
||||||
def _on_transfer_completed(self, event: Event) -> None:
|
|
||||||
"""
|
|
||||||
Обработка завершения передачи.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Событие завершения передачи
|
|
||||||
"""
|
|
||||||
# Разблокируем редактор
|
|
||||||
self._editor.set_readonly(False)
|
|
||||||
|
|
||||||
def _on_transfer_error(self, event: Event) -> None:
|
|
||||||
"""
|
|
||||||
Обработка ошибки передачи.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Событие ошибки
|
|
||||||
"""
|
|
||||||
# Разблокируем редактор
|
|
||||||
self._editor.set_readonly(False)
|
|
||||||
|
|
||||||
messagebox.showerror("Ошибка", str(event.data))
|
|
||||||
|
|
||||||
def _on_config_loaded(self, event: Event) -> None:
|
|
||||||
"""
|
|
||||||
Обработка загрузки конфигурации.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Событие загрузки
|
|
||||||
"""
|
|
||||||
self._state_manager.update_config_info(event.data)
|
|
||||||
|
|
||||||
def _on_config_saved(self, event: Event) -> None:
|
|
||||||
"""
|
|
||||||
Обработка сохранения конфигурации.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Событие сохранения
|
|
||||||
"""
|
|
||||||
self._state_manager.update_config_info(event.data)
|
|
||||||
self._load_configs()
|
|
||||||
|
|
||||||
def _on_config_deleted(self, event: Event) -> None:
|
|
||||||
"""
|
|
||||||
Обработка удаления конфигурации.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Событие удаления
|
|
||||||
"""
|
|
||||||
self._state_manager.remove_config_info(event.data["name"])
|
|
||||||
self._load_configs()
|
|
||||||
|
|
||||||
def _on_status_changed(self, event: Event) -> None:
|
|
||||||
"""
|
|
||||||
Обработка изменения статуса.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Событие изменения статуса
|
|
||||||
"""
|
|
||||||
if isinstance(event.data, dict):
|
|
||||||
message = event.data.get("message", "")
|
|
||||||
else:
|
|
||||||
message = str(event.data)
|
|
||||||
|
|
||||||
self._status_bar.configure(text=message)
|
|
||||||
|
|
||||||
def _on_editor_change(self, event=None) -> None:
|
|
||||||
"""
|
|
||||||
Обработка изменений в редакторе.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Событие изменения
|
|
||||||
"""
|
|
||||||
if not self._has_unsaved_changes:
|
|
||||||
self._has_unsaved_changes = True
|
|
||||||
self.title(f"*ComConfigCopy v{AppConfig.VERSION}")
|
|
||||||
|
|
||||||
def _save_config_changes(self) -> None:
|
|
||||||
"""Сохранение изменений в редакторе конфигурации."""
|
|
||||||
try:
|
|
||||||
# Получаем текущую конфигурацию
|
|
||||||
selection = self._configs_list.curselection()
|
|
||||||
if not selection:
|
|
||||||
return
|
|
||||||
|
|
||||||
config_name = self._configs_list.get(selection[0])
|
|
||||||
|
|
||||||
# Получаем содержимое редактора
|
|
||||||
content = self._editor.get_text()
|
|
||||||
|
|
||||||
# Сохраняем изменения
|
|
||||||
self._config_manager.save_config(config_name, content)
|
|
||||||
|
|
||||||
# Сбрасываем флаг изменений
|
|
||||||
self._has_unsaved_changes = False
|
|
||||||
self.title(f"ComConfigCopy v{AppConfig.VERSION}")
|
|
||||||
|
|
||||||
self._logger.info(f"Сохранены изменения в конфигурации: {config_name}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка сохранения изменений: {e}")
|
|
||||||
messagebox.showerror(
|
|
||||||
"Ошибка",
|
|
||||||
f"Не удалось сохранить изменения:\n{str(e)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def _apply_settings(self, settings: Dict[str, Any]) -> None:
|
|
||||||
"""
|
|
||||||
Применение новых настроек.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
settings: Словарь с настройками
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Настраиваем последовательный порт
|
|
||||||
self._serial_manager._protocol.configure(
|
|
||||||
port=settings["port"],
|
|
||||||
baudrate=settings["baudrate"],
|
|
||||||
timeout=settings["timeout"],
|
|
||||||
prompt=settings["prompt"]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Настраиваем TFTP сервер
|
|
||||||
self._tftp_server.configure(port=settings["tftp_port"])
|
|
||||||
|
|
||||||
# Сохраняем настройки передачи
|
|
||||||
self._state_manager.set_custom_data("copy_mode", settings["copy_mode"])
|
|
||||||
self._state_manager.set_custom_data("block_size", settings["block_size"])
|
|
||||||
|
|
||||||
self._logger.info("Настройки применены")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._logger.error(f"Ошибка применения настроек: {e}")
|
|
||||||
messagebox.showerror(
|
|
||||||
"Ошибка",
|
|
||||||
f"Не удалось применить настройки:\n{str(e)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def _on_close(self) -> None:
|
|
||||||
"""Обработка закрытия окна."""
|
|
||||||
if self._has_unsaved_changes:
|
|
||||||
answer = messagebox.askyesnocancel(
|
|
||||||
"Несохраненные изменения",
|
|
||||||
"Есть несохраненные изменения. Сохранить перед выходом?"
|
|
||||||
)
|
|
||||||
|
|
||||||
if answer is None: # Отмена
|
|
||||||
return
|
|
||||||
elif answer: # Да
|
|
||||||
self._save_config_changes()
|
|
||||||
|
|
||||||
# Останавливаем компоненты
|
|
||||||
self._config_watcher.stop()
|
|
||||||
self._tftp_server.stop()
|
|
||||||
self._serial_manager.disconnect()
|
|
||||||
|
|
||||||
# Закрываем окно
|
|
||||||
self.quit()
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import tkinter as tk
|
|
||||||
from tkinter import ttk
|
|
||||||
from typing import Optional, Callable, Any
|
|
||||||
import re
|
|
||||||
|
|
||||||
class CustomEntry(ttk.Frame):
|
|
||||||
"""Пользовательское поле ввода с дополнительными возможностями."""
|
|
||||||
|
|
||||||
def __init__(self, master, validator: Optional[Callable[[str], bool]] = None,
|
|
||||||
placeholder: Optional[str] = None, **kwargs):
|
|
||||||
super().__init__(master)
|
|
||||||
|
|
||||||
# Создаем поле ввода
|
|
||||||
self.entry = ttk.Entry(self, **kwargs)
|
|
||||||
self.entry.pack(fill=tk.X, expand=True)
|
|
||||||
|
|
||||||
# Функция валидации
|
|
||||||
self._validator = validator
|
|
||||||
|
|
||||||
# Текст подсказки
|
|
||||||
self._placeholder = placeholder
|
|
||||||
self._placeholder_color = "gray"
|
|
||||||
self._default_fg = self.entry["foreground"]
|
|
||||||
|
|
||||||
# Флаг показа пароля
|
|
||||||
self._show_password = False
|
|
||||||
self._default_show = self.entry.cget("show")
|
|
||||||
|
|
||||||
# Привязываем события
|
|
||||||
self.entry.bind("<FocusIn>", self._on_focus_in)
|
|
||||||
self.entry.bind("<FocusOut>", self._on_focus_out)
|
|
||||||
self.entry.bind("<KeyRelease>", self._on_key_release)
|
|
||||||
|
|
||||||
# Добавляем контекстное меню
|
|
||||||
self._create_context_menu()
|
|
||||||
self.entry.bind("<Button-3>", self._show_context_menu)
|
|
||||||
|
|
||||||
# Устанавливаем подсказку, если указана
|
|
||||||
if placeholder:
|
|
||||||
self._show_placeholder()
|
|
||||||
|
|
||||||
def _create_context_menu(self) -> None:
|
|
||||||
"""Создание контекстного меню."""
|
|
||||||
self.context_menu = tk.Menu(self, tearoff=0)
|
|
||||||
self.context_menu.add_command(label="Копировать", command=self.copy)
|
|
||||||
self.context_menu.add_command(label="Вставить", command=self.paste)
|
|
||||||
self.context_menu.add_command(label="Вырезать", command=self.cut)
|
|
||||||
self.context_menu.add_separator()
|
|
||||||
self.context_menu.add_command(label="Выделить всё", command=self.select_all)
|
|
||||||
|
|
||||||
def _show_context_menu(self, event) -> None:
|
|
||||||
"""
|
|
||||||
Отображение контекстного меню.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Событие мыши
|
|
||||||
"""
|
|
||||||
state = tk.NORMAL if not self.entry.cget("state") == tk.DISABLED else tk.DISABLED
|
|
||||||
self.context_menu.entryconfig("Вставить", state=state)
|
|
||||||
self.context_menu.entryconfig("Вырезать", state=state)
|
|
||||||
|
|
||||||
self.context_menu.tk_popup(event.x_root, event.y_root)
|
|
||||||
|
|
||||||
def _on_focus_in(self, event) -> None:
|
|
||||||
"""
|
|
||||||
Обработка получения фокуса.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Событие фокуса
|
|
||||||
"""
|
|
||||||
if self._placeholder and self.entry.get() == self._placeholder:
|
|
||||||
self.entry.delete(0, tk.END)
|
|
||||||
self.entry.configure(foreground=self._default_fg)
|
|
||||||
|
|
||||||
def _on_focus_out(self, event) -> None:
|
|
||||||
"""
|
|
||||||
Обработка потери фокуса.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Событие фокуса
|
|
||||||
"""
|
|
||||||
if self._placeholder and not self.entry.get():
|
|
||||||
self._show_placeholder()
|
|
||||||
|
|
||||||
def _on_key_release(self, event) -> None:
|
|
||||||
"""
|
|
||||||
Обработка отпускания клавиши.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Событие клавиатуры
|
|
||||||
"""
|
|
||||||
if self._validator:
|
|
||||||
text = self.entry.get()
|
|
||||||
if not self._validator(text):
|
|
||||||
self.entry.configure(foreground="red")
|
|
||||||
else:
|
|
||||||
self.entry.configure(foreground=self._default_fg)
|
|
||||||
|
|
||||||
def _show_placeholder(self) -> None:
|
|
||||||
"""Отображение текста подсказки."""
|
|
||||||
self.entry.delete(0, tk.END)
|
|
||||||
self.entry.insert(0, self._placeholder)
|
|
||||||
self.entry.configure(foreground=self._placeholder_color)
|
|
||||||
|
|
||||||
def get(self) -> str:
|
|
||||||
"""
|
|
||||||
Получение текста.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Текст поля ввода
|
|
||||||
"""
|
|
||||||
text = self.entry.get()
|
|
||||||
if self._placeholder and text == self._placeholder:
|
|
||||||
return ""
|
|
||||||
return text
|
|
||||||
|
|
||||||
def set(self, text: str) -> None:
|
|
||||||
"""
|
|
||||||
Установка текста в поле ввода.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
text: Текст для установки
|
|
||||||
"""
|
|
||||||
self.entry.delete(0, tk.END)
|
|
||||||
if text:
|
|
||||||
self.entry.insert(0, text)
|
|
||||||
self.entry.configure(foreground=self._default_fg)
|
|
||||||
|
|
||||||
def clear(self) -> None:
|
|
||||||
"""Очистка текста."""
|
|
||||||
self.entry.delete(0, tk.END)
|
|
||||||
if self._placeholder:
|
|
||||||
self._show_placeholder()
|
|
||||||
|
|
||||||
def copy(self) -> None:
|
|
||||||
"""Копирование текста."""
|
|
||||||
self.entry.event_generate("<<Copy>>")
|
|
||||||
|
|
||||||
def paste(self) -> None:
|
|
||||||
"""Вставка текста."""
|
|
||||||
self.entry.event_generate("<<Paste>>")
|
|
||||||
|
|
||||||
def cut(self) -> None:
|
|
||||||
"""Вырезание текста."""
|
|
||||||
self.entry.event_generate("<<Cut>>")
|
|
||||||
|
|
||||||
def select_all(self) -> None:
|
|
||||||
"""Выделение всего текста."""
|
|
||||||
self.entry.select_range(0, tk.END)
|
|
||||||
self.entry.icursor(tk.END)
|
|
||||||
|
|
||||||
def set_validator(self, validator: Optional[Callable[[str], bool]]) -> None:
|
|
||||||
"""
|
|
||||||
Установка функции валидации.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
validator: Функция валидации
|
|
||||||
"""
|
|
||||||
self._validator = validator
|
|
||||||
|
|
||||||
def set_placeholder(self, text: Optional[str]) -> None:
|
|
||||||
"""
|
|
||||||
Установка текста подсказки.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
text: Текст подсказки
|
|
||||||
"""
|
|
||||||
self._placeholder = text
|
|
||||||
if not self.entry.get():
|
|
||||||
self._show_placeholder()
|
|
||||||
|
|
||||||
def toggle_password(self) -> None:
|
|
||||||
"""Переключение отображения пароля."""
|
|
||||||
self._show_password = not self._show_password
|
|
||||||
if self._show_password:
|
|
||||||
self.entry.configure(show="")
|
|
||||||
else:
|
|
||||||
self.entry.configure(show=self._default_show)
|
|
||||||
|
|
||||||
def set_state(self, state: str) -> None:
|
|
||||||
"""
|
|
||||||
Установка состояния поля ввода.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
state: Состояние (tk.NORMAL или tk.DISABLED)
|
|
||||||
"""
|
|
||||||
self.entry.configure(state=state)
|
|
||||||
|
|
||||||
def bind_key(self, key: str, callback: Callable) -> None:
|
|
||||||
"""
|
|
||||||
Привязка обработчика к клавише.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Клавиша или комбинация клавиш
|
|
||||||
callback: Функция обработчик
|
|
||||||
"""
|
|
||||||
self.entry.bind(key, callback)
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import tkinter as tk
|
|
||||||
from tkinter import ttk
|
|
||||||
from typing import Optional, Callable
|
|
||||||
import re
|
|
||||||
|
|
||||||
class CustomText(ttk.Frame):
|
|
||||||
"""Пользовательский текстовый виджет с дополнительными возможностями."""
|
|
||||||
|
|
||||||
def __init__(self, master, **kwargs):
|
|
||||||
super().__init__(master)
|
|
||||||
|
|
||||||
# Создаем текстовый виджет
|
|
||||||
self.text = tk.Text(
|
|
||||||
self,
|
|
||||||
wrap=tk.WORD,
|
|
||||||
undo=True,
|
|
||||||
**kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
# Создаем скроллбар
|
|
||||||
self.scrollbar = ttk.Scrollbar(
|
|
||||||
self,
|
|
||||||
orient=tk.VERTICAL,
|
|
||||||
command=self.text.yview
|
|
||||||
)
|
|
||||||
|
|
||||||
# Настраиваем прокрутку
|
|
||||||
self.text.configure(yscrollcommand=self.scrollbar.set)
|
|
||||||
|
|
||||||
# Размещаем виджеты
|
|
||||||
self.text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
||||||
self.scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
|
||||||
|
|
||||||
# Настраиваем теги
|
|
||||||
self.text.tag_configure("error", foreground="red")
|
|
||||||
self.text.tag_configure("success", foreground="green")
|
|
||||||
self.text.tag_configure("warning", foreground="orange")
|
|
||||||
self.text.tag_configure("info", foreground="blue")
|
|
||||||
|
|
||||||
# Добавляем контекстное меню
|
|
||||||
self._create_context_menu()
|
|
||||||
|
|
||||||
# Привязываем события
|
|
||||||
self.text.bind("<Button-3>", self._show_context_menu)
|
|
||||||
|
|
||||||
# Флаг только для чтения
|
|
||||||
self._readonly = False
|
|
||||||
|
|
||||||
def _create_context_menu(self) -> None:
|
|
||||||
"""Создание контекстного меню."""
|
|
||||||
self.context_menu = tk.Menu(self, tearoff=0)
|
|
||||||
self.context_menu.add_command(label="Копировать", command=self.copy)
|
|
||||||
self.context_menu.add_command(label="Вставить", command=self.paste)
|
|
||||||
self.context_menu.add_command(label="Вырезать", command=self.cut)
|
|
||||||
self.context_menu.add_separator()
|
|
||||||
self.context_menu.add_command(label="Выделить всё", command=self.select_all)
|
|
||||||
|
|
||||||
def _show_context_menu(self, event) -> None:
|
|
||||||
"""
|
|
||||||
Отображение контекстного меню.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Событие мыши
|
|
||||||
"""
|
|
||||||
if self._readonly:
|
|
||||||
# В режиме только для чтения оставляем только копирование
|
|
||||||
self.context_menu.entryconfig("Вставить", state=tk.DISABLED)
|
|
||||||
self.context_menu.entryconfig("Вырезать", state=tk.DISABLED)
|
|
||||||
else:
|
|
||||||
self.context_menu.entryconfig("Вставить", state=tk.NORMAL)
|
|
||||||
self.context_menu.entryconfig("Вырезать", state=tk.NORMAL)
|
|
||||||
|
|
||||||
self.context_menu.tk_popup(event.x_root, event.y_root)
|
|
||||||
|
|
||||||
def get_text(self) -> str:
|
|
||||||
"""
|
|
||||||
Получение текста.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Текст виджета
|
|
||||||
"""
|
|
||||||
return self.text.get("1.0", tk.END).strip()
|
|
||||||
|
|
||||||
def set_text(self, content: str) -> None:
|
|
||||||
"""
|
|
||||||
Установка текста.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
content: Текст для установки
|
|
||||||
"""
|
|
||||||
self.clear()
|
|
||||||
self.text.insert("1.0", content)
|
|
||||||
|
|
||||||
def append_text(self, content: str, tag: Optional[str] = None) -> None:
|
|
||||||
"""
|
|
||||||
Добавление текста.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
content: Текст для добавления
|
|
||||||
tag: Тег форматирования
|
|
||||||
"""
|
|
||||||
self.text.insert(tk.END, content + "\n", tag)
|
|
||||||
self.text.see(tk.END)
|
|
||||||
|
|
||||||
def clear(self) -> None:
|
|
||||||
"""Очистка текста."""
|
|
||||||
self.text.delete("1.0", tk.END)
|
|
||||||
|
|
||||||
def copy(self) -> None:
|
|
||||||
"""Копирование выделенного текста."""
|
|
||||||
self.text.event_generate("<<Copy>>")
|
|
||||||
|
|
||||||
def paste(self) -> None:
|
|
||||||
"""Вставка текста."""
|
|
||||||
if not self._readonly:
|
|
||||||
self.text.event_generate("<<Paste>>")
|
|
||||||
|
|
||||||
def cut(self) -> None:
|
|
||||||
"""Вырезание выделенного текста."""
|
|
||||||
if not self._readonly:
|
|
||||||
self.text.event_generate("<<Cut>>")
|
|
||||||
|
|
||||||
def select_all(self) -> None:
|
|
||||||
"""Выделение всего текста."""
|
|
||||||
self.text.tag_add(tk.SEL, "1.0", tk.END)
|
|
||||||
self.text.mark_set(tk.INSERT, "1.0")
|
|
||||||
self.text.see(tk.INSERT)
|
|
||||||
|
|
||||||
def highlight_pattern(self, pattern: str, tag: str) -> None:
|
|
||||||
"""
|
|
||||||
Подсветка текста по шаблону.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
pattern: Регулярное выражение
|
|
||||||
tag: Тег форматирования
|
|
||||||
"""
|
|
||||||
self.text.tag_remove(tag, "1.0", tk.END)
|
|
||||||
|
|
||||||
start = "1.0"
|
|
||||||
while True:
|
|
||||||
start = self.text.search(pattern, start, tk.END, regexp=True)
|
|
||||||
if not start:
|
|
||||||
break
|
|
||||||
|
|
||||||
end = f"{start}+{len(pattern)}c"
|
|
||||||
self.text.tag_add(tag, start, end)
|
|
||||||
start = end
|
|
||||||
|
|
||||||
def set_readonly(self, readonly: bool = True) -> None:
|
|
||||||
"""
|
|
||||||
Установка режима только для чтения.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
readonly: Флаг режима
|
|
||||||
"""
|
|
||||||
self._readonly = readonly
|
|
||||||
state = tk.DISABLED if readonly else tk.NORMAL
|
|
||||||
self.text.configure(state=state)
|
|
||||||
|
|
||||||
def bind_key(self, key: str, callback: Callable) -> None:
|
|
||||||
"""
|
|
||||||
Привязка обработчика к клавише.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Клавиша или комбинация клавиш
|
|
||||||
callback: Функция обработчик
|
|
||||||
"""
|
|
||||||
self.text.bind(key, callback)
|
|
||||||
|
|
||||||
def get_line(self, line_number: int) -> str:
|
|
||||||
"""
|
|
||||||
Получение строки по номеру.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
line_number: Номер строки (1-based)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Текст строки
|
|
||||||
"""
|
|
||||||
start = f"{line_number}.0"
|
|
||||||
end = f"{line_number}.end"
|
|
||||||
return self.text.get(start, end)
|
|
||||||
|
|
||||||
def get_selection(self) -> str:
|
|
||||||
"""
|
|
||||||
Получение выделенного текста.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Выделенный текст
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return self.text.get(tk.SEL_FIRST, tk.SEL_LAST)
|
|
||||||
except tk.TclError:
|
|
||||||
return ""
|
|
||||||
@@ -1,233 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import tkinter as tk
|
|
||||||
from tkinter import ttk
|
|
||||||
from typing import Optional, Dict, Any
|
|
||||||
import time
|
|
||||||
|
|
||||||
from core.events import event_bus, Event, EventTypes
|
|
||||||
|
|
||||||
class TransferView(ttk.Frame):
|
|
||||||
"""Виджет для отображения прогресса передачи файлов."""
|
|
||||||
|
|
||||||
def __init__(self, master):
|
|
||||||
super().__init__(master)
|
|
||||||
|
|
||||||
# Создаем элементы интерфейса
|
|
||||||
self._create_widgets()
|
|
||||||
|
|
||||||
# Подписываемся на события
|
|
||||||
self._setup_event_handlers()
|
|
||||||
|
|
||||||
# Состояние передачи
|
|
||||||
self._transfer_id: Optional[str] = None
|
|
||||||
self._start_time: float = 0
|
|
||||||
self._total_bytes: int = 0
|
|
||||||
self._transferred_bytes: int = 0
|
|
||||||
|
|
||||||
def _create_widgets(self) -> None:
|
|
||||||
"""Создание элементов интерфейса."""
|
|
||||||
# Заголовок
|
|
||||||
self.title_label = ttk.Label(
|
|
||||||
self,
|
|
||||||
text="Прогресс передачи",
|
|
||||||
font=("Arial", 10, "bold")
|
|
||||||
)
|
|
||||||
self.title_label.pack(fill=tk.X, padx=5, pady=5)
|
|
||||||
|
|
||||||
# Имя файла
|
|
||||||
self.file_frame = ttk.Frame(self)
|
|
||||||
self.file_frame.pack(fill=tk.X, padx=5)
|
|
||||||
|
|
||||||
ttk.Label(self.file_frame, text="Файл:").pack(side=tk.LEFT)
|
|
||||||
self.file_label = ttk.Label(self.file_frame, text="-")
|
|
||||||
self.file_label.pack(side=tk.LEFT, padx=(5, 0))
|
|
||||||
|
|
||||||
# Прогресс-бар
|
|
||||||
self.progress = ttk.Progressbar(
|
|
||||||
self,
|
|
||||||
orient=tk.HORIZONTAL,
|
|
||||||
mode="determinate",
|
|
||||||
length=200
|
|
||||||
)
|
|
||||||
self.progress.pack(fill=tk.X, padx=5, pady=5)
|
|
||||||
|
|
||||||
# Процент выполнения
|
|
||||||
self.percent_label = ttk.Label(self, text="0%")
|
|
||||||
self.percent_label.pack()
|
|
||||||
|
|
||||||
# Скорость передачи
|
|
||||||
self.speed_frame = ttk.Frame(self)
|
|
||||||
self.speed_frame.pack(fill=tk.X, padx=5)
|
|
||||||
|
|
||||||
ttk.Label(self.speed_frame, text="Скорость:").pack(side=tk.LEFT)
|
|
||||||
self.speed_label = ttk.Label(self.speed_frame, text="-")
|
|
||||||
self.speed_label.pack(side=tk.LEFT, padx=(5, 0))
|
|
||||||
|
|
||||||
# Оставшееся время
|
|
||||||
self.time_frame = ttk.Frame(self)
|
|
||||||
self.time_frame.pack(fill=tk.X, padx=5)
|
|
||||||
|
|
||||||
ttk.Label(self.time_frame, text="Осталось:").pack(side=tk.LEFT)
|
|
||||||
self.time_label = ttk.Label(self.time_frame, text="-")
|
|
||||||
self.time_label.pack(side=tk.LEFT, padx=(5, 0))
|
|
||||||
|
|
||||||
# Статус
|
|
||||||
self.status_label = ttk.Label(
|
|
||||||
self,
|
|
||||||
text="Готов к передаче",
|
|
||||||
font=("Arial", 9, "italic")
|
|
||||||
)
|
|
||||||
self.status_label.pack(pady=5)
|
|
||||||
|
|
||||||
def _setup_event_handlers(self) -> None:
|
|
||||||
"""Настройка обработчиков событий."""
|
|
||||||
event_bus.subscribe(EventTypes.TRANSFER_STARTED, self._handle_transfer_started)
|
|
||||||
event_bus.subscribe(EventTypes.TRANSFER_PROGRESS, self._handle_transfer_progress)
|
|
||||||
event_bus.subscribe(EventTypes.TRANSFER_COMPLETED, self._handle_transfer_completed)
|
|
||||||
event_bus.subscribe(EventTypes.TRANSFER_ERROR, self._handle_transfer_error)
|
|
||||||
|
|
||||||
def _handle_transfer_started(self, event: Event) -> None:
|
|
||||||
"""
|
|
||||||
Обработка начала передачи.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Событие начала передачи
|
|
||||||
"""
|
|
||||||
data = event.data
|
|
||||||
self._transfer_id = data.get("transfer_id")
|
|
||||||
self._total_bytes = data.get("total_bytes", 0)
|
|
||||||
self._transferred_bytes = 0
|
|
||||||
self._start_time = time.time()
|
|
||||||
|
|
||||||
# Обновляем интерфейс
|
|
||||||
self.file_label.configure(text=data.get("file", "-"))
|
|
||||||
self.progress.configure(value=0)
|
|
||||||
self.percent_label.configure(text="0%")
|
|
||||||
self.speed_label.configure(text="-")
|
|
||||||
self.time_label.configure(text="-")
|
|
||||||
self.status_label.configure(text="Передача начата...")
|
|
||||||
|
|
||||||
def _handle_transfer_progress(self, event: Event) -> None:
|
|
||||||
"""
|
|
||||||
Обработка прогресса передачи.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Событие прогресса
|
|
||||||
"""
|
|
||||||
data = event.data
|
|
||||||
if data.get("transfer_id") != self._transfer_id:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Обновляем прогресс
|
|
||||||
progress = data.get("progress", 0)
|
|
||||||
self.progress.configure(value=progress)
|
|
||||||
self.percent_label.configure(text=f"{progress:.1f}%")
|
|
||||||
|
|
||||||
# Обновляем скорость
|
|
||||||
speed = data.get("speed", 0)
|
|
||||||
self.speed_label.configure(text=self._format_speed(speed))
|
|
||||||
|
|
||||||
# Обновляем оставшееся время
|
|
||||||
time_left = data.get("estimated_time", 0)
|
|
||||||
self.time_label.configure(text=self._format_time(time_left))
|
|
||||||
|
|
||||||
# Обновляем статус
|
|
||||||
self.status_label.configure(text="Передача данных...")
|
|
||||||
|
|
||||||
def _handle_transfer_completed(self, event: Event) -> None:
|
|
||||||
"""
|
|
||||||
Обработка завершения передачи.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Событие завершения
|
|
||||||
"""
|
|
||||||
data = event.data
|
|
||||||
if data.get("transfer_id") != self._transfer_id:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Обновляем прогресс
|
|
||||||
self.progress.configure(value=100)
|
|
||||||
self.percent_label.configure(text="100%")
|
|
||||||
|
|
||||||
# Обновляем статус
|
|
||||||
total_time = data.get("total_time", 0)
|
|
||||||
average_speed = data.get("average_speed", 0)
|
|
||||||
|
|
||||||
status = (
|
|
||||||
f"Передача завершена за {self._format_time(total_time)}\n"
|
|
||||||
f"Средняя скорость: {self._format_speed(average_speed)}"
|
|
||||||
)
|
|
||||||
self.status_label.configure(text=status)
|
|
||||||
|
|
||||||
# Сбрасываем состояние
|
|
||||||
self._transfer_id = None
|
|
||||||
|
|
||||||
def _handle_transfer_error(self, event: Event) -> None:
|
|
||||||
"""
|
|
||||||
Обработка ошибки передачи.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Событие ошибки
|
|
||||||
"""
|
|
||||||
data = event.data
|
|
||||||
if data.get("transfer_id") != self._transfer_id:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Обновляем статус
|
|
||||||
error = data.get("error", "Неизвестная ошибка")
|
|
||||||
self.status_label.configure(text=f"Ошибка: {error}")
|
|
||||||
|
|
||||||
# Сбрасываем состояние
|
|
||||||
self._transfer_id = None
|
|
||||||
|
|
||||||
def _format_speed(self, speed: float) -> str:
|
|
||||||
"""
|
|
||||||
Форматирование скорости передачи.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
speed: Скорость в байтах в секунду
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Отформатированная строка
|
|
||||||
"""
|
|
||||||
if speed < 1024:
|
|
||||||
return f"{speed:.1f} Б/с"
|
|
||||||
elif speed < 1024 * 1024:
|
|
||||||
return f"{speed/1024:.1f} КБ/с"
|
|
||||||
else:
|
|
||||||
return f"{speed/1024/1024:.1f} МБ/с"
|
|
||||||
|
|
||||||
def _format_time(self, seconds: float) -> str:
|
|
||||||
"""
|
|
||||||
Форматирование времени.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
seconds: Время в секундах
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Отформатированная строка
|
|
||||||
"""
|
|
||||||
if seconds < 60:
|
|
||||||
return f"{seconds:.1f} сек"
|
|
||||||
elif seconds < 3600:
|
|
||||||
minutes = seconds / 60
|
|
||||||
return f"{minutes:.1f} мин"
|
|
||||||
else:
|
|
||||||
hours = seconds / 3600
|
|
||||||
return f"{hours:.1f} ч"
|
|
||||||
|
|
||||||
def reset(self) -> None:
|
|
||||||
"""Сброс состояния виджета."""
|
|
||||||
self._transfer_id = None
|
|
||||||
self._start_time = 0
|
|
||||||
self._total_bytes = 0
|
|
||||||
self._transferred_bytes = 0
|
|
||||||
|
|
||||||
self.file_label.configure(text="-")
|
|
||||||
self.progress.configure(value=0)
|
|
||||||
self.percent_label.configure(text="0%")
|
|
||||||
self.speed_label.configure(text="-")
|
|
||||||
self.time_label.configure(text="-")
|
|
||||||
self.status_label.configure(text="Готов к передаче")
|
|
||||||
175
update_checker.py
Normal file
175
update_checker.py
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
import threading
|
||||||
|
from packaging import version
|
||||||
|
|
||||||
|
class UpdateCheckError(Exception):
|
||||||
|
"""Исключение для ошибок проверки обновлений"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class UpdateChecker:
|
||||||
|
"""Класс для проверки обновлений программы"""
|
||||||
|
|
||||||
|
def __init__(self, current_version, repo_url):
|
||||||
|
self.current_version = current_version
|
||||||
|
self.repo_url = repo_url
|
||||||
|
# Формируем базовый URL API
|
||||||
|
self.api_url = repo_url.replace("gitea.filow.ru", "gitea.filow.ru/api/v1/repos/LowaSC/ComConfigCopy")
|
||||||
|
self._update_available = False
|
||||||
|
self._latest_version = None
|
||||||
|
self._latest_release = None
|
||||||
|
self._error = None
|
||||||
|
self._changelog = None
|
||||||
|
|
||||||
|
def get_changelog(self, callback=None):
|
||||||
|
"""
|
||||||
|
Получение changelog из репозитория.
|
||||||
|
:param callback: Функция обратного вызова, которая будет вызвана после получения changelog
|
||||||
|
"""
|
||||||
|
def fetch():
|
||||||
|
try:
|
||||||
|
# Пытаемся получить CHANGELOG.md из репозитория
|
||||||
|
response = requests.get(f"{self.api_url}/contents/CHANGELOG.md", timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
content = response.json()
|
||||||
|
if "content" in content:
|
||||||
|
import base64
|
||||||
|
changelog_content = base64.b64decode(content["content"]).decode("utf-8")
|
||||||
|
self._changelog = changelog_content
|
||||||
|
self._error = None
|
||||||
|
else:
|
||||||
|
raise UpdateCheckError("Не удалось получить содержимое CHANGELOG.md")
|
||||||
|
|
||||||
|
except requests.RequestException as e:
|
||||||
|
error_msg = f"Ошибка получения changelog: {e}"
|
||||||
|
logging.error(error_msg, exc_info=True)
|
||||||
|
self._error = error_msg
|
||||||
|
self._changelog = None
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Неизвестная ошибка при получении changelog: {e}"
|
||||||
|
logging.error(error_msg, exc_info=True)
|
||||||
|
self._error = error_msg
|
||||||
|
self._changelog = None
|
||||||
|
finally:
|
||||||
|
if callback:
|
||||||
|
callback(self._changelog, self._error)
|
||||||
|
|
||||||
|
# Запускаем получение в отдельном потоке
|
||||||
|
threading.Thread(target=fetch, daemon=True).start()
|
||||||
|
|
||||||
|
def check_updates(self, callback=None):
|
||||||
|
"""
|
||||||
|
Проверка наличия обновлений.
|
||||||
|
:param callback: Функция обратного вызова, которая будет вызвана после проверки
|
||||||
|
"""
|
||||||
|
def check():
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{self.api_url}/releases", timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
releases = response.json()
|
||||||
|
if not releases:
|
||||||
|
raise UpdateCheckError("Не найдено релизов в репозитории")
|
||||||
|
|
||||||
|
latest_release = releases[0]
|
||||||
|
latest_version = latest_release.get("tag_name", "").lstrip("v")
|
||||||
|
|
||||||
|
if not latest_version:
|
||||||
|
raise UpdateCheckError("Не удалось определить версию последнего релиза")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if version.parse(latest_version) > version.parse(self.current_version):
|
||||||
|
self._update_available = True
|
||||||
|
self._latest_version = latest_version
|
||||||
|
self._latest_release = latest_release
|
||||||
|
logging.info(f"Доступно обновление: {latest_version}")
|
||||||
|
else:
|
||||||
|
logging.info("Обновления не требуются")
|
||||||
|
except version.InvalidVersion as e:
|
||||||
|
raise UpdateCheckError(f"Некорректный формат версии: {e}")
|
||||||
|
|
||||||
|
self._error = None
|
||||||
|
|
||||||
|
except requests.RequestException as e:
|
||||||
|
error_msg = f"Ошибка сетевого подключения: {e}"
|
||||||
|
logging.error(error_msg, exc_info=True)
|
||||||
|
self._error = error_msg
|
||||||
|
except UpdateCheckError as e:
|
||||||
|
logging.error(str(e), exc_info=True)
|
||||||
|
self._error = str(e)
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Неизвестная ошибка при проверке обновлений: {e}"
|
||||||
|
logging.error(error_msg, exc_info=True)
|
||||||
|
self._error = error_msg
|
||||||
|
finally:
|
||||||
|
if callback:
|
||||||
|
callback(self._update_available, self._error)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def update_available(self):
|
||||||
|
"""Доступно ли обновление"""
|
||||||
|
return self._update_available
|
||||||
|
|
||||||
|
@property
|
||||||
|
def latest_version(self):
|
||||||
|
"""Последняя доступная версия"""
|
||||||
|
return self._latest_version
|
||||||
|
|
||||||
|
@property
|
||||||
|
def error(self):
|
||||||
|
"""Последняя ошибка при проверке обновлений"""
|
||||||
|
return self._error
|
||||||
|
|
||||||
|
@property
|
||||||
|
def changelog(self):
|
||||||
|
"""Текущий changelog"""
|
||||||
|
return self._changelog
|
||||||
|
|
||||||
|
def get_release_notes(self):
|
||||||
|
"""Получение информации о последнем релизе"""
|
||||||
|
if self._latest_release:
|
||||||
|
return {
|
||||||
|
"version": self._latest_version,
|
||||||
|
"description": self._latest_release.get("body", ""),
|
||||||
|
"download_url": self._latest_release.get("assets", [{}])[0].get("browser_download_url", "")
|
||||||
|
}
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_releases(self, callback=None):
|
||||||
|
"""
|
||||||
|
Получение списка релизов из репозитория.
|
||||||
|
:param callback: Функция обратного вызова, которая будет вызвана после получения списка релизов
|
||||||
|
"""
|
||||||
|
def fetch():
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{self.api_url}/releases", timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
releases = response.json()
|
||||||
|
|
||||||
|
if not releases:
|
||||||
|
raise UpdateCheckError("Не найдено релизов в репозитории")
|
||||||
|
|
||||||
|
self._error = None
|
||||||
|
if callback:
|
||||||
|
callback(releases, None)
|
||||||
|
|
||||||
|
except requests.RequestException as e:
|
||||||
|
error_msg = f"Ошибка сетевого подключения: {e}"
|
||||||
|
logging.error(error_msg, exc_info=True)
|
||||||
|
self._error = error_msg
|
||||||
|
if callback:
|
||||||
|
callback(None, error_msg)
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Ошибка при получении списка релизов: {e}"
|
||||||
|
logging.error(error_msg, exc_info=True)
|
||||||
|
self._error = error_msg
|
||||||
|
if callback:
|
||||||
|
callback(None, error_msg)
|
||||||
|
|
||||||
|
# Запускаем получение в отдельном потоке
|
||||||
|
threading.Thread(target=fetch, daemon=True).start()
|
||||||
Reference in New Issue
Block a user