diff --git a/ComConfigCopy.py b/ComConfigCopy.py index 7665c8b..ab5d9cc 100644 --- a/ComConfigCopy.py +++ b/ComConfigCopy.py @@ -829,86 +829,202 @@ class SerialAppGUI(tk.Tk): about_window.grab_set() def create_tftp_tab(self, frame): + """Создание вкладки TFTP сервера.""" # Создаем фрейм для управления TFTP сервером - control_frame = ttk.Frame(frame) - control_frame.pack(fill=X, pady=5) - + tftp_frame = ttk.Frame(frame) + 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_frame = ttk.Frame(control_frame) - ip_frame.pack(fill=X, pady=5) + ip_frame = ttk.Frame(controls_frame) + ip_frame.pack(fill=X, padx=5, pady=2) ttk.Label(ip_frame, text="IP адрес:").pack(side=LEFT, padx=5) 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.pack(fill=X, pady=5) + port_frame = ttk.Frame(controls_frame) + port_frame.pack(fill=X, padx=5, pady=2) ttk.Label(port_frame, text="Порт:").pack(side=LEFT, padx=5) 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) - button_frame.pack(fill=X, 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) + buttons_frame = ttk.Frame(controls_frame) + buttons_frame.pack(fill=X, padx=5, pady=5) - # Лог TFTP сервера - log_frame = ttk.Frame(frame) - log_frame.pack(fill=BOTH, expand=True, pady=5) - ttk.Label(log_frame, text="Лог сервера:").pack(anchor=W, padx=5) - self.tftp_log_text = tk.Text(log_frame, wrap="word", height=15) + self.start_tftp_button = ttk.Button( + buttons_frame, + text="Запустить сервер", + command=self.start_tftp_server + ) + 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) + # Добавляем скроллбар для лога + 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): + """Запуск TFTP сервера.""" try: ip = self.tftp_ip_var.get() port = int(self.tftp_port_var.get()) - if not self.tftp_server: - self.tftp_server = TFTPServer("Firmware") - - # Устанавливаем функцию обратного вызова для логирования + # Создаем экземпляр TFTP сервера + self.tftp_server = TFTPServer("Firmware") + + # Устанавливаем callback для логирования def log_callback(message): - self.tftp_log_text.insert(END, f"{message}\n") - self.tftp_log_text.see(END) + self.append_tftp_log(message) + # Обновляем информацию о передачах + self.update_transfers_info() self.tftp_server.set_log_callback(log_callback) - threading.Thread( + # Запускаем сервер в отдельном потоке + self.tftp_server_thread = threading.Thread( target=self.run_tftp_server, args=(ip, port), daemon=True - ).start() + ) + self.tftp_server_thread.start() + # Обновляем состояние кнопок self.start_tftp_button.config(state="disabled") 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: + self.append_tftp_log(f"[ERROR] Ошибка запуска сервера: {str(e)}") messagebox.showerror("Ошибка", f"Не удалось запустить TFTP сервер: {str(e)}") - + def run_tftp_server(self, ip, port): + """Запуск TFTP сервера в отдельном потоке.""" try: self.tftp_server.start_server(ip, port) except Exception as e: - self.tftp_log_text.insert(END, f"[ERROR] Ошибка TFTP сервера: {str(e)}\n") - self.stop_tftp_server() - + self.append_tftp_log(f"[ERROR] Ошибка работы сервера: {str(e)}") + def stop_tftp_server(self): + """Остановка TFTP сервера.""" if self.tftp_server: try: self.tftp_server.stop_server() - self.tftp_server = None - self.tftp_log_text.insert(END, "[INFO] TFTP сервер остановлен\n") - except Exception as e: - self.tftp_log_text.insert(END, f"[ERROR] Ошибка при остановке TFTP сервера: {str(e)}\n") - finally: + if self.tftp_server_thread: + self.tftp_server_thread.join(timeout=2.0) + + # Обновляем состояние кнопок self.start_tftp_button.config(state="normal") 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) # ========================== # Парсер аргументов (не используется) diff --git a/TFTPServer.py b/TFTPServer.py index 434541d..48dace0 100644 --- a/TFTPServer.py +++ b/TFTPServer.py @@ -1,178 +1,277 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +""" +TFTP сервер для передачи прошивки с компьютера на коммутатор. + +- Создает сервер по заданному IP и порту. +- Расшаривает папку Firmware. +- Показывает текущее состояние сервера и статус передачи файла: + - кому (IP устройства), + - сколько осталось байт, + - сколько передано байт, + - время передачи. +""" import os -import tftpy -import logging -from typing import Optional, Callable, Dict -from dataclasses import dataclass -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 = "в процессе" +import socket +import struct +import threading +import time class TFTPServer: - def __init__(self, root_path: str): + def __init__(self, share_folder): """ - Инициализация 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] = {} + Инициализация TFTP сервера. - 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 - def set_progress_callback(self, callback: Callable[[str, tuple, int, int, str], None]): - """Установка функции обратного вызова для отображения прогресса""" - 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): + def log(self, message): """ - Запуск TFTP сервера + Функция логирования: вызывает callback (если задан) или выводит сообщение в консоль. - Args: - ip (str): IP адрес для прослушивания (по умолчанию "0.0.0.0") - port (int): Порт для прослушивания (по умолчанию 69) + :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 сервера. + """ + 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: - if not os.path.exists(self.root_path): - os.makedirs(self.root_path) - - # Создаем серверный класс с обработчиками событий - server = tftpy.TftpServer(self.root_path) - - # Добавляем обработчики событий - def on_read_request(filename: str, client_address: tuple): - self.log_message(f"Получен запрос на скачивание файла '{filename}' от {client_address[0]}:{client_address[1]}") - file_path = os.path.join(self.root_path, filename) - if not os.path.exists(file_path): - self.log_message(f"Файл '{filename}' не найден", "ERROR") - return False - - # Создаем запись о передаче файла - 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 + while self.running: + try: + # Устанавливаем таймаут для возможности периодической проверки состояния сервера. + self.server_socket.settimeout(1.0) + data, client_addr = self.server_socket.recvfrom(2048) + if data: + # Каждая обработка запроса выполняется в отдельном потоке. + threading.Thread(target=self.handle_request, args=(data, client_addr), daemon=True).start() + except socket.timeout: + continue + except Exception as e: + self.log(f"[ERROR] Ошибка получения данных: {str(e)}") + finally: + self.server_socket.close() + self.log("[INFO] TFTP сервер остановлен.") 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: - # Помечаем все активные передачи как прерванные - for transfer in self.active_transfers.values(): - transfer.status = "прервано" - self.update_progress(transfer) - self.active_transfers.clear() - - self.server.stop() - self.log_message("TFTP сервер остановлен") - except Exception as e: - error_msg = f"Ошибка при остановке TFTP сервера: {str(e)}" - self.log_message(error_msg, "ERROR") - raise \ No newline at end of file + self.send_error(client_addr, 0, str(e)) + except: + pass + finally: + if transfer_socket: + try: + transfer_socket.close() + except: + pass + with self.lock: + if client_addr in self.active_transfers: + del self.active_transfers[client_addr] \ No newline at end of file