Enhance TFTP server implementation with advanced monitoring and UI improvements
- Completely refactor TFTP server implementation with more robust file transfer handling - Add detailed transfer tracking with active transfers table - Implement periodic transfer status updates - Improve log and UI layout for TFTP server tab - Add more granular error handling and logging - Enhance threading and socket management for file transfers
This commit is contained in:
198
ComConfigCopy.py
198
ComConfigCopy.py
@@ -829,86 +829,202 @@ class SerialAppGUI(tk.Tk):
|
|||||||
about_window.grab_set()
|
about_window.grab_set()
|
||||||
|
|
||||||
def create_tftp_tab(self, frame):
|
def create_tftp_tab(self, frame):
|
||||||
|
"""Создание вкладки TFTP сервера."""
|
||||||
# Создаем фрейм для управления TFTP сервером
|
# Создаем фрейм для управления TFTP сервером
|
||||||
control_frame = ttk.Frame(frame)
|
tftp_frame = ttk.Frame(frame)
|
||||||
control_frame.pack(fill=X, pady=5)
|
tftp_frame.pack(fill=BOTH, expand=True, padx=5, pady=5)
|
||||||
|
|
||||||
|
# Создаем и размещаем элементы управления
|
||||||
|
controls_frame = ttk.LabelFrame(tftp_frame, text="Управление TFTP сервером")
|
||||||
|
controls_frame.pack(fill=X, padx=5, pady=5)
|
||||||
|
|
||||||
# IP адрес
|
# IP адрес
|
||||||
ip_frame = ttk.Frame(control_frame)
|
ip_frame = ttk.Frame(controls_frame)
|
||||||
ip_frame.pack(fill=X, pady=5)
|
ip_frame.pack(fill=X, padx=5, pady=2)
|
||||||
ttk.Label(ip_frame, text="IP адрес:").pack(side=LEFT, padx=5)
|
ttk.Label(ip_frame, text="IP адрес:").pack(side=LEFT, padx=5)
|
||||||
self.tftp_ip_var = StringVar(value="0.0.0.0")
|
self.tftp_ip_var = StringVar(value="0.0.0.0")
|
||||||
ttk.Entry(ip_frame, textvariable=self.tftp_ip_var, width=15).pack(side=LEFT, padx=5)
|
self.tftp_ip_entry = ttk.Entry(ip_frame, textvariable=self.tftp_ip_var)
|
||||||
|
self.tftp_ip_entry.pack(fill=X, expand=True, padx=5)
|
||||||
|
|
||||||
# Порт
|
# Порт
|
||||||
port_frame = ttk.Frame(control_frame)
|
port_frame = ttk.Frame(controls_frame)
|
||||||
port_frame.pack(fill=X, pady=5)
|
port_frame.pack(fill=X, padx=5, pady=2)
|
||||||
ttk.Label(port_frame, text="Порт:").pack(side=LEFT, padx=5)
|
ttk.Label(port_frame, text="Порт:").pack(side=LEFT, padx=5)
|
||||||
self.tftp_port_var = StringVar(value="69")
|
self.tftp_port_var = StringVar(value="69")
|
||||||
ttk.Entry(port_frame, textvariable=self.tftp_port_var, width=6).pack(side=LEFT, padx=5)
|
self.tftp_port_entry = ttk.Entry(port_frame, textvariable=self.tftp_port_var)
|
||||||
|
self.tftp_port_entry.pack(fill=X, expand=True, padx=5)
|
||||||
|
|
||||||
# Кнопки управления
|
# Кнопки управления
|
||||||
button_frame = ttk.Frame(control_frame)
|
buttons_frame = ttk.Frame(controls_frame)
|
||||||
button_frame.pack(fill=X, pady=5)
|
buttons_frame.pack(fill=X, padx=5, pady=5)
|
||||||
self.start_tftp_button = ttk.Button(button_frame, text="Запустить сервер", command=self.start_tftp_server)
|
|
||||||
self.start_tftp_button.pack(side=LEFT, padx=5)
|
|
||||||
self.stop_tftp_button = ttk.Button(button_frame, text="Остановить сервер", command=self.stop_tftp_server, state="disabled")
|
|
||||||
self.stop_tftp_button.pack(side=LEFT, padx=5)
|
|
||||||
|
|
||||||
# Лог TFTP сервера
|
self.start_tftp_button = ttk.Button(
|
||||||
log_frame = ttk.Frame(frame)
|
buttons_frame,
|
||||||
log_frame.pack(fill=BOTH, expand=True, pady=5)
|
text="Запустить сервер",
|
||||||
ttk.Label(log_frame, text="Лог сервера:").pack(anchor=W, padx=5)
|
command=self.start_tftp_server
|
||||||
self.tftp_log_text = tk.Text(log_frame, wrap="word", height=15)
|
)
|
||||||
|
self.start_tftp_button.pack(side=LEFT, padx=5)
|
||||||
|
|
||||||
|
self.stop_tftp_button = ttk.Button(
|
||||||
|
buttons_frame,
|
||||||
|
text="Остановить сервер",
|
||||||
|
command=self.stop_tftp_server,
|
||||||
|
state="disabled"
|
||||||
|
)
|
||||||
|
self.stop_tftp_button.pack(side=LEFT, padx=5)
|
||||||
|
|
||||||
|
# Лог сервера
|
||||||
|
log_frame = ttk.LabelFrame(tftp_frame, text="Лог сервера")
|
||||||
|
log_frame.pack(fill=BOTH, expand=True, padx=5, pady=5)
|
||||||
|
|
||||||
|
self.tftp_log_text = tk.Text(log_frame, wrap=tk.WORD, height=10)
|
||||||
self.tftp_log_text.pack(fill=BOTH, expand=True, padx=5, pady=5)
|
self.tftp_log_text.pack(fill=BOTH, expand=True, padx=5, pady=5)
|
||||||
|
|
||||||
|
# Добавляем скроллбар для лога
|
||||||
|
scrollbar = ttk.Scrollbar(self.tftp_log_text, command=self.tftp_log_text.yview)
|
||||||
|
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||||
|
self.tftp_log_text.config(yscrollcommand=scrollbar.set)
|
||||||
|
|
||||||
|
# Статус передач
|
||||||
|
transfers_frame = ttk.LabelFrame(tftp_frame, text="Активные передачи")
|
||||||
|
transfers_frame.pack(fill=BOTH, expand=True, padx=5, pady=5)
|
||||||
|
|
||||||
|
# Создаем таблицу для отображения активных передач
|
||||||
|
columns = ("client", "filename", "progress", "remaining", "time")
|
||||||
|
self.transfers_tree = ttk.Treeview(transfers_frame, columns=columns, show="headings")
|
||||||
|
|
||||||
|
# Настраиваем заголовки колонок
|
||||||
|
self.transfers_tree.heading("client", text="Клиент")
|
||||||
|
self.transfers_tree.heading("filename", text="Файл")
|
||||||
|
self.transfers_tree.heading("progress", text="Прогресс")
|
||||||
|
self.transfers_tree.heading("remaining", text="Осталось")
|
||||||
|
self.transfers_tree.heading("time", text="Время")
|
||||||
|
|
||||||
|
# Настраиваем ширину колонок
|
||||||
|
self.transfers_tree.column("client", width=120)
|
||||||
|
self.transfers_tree.column("filename", width=150)
|
||||||
|
self.transfers_tree.column("progress", width=100)
|
||||||
|
self.transfers_tree.column("remaining", width=100)
|
||||||
|
self.transfers_tree.column("time", width=80)
|
||||||
|
|
||||||
|
self.transfers_tree.pack(fill=BOTH, expand=True, padx=5, pady=5)
|
||||||
|
|
||||||
|
# Инициализация TFTP сервера
|
||||||
|
self.tftp_server = None
|
||||||
|
self.tftp_server_thread = None
|
||||||
|
|
||||||
def start_tftp_server(self):
|
def start_tftp_server(self):
|
||||||
|
"""Запуск TFTP сервера."""
|
||||||
try:
|
try:
|
||||||
ip = self.tftp_ip_var.get()
|
ip = self.tftp_ip_var.get()
|
||||||
port = int(self.tftp_port_var.get())
|
port = int(self.tftp_port_var.get())
|
||||||
|
|
||||||
if not self.tftp_server:
|
# Создаем экземпляр TFTP сервера
|
||||||
self.tftp_server = TFTPServer("Firmware")
|
self.tftp_server = TFTPServer("Firmware")
|
||||||
|
|
||||||
# Устанавливаем функцию обратного вызова для логирования
|
# Устанавливаем callback для логирования
|
||||||
def log_callback(message):
|
def log_callback(message):
|
||||||
self.tftp_log_text.insert(END, f"{message}\n")
|
self.append_tftp_log(message)
|
||||||
self.tftp_log_text.see(END)
|
# Обновляем информацию о передачах
|
||||||
|
self.update_transfers_info()
|
||||||
|
|
||||||
self.tftp_server.set_log_callback(log_callback)
|
self.tftp_server.set_log_callback(log_callback)
|
||||||
|
|
||||||
threading.Thread(
|
# Запускаем сервер в отдельном потоке
|
||||||
|
self.tftp_server_thread = threading.Thread(
|
||||||
target=self.run_tftp_server,
|
target=self.run_tftp_server,
|
||||||
args=(ip, port),
|
args=(ip, port),
|
||||||
daemon=True
|
daemon=True
|
||||||
).start()
|
)
|
||||||
|
self.tftp_server_thread.start()
|
||||||
|
|
||||||
|
# Обновляем состояние кнопок
|
||||||
self.start_tftp_button.config(state="disabled")
|
self.start_tftp_button.config(state="disabled")
|
||||||
self.stop_tftp_button.config(state="normal")
|
self.stop_tftp_button.config(state="normal")
|
||||||
|
self.tftp_ip_entry.config(state="disabled")
|
||||||
|
self.tftp_port_entry.config(state="disabled")
|
||||||
|
|
||||||
|
self.append_tftp_log(f"[INFO] TFTP сервер запущен на {ip}:{port}")
|
||||||
|
|
||||||
|
# Запускаем периодическое обновление информации о передачах
|
||||||
|
self.update_transfers_periodically()
|
||||||
|
|
||||||
except ValueError:
|
|
||||||
messagebox.showerror("Ошибка", "Некорректный порт")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
self.append_tftp_log(f"[ERROR] Ошибка запуска сервера: {str(e)}")
|
||||||
messagebox.showerror("Ошибка", f"Не удалось запустить TFTP сервер: {str(e)}")
|
messagebox.showerror("Ошибка", f"Не удалось запустить TFTP сервер: {str(e)}")
|
||||||
|
|
||||||
def run_tftp_server(self, ip, port):
|
def run_tftp_server(self, ip, port):
|
||||||
|
"""Запуск TFTP сервера в отдельном потоке."""
|
||||||
try:
|
try:
|
||||||
self.tftp_server.start_server(ip, port)
|
self.tftp_server.start_server(ip, port)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.tftp_log_text.insert(END, f"[ERROR] Ошибка TFTP сервера: {str(e)}\n")
|
self.append_tftp_log(f"[ERROR] Ошибка работы сервера: {str(e)}")
|
||||||
self.stop_tftp_server()
|
|
||||||
|
|
||||||
def stop_tftp_server(self):
|
def stop_tftp_server(self):
|
||||||
|
"""Остановка TFTP сервера."""
|
||||||
if self.tftp_server:
|
if self.tftp_server:
|
||||||
try:
|
try:
|
||||||
self.tftp_server.stop_server()
|
self.tftp_server.stop_server()
|
||||||
self.tftp_server = None
|
if self.tftp_server_thread:
|
||||||
self.tftp_log_text.insert(END, "[INFO] TFTP сервер остановлен\n")
|
self.tftp_server_thread.join(timeout=2.0)
|
||||||
except Exception as e:
|
|
||||||
self.tftp_log_text.insert(END, f"[ERROR] Ошибка при остановке TFTP сервера: {str(e)}\n")
|
# Обновляем состояние кнопок
|
||||||
finally:
|
|
||||||
self.start_tftp_button.config(state="normal")
|
self.start_tftp_button.config(state="normal")
|
||||||
self.stop_tftp_button.config(state="disabled")
|
self.stop_tftp_button.config(state="disabled")
|
||||||
|
self.tftp_ip_entry.config(state="normal")
|
||||||
|
self.tftp_port_entry.config(state="normal")
|
||||||
|
|
||||||
|
self.append_tftp_log("[INFO] TFTP сервер остановлен")
|
||||||
|
|
||||||
|
# Очищаем таблицу передач
|
||||||
|
for item in self.transfers_tree.get_children():
|
||||||
|
self.transfers_tree.delete(item)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.append_tftp_log(f"[ERROR] Ошибка остановки сервера: {str(e)}")
|
||||||
|
messagebox.showerror("Ошибка", f"Не удалось остановить TFTP сервер: {str(e)}")
|
||||||
|
|
||||||
|
def append_tftp_log(self, message):
|
||||||
|
"""Добавление сообщения в лог TFTP сервера."""
|
||||||
|
self.tftp_log_text.insert(END, message + "\n")
|
||||||
|
self.tftp_log_text.see(END)
|
||||||
|
|
||||||
|
def update_transfers_info(self):
|
||||||
|
"""Обновление информации об активных передачах."""
|
||||||
|
if not self.tftp_server:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Очищаем текущие записи
|
||||||
|
for item in self.transfers_tree.get_children():
|
||||||
|
self.transfers_tree.delete(item)
|
||||||
|
|
||||||
|
# Добавляем информацию о текущих передачах
|
||||||
|
for client_addr, transfer_info in self.tftp_server.active_transfers.items():
|
||||||
|
filename = transfer_info['filename']
|
||||||
|
bytes_sent = transfer_info['bytes_sent']
|
||||||
|
filesize = transfer_info['filesize']
|
||||||
|
start_time = transfer_info['start_time']
|
||||||
|
|
||||||
|
# Вычисляем прогресс
|
||||||
|
progress = f"{bytes_sent}/{filesize} байт"
|
||||||
|
remaining = filesize - bytes_sent
|
||||||
|
elapsed_time = time.time() - start_time
|
||||||
|
|
||||||
|
# Добавляем запись в таблицу
|
||||||
|
self.transfers_tree.insert("", END, values=(
|
||||||
|
f"{client_addr[0]}:{client_addr[1]}",
|
||||||
|
filename,
|
||||||
|
progress,
|
||||||
|
f"{remaining} байт",
|
||||||
|
f"{elapsed_time:.1f}с"
|
||||||
|
))
|
||||||
|
|
||||||
|
def update_transfers_periodically(self):
|
||||||
|
"""Периодическое обновление информации о передачах."""
|
||||||
|
if self.tftp_server and self.tftp_server.running:
|
||||||
|
self.update_transfers_info()
|
||||||
|
# Планируем следующее обновление через 1 секунду
|
||||||
|
self.after(1000, self.update_transfers_periodically)
|
||||||
|
|
||||||
# ==========================
|
# ==========================
|
||||||
# Парсер аргументов (не используется)
|
# Парсер аргументов (не используется)
|
||||||
|
|||||||
421
TFTPServer.py
421
TFTPServer.py
@@ -1,178 +1,277 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
TFTP сервер для передачи прошивки с компьютера на коммутатор.
|
||||||
|
|
||||||
|
- Создает сервер по заданному IP и порту.
|
||||||
|
- Расшаривает папку Firmware.
|
||||||
|
- Показывает текущее состояние сервера и статус передачи файла:
|
||||||
|
- кому (IP устройства),
|
||||||
|
- сколько осталось байт,
|
||||||
|
- сколько передано байт,
|
||||||
|
- время передачи.
|
||||||
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import tftpy
|
import socket
|
||||||
import logging
|
import struct
|
||||||
from typing import Optional, Callable, Dict
|
import threading
|
||||||
from dataclasses import dataclass
|
import time
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class FileTransfer:
|
|
||||||
filename: str
|
|
||||||
client_address: tuple
|
|
||||||
start_time: datetime
|
|
||||||
total_blocks: int = 0
|
|
||||||
current_block: int = 0
|
|
||||||
status: str = "в процессе"
|
|
||||||
|
|
||||||
class TFTPServer:
|
class TFTPServer:
|
||||||
def __init__(self, root_path: str):
|
def __init__(self, share_folder):
|
||||||
"""
|
"""
|
||||||
Инициализация TFTP сервера
|
Инициализация TFTP сервера.
|
||||||
|
|
||||||
Args:
|
|
||||||
root_path (str): Путь к корневой директории для файлов
|
|
||||||
"""
|
|
||||||
self.root_path = root_path
|
|
||||||
self.server: Optional[tftpy.TftpServer] = None
|
|
||||||
self.logger = logging.getLogger(__name__)
|
|
||||||
self.log_callback: Optional[Callable[[str], None]] = None
|
|
||||||
self.progress_callback: Optional[Callable[[str, tuple, int, int, str], None]] = None
|
|
||||||
self.active_transfers: Dict[str, FileTransfer] = {}
|
|
||||||
|
|
||||||
def set_log_callback(self, callback: Callable[[str], None]):
|
: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.active_transfers = {}
|
||||||
|
|
||||||
|
def set_log_callback(self, callback):
|
||||||
|
"""
|
||||||
|
Установка функции обратного вызова для логирования сообщений.
|
||||||
|
|
||||||
|
:param callback: Функция, принимающая строку сообщения.
|
||||||
|
"""
|
||||||
self.log_callback = callback
|
self.log_callback = callback
|
||||||
|
|
||||||
def set_progress_callback(self, callback: Callable[[str, tuple, int, int, str], None]):
|
def log(self, message):
|
||||||
"""Установка функции обратного вызова для отображения прогресса"""
|
|
||||||
self.progress_callback = callback
|
|
||||||
|
|
||||||
def log_message(self, message: str, level: str = "INFO"):
|
|
||||||
"""Логирование сообщения"""
|
|
||||||
if self.log_callback:
|
|
||||||
self.log_callback(f"[{level}] {message}")
|
|
||||||
if level == "INFO":
|
|
||||||
self.logger.info(message)
|
|
||||||
elif level == "ERROR":
|
|
||||||
self.logger.error(message)
|
|
||||||
elif level == "WARNING":
|
|
||||||
self.logger.warning(message)
|
|
||||||
|
|
||||||
def update_progress(self, transfer: FileTransfer):
|
|
||||||
"""Обновление прогресса передачи файла"""
|
|
||||||
if self.progress_callback:
|
|
||||||
self.progress_callback(
|
|
||||||
transfer.filename,
|
|
||||||
transfer.client_address,
|
|
||||||
transfer.current_block,
|
|
||||||
transfer.total_blocks,
|
|
||||||
transfer.status
|
|
||||||
)
|
|
||||||
|
|
||||||
def start_server(self, ip: str = "0.0.0.0", port: int = 69):
|
|
||||||
"""
|
"""
|
||||||
Запуск TFTP сервера
|
Функция логирования: вызывает callback (если задан) или выводит сообщение в консоль.
|
||||||
|
|
||||||
Args:
|
:param message: Строка с сообщением для логирования.
|
||||||
ip (str): IP адрес для прослушивания (по умолчанию "0.0.0.0")
|
|
||||||
port (int): Порт для прослушивания (по умолчанию 69)
|
|
||||||
"""
|
"""
|
||||||
|
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 сервера.
|
||||||
|
"""
|
||||||
|
self.running = True
|
||||||
|
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
self.server_socket.bind((ip, port))
|
||||||
|
self.log(f"[INFO] TFTP сервер запущен на {ip}:{port}")
|
||||||
try:
|
try:
|
||||||
if not os.path.exists(self.root_path):
|
while self.running:
|
||||||
os.makedirs(self.root_path)
|
try:
|
||||||
|
# Устанавливаем таймаут для возможности периодической проверки состояния сервера.
|
||||||
# Создаем серверный класс с обработчиками событий
|
self.server_socket.settimeout(1.0)
|
||||||
server = tftpy.TftpServer(self.root_path)
|
data, client_addr = self.server_socket.recvfrom(2048)
|
||||||
|
if data:
|
||||||
# Добавляем обработчики событий
|
# Каждая обработка запроса выполняется в отдельном потоке.
|
||||||
def on_read_request(filename: str, client_address: tuple):
|
threading.Thread(target=self.handle_request, args=(data, client_addr), daemon=True).start()
|
||||||
self.log_message(f"Получен запрос на скачивание файла '{filename}' от {client_address[0]}:{client_address[1]}")
|
except socket.timeout:
|
||||||
file_path = os.path.join(self.root_path, filename)
|
continue
|
||||||
if not os.path.exists(file_path):
|
except Exception as e:
|
||||||
self.log_message(f"Файл '{filename}' не найден", "ERROR")
|
self.log(f"[ERROR] Ошибка получения данных: {str(e)}")
|
||||||
return False
|
finally:
|
||||||
|
self.server_socket.close()
|
||||||
# Создаем запись о передаче файла
|
self.log("[INFO] TFTP сервер остановлен.")
|
||||||
file_size = os.path.getsize(file_path)
|
|
||||||
total_blocks = (file_size + 511) // 512 # Размер блока TFTP = 512 байт
|
|
||||||
transfer = FileTransfer(
|
|
||||||
filename=filename,
|
|
||||||
client_address=client_address,
|
|
||||||
start_time=datetime.now(),
|
|
||||||
total_blocks=total_blocks
|
|
||||||
)
|
|
||||||
self.active_transfers[f"{filename}_{client_address}"] = transfer
|
|
||||||
self.update_progress(transfer)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def on_write_request(filename: str, client_address: tuple):
|
|
||||||
self.log_message(f"Получен запрос на загрузку файла '{filename}' от {client_address[0]}:{client_address[1]}")
|
|
||||||
transfer = FileTransfer(
|
|
||||||
filename=filename,
|
|
||||||
client_address=client_address,
|
|
||||||
start_time=datetime.now()
|
|
||||||
)
|
|
||||||
self.active_transfers[f"{filename}_{client_address}"] = transfer
|
|
||||||
self.update_progress(transfer)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def on_read_block_sent(filename: str, block_number: int, client_address: tuple):
|
|
||||||
key = f"{filename}_{client_address}"
|
|
||||||
if key in self.active_transfers:
|
|
||||||
transfer = self.active_transfers[key]
|
|
||||||
transfer.current_block = block_number
|
|
||||||
if transfer.current_block >= transfer.total_blocks:
|
|
||||||
transfer.status = "завершено"
|
|
||||||
del self.active_transfers[key]
|
|
||||||
self.update_progress(transfer)
|
|
||||||
self.log_message(f"Отправлен блок {block_number} файла '{filename}' клиенту {client_address[0]}:{client_address[1]}")
|
|
||||||
|
|
||||||
def on_write_block_received(filename: str, block_number: int, client_address: tuple):
|
|
||||||
key = f"{filename}_{client_address}"
|
|
||||||
if key in self.active_transfers:
|
|
||||||
transfer = self.active_transfers[key]
|
|
||||||
transfer.current_block = block_number
|
|
||||||
if block_number == 1: # Первый блок
|
|
||||||
file_path = os.path.join(self.root_path, filename)
|
|
||||||
if os.path.exists(file_path):
|
|
||||||
transfer.total_blocks = (os.path.getsize(file_path) + 511) // 512
|
|
||||||
if transfer.current_block >= transfer.total_blocks:
|
|
||||||
transfer.status = "завершено"
|
|
||||||
del self.active_transfers[key]
|
|
||||||
self.update_progress(transfer)
|
|
||||||
self.log_message(f"Получен блок {block_number} файла '{filename}' от клиента {client_address[0]}:{client_address[1]}")
|
|
||||||
|
|
||||||
def on_error(error: Exception, client_address: tuple):
|
|
||||||
# Помечаем все активные передачи для этого клиента как ошибочные
|
|
||||||
for key, transfer in list(self.active_transfers.items()):
|
|
||||||
if transfer.client_address == client_address:
|
|
||||||
transfer.status = f"ошибка: {str(error)}"
|
|
||||||
self.update_progress(transfer)
|
|
||||||
del self.active_transfers[key]
|
|
||||||
self.log_message(f"Ошибка при обработке запроса от {client_address[0]}:{client_address[1]}: {str(error)}", "ERROR")
|
|
||||||
|
|
||||||
# Устанавливаем обработчики
|
|
||||||
server.on_read_request = on_read_request
|
|
||||||
server.on_write_request = on_write_request
|
|
||||||
server.on_read_block_sent = on_read_block_sent
|
|
||||||
server.on_write_block_received = on_write_block_received
|
|
||||||
server.on_error = on_error
|
|
||||||
|
|
||||||
self.server = server
|
|
||||||
self.log_message(f"Запуск TFTP сервера на {ip}:{port}")
|
|
||||||
self.server.listen(ip, port)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error_msg = f"Ошибка при запуске TFTP сервера: {str(e)}"
|
|
||||||
self.log_message(error_msg, "ERROR")
|
|
||||||
raise
|
|
||||||
|
|
||||||
def stop_server(self):
|
def stop_server(self):
|
||||||
"""Остановка TFTP сервера"""
|
"""
|
||||||
if self.server:
|
Остановка TFTP сервера.
|
||||||
|
"""
|
||||||
|
self.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.
|
||||||
|
|
||||||
|
В процессе передачи обновляется состояние активной передачи, которое логируется с информацией:
|
||||||
|
- адрес клиента,
|
||||||
|
- имя файла,
|
||||||
|
- размер файла,
|
||||||
|
- количество переданных байт,
|
||||||
|
- оставшиеся байты,
|
||||||
|
- время передачи.
|
||||||
|
|
||||||
|
:param file_path: Полный путь к файлу для передачи.
|
||||||
|
:param client_addr: Адрес клиента.
|
||||||
|
"""
|
||||||
|
BLOCK_SIZE = 512
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
self.log(f"[INFO] Начало передачи файла '{file_basename}' клиенту {client_addr}. Размер файла: {filesize} байт.")
|
||||||
|
|
||||||
|
# Создаем отдельный сокет для передачи файла
|
||||||
|
transfer_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
transfer_socket.settimeout(5.0) # Таймаут ожидания ACK
|
||||||
|
|
||||||
|
block_num = 1
|
||||||
|
bytes_sent = 0
|
||||||
|
last_progress_time = time.time()
|
||||||
|
|
||||||
|
with open(file_path, "rb") as f:
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
data_block = f.read(BLOCK_SIZE)
|
||||||
|
if not data_block: # Достигнут конец файла
|
||||||
|
break
|
||||||
|
|
||||||
|
# Формируем TFTP пакет данных
|
||||||
|
packet = struct.pack("!HH", 3, block_num) + data_block
|
||||||
|
attempts = 0
|
||||||
|
ack_received = False
|
||||||
|
|
||||||
|
# Попытка отправки текущего блока (до 3 повторных попыток)
|
||||||
|
while attempts < 3 and not ack_received:
|
||||||
|
try:
|
||||||
|
transfer_socket.sendto(packet, client_addr)
|
||||||
|
|
||||||
|
# Логируем прогресс каждую секунду
|
||||||
|
current_time = time.time()
|
||||||
|
if current_time - last_progress_time >= 1.0:
|
||||||
|
elapsed_time = current_time - start_time
|
||||||
|
remaining = filesize - bytes_sent
|
||||||
|
self.log(f"[STATUS] Клиент: {client_addr} | Файл: {file_basename} | "
|
||||||
|
f"Отправлено: {bytes_sent}/{filesize} байт | "
|
||||||
|
f"Осталось: {remaining} байт | "
|
||||||
|
f"Время: {elapsed_time:.2f} сек.")
|
||||||
|
last_progress_time = current_time
|
||||||
|
|
||||||
|
# Ожидаем подтверждение
|
||||||
|
ack_data, addr = transfer_socket.recvfrom(4) # Ожидаем только 4 байта для ACK
|
||||||
|
if addr == client_addr:
|
||||||
|
ack_opcode, ack_block = struct.unpack("!HH", ack_data)
|
||||||
|
if ack_opcode == 4 and ack_block == block_num:
|
||||||
|
ack_received = True
|
||||||
|
bytes_sent += len(data_block)
|
||||||
|
with self.lock:
|
||||||
|
if client_addr in self.active_transfers:
|
||||||
|
self.active_transfers[client_addr]['bytes_sent'] = bytes_sent
|
||||||
|
else:
|
||||||
|
self.log(f"[WARN] Неверный ACK от {client_addr}. "
|
||||||
|
f"Ожидался блок {block_num}, получен {ack_block}.")
|
||||||
|
except socket.timeout:
|
||||||
|
attempts += 1
|
||||||
|
self.log(f"[WARN] Таймаут ожидания ACK для блока {block_num} "
|
||||||
|
f"от {client_addr}. Попытка {attempts+1}.")
|
||||||
|
except Exception as e:
|
||||||
|
self.log(f"[ERROR] Ошибка при отправке блока {block_num}: {str(e)}")
|
||||||
|
attempts += 1
|
||||||
|
|
||||||
|
if not ack_received:
|
||||||
|
raise Exception(f"Не удалось получить подтверждение для блока {block_num}")
|
||||||
|
|
||||||
|
block_num = (block_num + 1) % 65536
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.log(f"[ERROR] Ошибка при обработке блока {block_num}: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
elapsed_time = time.time() - start_time
|
||||||
|
self.log(f"[INFO] Передача файла '{file_basename}' клиенту {client_addr} "
|
||||||
|
f"завершена за {elapsed_time:.2f} сек. Всего отправлено {bytes_sent} байт.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.log(f"[ERROR] Ошибка при передаче файла '{os.path.basename(file_path)}' "
|
||||||
|
f"клиенту {client_addr}: {str(e)}")
|
||||||
try:
|
try:
|
||||||
# Помечаем все активные передачи как прерванные
|
self.send_error(client_addr, 0, str(e))
|
||||||
for transfer in self.active_transfers.values():
|
except:
|
||||||
transfer.status = "прервано"
|
pass
|
||||||
self.update_progress(transfer)
|
finally:
|
||||||
self.active_transfers.clear()
|
if transfer_socket:
|
||||||
|
try:
|
||||||
self.server.stop()
|
transfer_socket.close()
|
||||||
self.log_message("TFTP сервер остановлен")
|
except:
|
||||||
except Exception as e:
|
pass
|
||||||
error_msg = f"Ошибка при остановке TFTP сервера: {str(e)}"
|
with self.lock:
|
||||||
self.log_message(error_msg, "ERROR")
|
if client_addr in self.active_transfers:
|
||||||
raise
|
del self.active_transfers[client_addr]
|
||||||
Reference in New Issue
Block a user