From 299ce329f7d450e7baf767b53e5ee0fac38f97dc Mon Sep 17 00:00:00 2001 From: Lowa Date: Sun, 16 Feb 2025 02:51:47 +0300 Subject: [PATCH 1/6] Add TFTP server functionality to the application - Implement TFTP server tab with IP and port configuration - Create methods to start and stop TFTP server - Add logging functionality for TFTP server events - Integrate TFTPServer class into the main application - Re-enable Firmware directory creation --- ComConfigCopy.py | 89 +++++++++++++++++++++++- TFTPServer.py | 178 +++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 1 + 3 files changed, 267 insertions(+), 1 deletion(-) create mode 100644 TFTPServer.py create mode 100644 requirements.txt diff --git a/ComConfigCopy.py b/ComConfigCopy.py index 467c80c..7665c8b 100644 --- a/ComConfigCopy.py +++ b/ComConfigCopy.py @@ -37,13 +37,14 @@ import serial import serial.tools.list_ports from serial.serialutil import SerialException from about_window import AboutWindow +from TFTPServer import TFTPServer # from TFTPServer import TFTPServerThread # Создаем необходимые папки os.makedirs("Logs", exist_ok=True) os.makedirs("Configs", exist_ok=True) os.makedirs("Settings", exist_ok=True) -# os.makedirs("Firmware", exist_ok=True) +os.makedirs("Firmware", exist_ok=True) # Файл настроек находится в папке Settings SETTINGS_FILE = os.path.join("Settings", "settings.json") @@ -535,6 +536,7 @@ class SerialAppGUI(tk.Tk): self.option_add("*Font", default_font) self.settings = settings self.connection = None + self.tftp_server = None # Глобальные биндинги self.bind_class("Text", "", lambda event: event.widget.event_generate("<>")) @@ -613,14 +615,17 @@ class SerialAppGUI(tk.Tk): interactive_frame = ttk.Frame(self.notebook) file_exec_frame = ttk.Frame(self.notebook) config_editor_frame = ttk.Frame(self.notebook) + tftp_frame = ttk.Frame(self.notebook) self.notebook.add(interactive_frame, text="Интерактивный режим") self.notebook.add(file_exec_frame, text="Выполнение файла") self.notebook.add(config_editor_frame, text="Редактор конфигурации") + self.notebook.add(tftp_frame, text="TFTP Сервер") self.create_interactive_tab(interactive_frame) self.create_file_exec_tab(file_exec_frame) self.create_config_editor_tab(config_editor_frame) + self.create_tftp_tab(tftp_frame) # -------------- Вкладка "Интерактивный режим" -------------- def create_interactive_tab(self, frame): @@ -823,6 +828,88 @@ class SerialAppGUI(tk.Tk): about_window.transient(self) about_window.grab_set() + def create_tftp_tab(self, frame): + # Создаем фрейм для управления TFTP сервером + control_frame = ttk.Frame(frame) + control_frame.pack(fill=X, pady=5) + + # IP адрес + ip_frame = ttk.Frame(control_frame) + ip_frame.pack(fill=X, pady=5) + 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) + + # Порт + port_frame = ttk.Frame(control_frame) + port_frame.pack(fill=X, pady=5) + 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) + + # Кнопки управления + 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) + + # Лог 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.tftp_log_text.pack(fill=BOTH, expand=True, padx=5, pady=5) + + def start_tftp_server(self): + try: + ip = self.tftp_ip_var.get() + port = int(self.tftp_port_var.get()) + + if not self.tftp_server: + self.tftp_server = TFTPServer("Firmware") + + # Устанавливаем функцию обратного вызова для логирования + def log_callback(message): + self.tftp_log_text.insert(END, f"{message}\n") + self.tftp_log_text.see(END) + + self.tftp_server.set_log_callback(log_callback) + + threading.Thread( + target=self.run_tftp_server, + args=(ip, port), + daemon=True + ).start() + + self.start_tftp_button.config(state="disabled") + self.stop_tftp_button.config(state="normal") + + except ValueError: + messagebox.showerror("Ошибка", "Некорректный порт") + except Exception as e: + messagebox.showerror("Ошибка", f"Не удалось запустить TFTP сервер: {str(e)}") + + def run_tftp_server(self, ip, port): + 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() + + def stop_tftp_server(self): + 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: + self.start_tftp_button.config(state="normal") + self.stop_tftp_button.config(state="disabled") + # ========================== # Парсер аргументов (не используется) # ========================== diff --git a/TFTPServer.py b/TFTPServer.py new file mode 100644 index 0000000..434541d --- /dev/null +++ b/TFTPServer.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +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 = "в процессе" + +class TFTPServer: + def __init__(self, root_path: str): + """ + Инициализация 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]): + """Установка функции обратного вызова для логирования""" + 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): + """ + Запуск TFTP сервера + + Args: + ip (str): IP адрес для прослушивания (по умолчанию "0.0.0.0") + port (int): Порт для прослушивания (по умолчанию 69) + """ + 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 + + def stop_server(self): + """Остановка TFTP сервера""" + if self.server: + 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 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d11b739 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +tftpy>=0.8.0 \ No newline at end of file -- 2.49.1 From c95915483f3133fe28d5475c2b44ebb53c9c0d36 Mon Sep 17 00:00:00 2001 From: Lowa Date: Sun, 16 Feb 2025 03:09:23 +0300 Subject: [PATCH 2/6] Update .gitignore to include virtual environment directory - Add '.venv/' to .gitignore to exclude Python virtual environment files --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4df8abb..059c901 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ test.py __pycache__/ Firmware/1.jpg Firmware/2 -output/ \ No newline at end of file +output/ +.venv/ \ No newline at end of file -- 2.49.1 From f1ca31c19846754daf6af94c9bb020877a72f001 Mon Sep 17 00:00:00 2001 From: Lowa Date: Sun, 16 Feb 2025 03:19:44 +0300 Subject: [PATCH 3/6] 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 --- ComConfigCopy.py | 198 +++++++++++++++++----- TFTPServer.py | 421 +++++++++++++++++++++++++++++------------------ 2 files changed, 417 insertions(+), 202 deletions(-) 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 -- 2.49.1 From 3126811f09ed198a98589d3487afd5473bb69e42 Mon Sep 17 00:00:00 2001 From: Lowa Date: Sun, 16 Feb 2025 03:28:53 +0300 Subject: [PATCH 4/6] Improve TFTP server shutdown and error handling - Enhance server stop mechanism with more robust socket and thread management - Add better handling of active transfers during server shutdown - Implement additional safety checks and timeout handling - Improve logging and error reporting for server stop process - Prevent potential deadlocks and resource leaks during server termination --- ComConfigCopy.py | 19 +++- TFTPServer.py | 255 ++++++++++++++++++++++++++++++++++------------- 2 files changed, 201 insertions(+), 73 deletions(-) diff --git a/ComConfigCopy.py b/ComConfigCopy.py index ab5d9cc..d691199 100644 --- a/ComConfigCopy.py +++ b/ComConfigCopy.py @@ -964,9 +964,22 @@ class SerialAppGUI(tk.Tk): """Остановка TFTP сервера.""" if self.tftp_server: try: + # Отключаем кнопки на время остановки сервера + self.start_tftp_button.config(state="disabled") + self.stop_tftp_button.config(state="disabled") + + # Останавливаем сервер self.tftp_server.stop_server() + + # Ждем завершения потока сервера с таймаутом if self.tftp_server_thread: - self.tftp_server_thread.join(timeout=2.0) + self.tftp_server_thread.join(timeout=5.0) + if self.tftp_server_thread.is_alive(): + self.append_tftp_log("[WARN] Превышено время ожидания остановки сервера") + + # Очищаем ссылки на сервер и поток + self.tftp_server = None + self.tftp_server_thread = None # Обновляем состояние кнопок self.start_tftp_button.config(state="normal") @@ -983,6 +996,10 @@ class SerialAppGUI(tk.Tk): except Exception as e: self.append_tftp_log(f"[ERROR] Ошибка остановки сервера: {str(e)}") messagebox.showerror("Ошибка", f"Не удалось остановить TFTP сервер: {str(e)}") + + # Восстанавливаем состояние кнопок в случае ошибки + self.start_tftp_button.config(state="disabled") + self.stop_tftp_button.config(state="normal") def append_tftp_log(self, message): """Добавление сообщения в лог TFTP сервера.""" diff --git a/TFTPServer.py b/TFTPServer.py index 48dace0..8a038e4 100644 --- a/TFTPServer.py +++ b/TFTPServer.py @@ -30,6 +30,7 @@ class TFTPServer: self.running = False self.server_socket = None self.lock = threading.Lock() + self.transfer_sockets = set() # Множество для хранения всех активных сокетов передачи # Словарь активных передач для мониторинга их статуса. # Ключ – адрес клиента, значение – словарь с информацией о передаче. self.active_transfers = {} @@ -60,33 +61,106 @@ class TFTPServer: :param ip: IP-адрес для привязки сервера. :param port: Порт для TFTP сервера. """ + if self.running: + self.log("[WARN] Сервер уже запущен") + return + 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: + 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: - # Каждая обработка запроса выполняется в отдельном потоке. + 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: - self.log(f"[ERROR] Ошибка получения данных: {str(e)}") + if self.running: # Логируем ошибку только если сервер еще запущен + self.log(f"[ERROR] Ошибка получения данных: {str(e)}") + break + except Exception as e: + self.log(f"[ERROR] Ошибка запуска сервера: {str(e)}") finally: - self.server_socket.close() + self.running = False + if self.server_socket: + try: + self.server_socket.close() + except: + pass + self.server_socket = None self.log("[INFO] TFTP сервер остановлен.") def stop_server(self): """ Остановка TFTP сервера. """ - self.running = False + 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): """ @@ -193,85 +267,122 @@ class TFTPServer: transfer_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) transfer_socket.settimeout(5.0) # Таймаут ожидания ACK + # Добавляем сокет в множество активных сокетов + with self.lock: + self.transfer_sockets.add(transfer_socket) + 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 + try: + with open(file_path, "rb") as f: + while self.running: # Проверяем флаг running + 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 + # Проверяем флаг running перед отправкой блока + if not self.running: + raise Exception("Передача прервана: сервер остановлен") - # Попытка отправки текущего блока (до 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 + # Формируем TFTP пакет данных + packet = struct.pack("!HH", 3, block_num) + data_block + attempts = 0 + ack_received = False - # Ожидаем подтверждение - 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 + # Попытка отправки текущего блока (до 3 повторных попыток) + while attempts < 3 and not ack_received and self.running: + if transfer_socket is None: + raise Exception("Сокет передачи закрыт") + + 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 - if not ack_received: - raise Exception(f"Не удалось получить подтверждение для блока {block_num}") + # Ожидаем подтверждение + ack_data, addr = transfer_socket.recvfrom(4) + 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 socket.error as e: + if not self.running: + raise Exception("Передача прервана: сервер остановлен") + self.log(f"[ERROR] Ошибка сокета при отправке блока {block_num}: {str(e)}") + attempts += 1 + except Exception as e: + if not self.running: + raise Exception("Передача прервана: сервер остановлен") + self.log(f"[ERROR] Ошибка при отправке блока {block_num}: {str(e)}") + attempts += 1 - block_num = (block_num + 1) % 65536 + if not ack_received: + raise Exception(f"Не удалось получить подтверждение для блока {block_num}") - except Exception as e: - self.log(f"[ERROR] Ошибка при обработке блока {block_num}: {str(e)}") - raise + block_num = (block_num + 1) % 65536 - elapsed_time = time.time() - start_time - self.log(f"[INFO] Передача файла '{file_basename}' клиенту {client_addr} " - f"завершена за {elapsed_time:.2f} сек. Всего отправлено {bytes_sent} байт.") + except Exception as e: + if not self.running: + raise Exception("Передача прервана: сервер остановлен") + raise + + if bytes_sent == filesize: + elapsed_time = time.time() - start_time + self.log(f"[INFO] Передача файла '{file_basename}' клиенту {client_addr} " + f"завершена за {elapsed_time:.2f} сек. Всего отправлено {bytes_sent} байт.") + except Exception as e: + if not self.running: + self.log(f"[INFO] Передача файла '{file_basename}' клиенту {client_addr} прервана: сервер остановлен") + else: + self.log(f"[ERROR] Ошибка при передаче файла '{file_basename}' " + f"клиенту {client_addr}: {str(e)}") + raise except Exception as e: - self.log(f"[ERROR] Ошибка при передаче файла '{os.path.basename(file_path)}' " - f"клиенту {client_addr}: {str(e)}") - try: - self.send_error(client_addr, 0, str(e)) - except: - pass - finally: - if transfer_socket: + if not self.running: + self.log(f"[INFO] Передача файла прервана: сервер остановлен") + else: + self.log(f"[ERROR] Ошибка при передаче файла '{os.path.basename(file_path)}' " + f"клиенту {client_addr}: {str(e)}") try: - transfer_socket.close() + self.send_error(client_addr, 0, str(e)) except: pass + finally: + # Закрываем сокет передачи + if transfer_socket: + try: + with self.lock: + self.transfer_sockets.discard(transfer_socket) + transfer_socket.close() + transfer_socket = None + except: + pass + + # Удаляем информацию о передаче with self.lock: if client_addr in self.active_transfers: del self.active_transfers[client_addr] \ No newline at end of file -- 2.49.1 From a252a0f1532876cf31c363f55bf4262e7dcfdb9f Mon Sep 17 00:00:00 2001 From: Lowa Date: Sun, 16 Feb 2025 03:31:32 +0300 Subject: [PATCH 5/6] Prevent duplicate TFTP server log messages and improve state tracking - Add filtering mechanism to prevent repeated server start/stop log entries - Implement state tracking flags to manage server status - Remove redundant log messages in both ComConfigCopy.py and TFTPServer.py - Enhance log callback to avoid unnecessary logging of server state changes --- ComConfigCopy.py | 23 +++++++++++++++++++---- TFTPServer.py | 32 ++++++++------------------------ 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/ComConfigCopy.py b/ComConfigCopy.py index d691199..e4218cd 100644 --- a/ComConfigCopy.py +++ b/ComConfigCopy.py @@ -924,7 +924,20 @@ class SerialAppGUI(tk.Tk): # Устанавливаем callback для логирования def log_callback(message): + # Фильтруем дублирующиеся сообщения о запуске/остановке сервера + if "[INFO] TFTP сервер запущен" in message and hasattr(self, '_server_started'): + return + if "[INFO] TFTP сервер остановлен" in message and hasattr(self, '_server_stopped'): + return + self.append_tftp_log(message) + + # Устанавливаем флаги для отслеживания состояния + if "[INFO] TFTP сервер запущен" in message: + self._server_started = True + elif "[INFO] TFTP сервер остановлен" in message: + self._server_stopped = True + # Обновляем информацию о передачах self.update_transfers_info() @@ -944,8 +957,6 @@ class SerialAppGUI(tk.Tk): 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() @@ -968,6 +979,12 @@ class SerialAppGUI(tk.Tk): self.start_tftp_button.config(state="disabled") self.stop_tftp_button.config(state="disabled") + # Сбрасываем флаги состояния + if hasattr(self, '_server_started'): + delattr(self, '_server_started') + if hasattr(self, '_server_stopped'): + delattr(self, '_server_stopped') + # Останавливаем сервер self.tftp_server.stop_server() @@ -987,8 +1004,6 @@ class SerialAppGUI(tk.Tk): 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) diff --git a/TFTPServer.py b/TFTPServer.py index 8a038e4..f01bd89 100644 --- a/TFTPServer.py +++ b/TFTPServer.py @@ -97,7 +97,6 @@ class TFTPServer: except: pass self.server_socket = None - self.log("[INFO] TFTP сервер остановлен.") def stop_server(self): """ @@ -160,7 +159,7 @@ class TFTPServer: self.log(f"[ERROR] Ошибка при остановке сервера: {str(e)}") finally: self.running = False # Гарантируем, что флаг running будет False - self.log("[INFO] TFTP сервер остановлен.") + self.log("[INFO] TFTP сервер остановлен") def handle_request(self, data, client_addr): """ @@ -223,17 +222,6 @@ class TFTPServer: def send_file(self, file_path, client_addr): """ Передача файла клиенту по протоколу TFTP. - - В процессе передачи обновляется состояние активной передачи, которое логируется с информацией: - - адрес клиента, - - имя файла, - - размер файла, - - количество переданных байт, - - оставшиеся байты, - - время передачи. - - :param file_path: Полный путь к файлу для передачи. - :param client_addr: Адрес клиента. """ BLOCK_SIZE = 512 transfer_socket = None @@ -356,21 +344,17 @@ class TFTPServer: except Exception as e: if not self.running: self.log(f"[INFO] Передача файла '{file_basename}' клиенту {client_addr} прервана: сервер остановлен") - else: - self.log(f"[ERROR] Ошибка при передаче файла '{file_basename}' " - f"клиенту {client_addr}: {str(e)}") raise except Exception as e: if not self.running: - self.log(f"[INFO] Передача файла прервана: сервер остановлен") - else: - self.log(f"[ERROR] Ошибка при передаче файла '{os.path.basename(file_path)}' " - f"клиенту {client_addr}: {str(e)}") - try: - self.send_error(client_addr, 0, str(e)) - except: - pass + return # Не логируем повторно о прерывании передачи + self.log(f"[ERROR] Ошибка при передаче файла '{os.path.basename(file_path)}' " + f"клиенту {client_addr}: {str(e)}") + try: + self.send_error(client_addr, 0, str(e)) + except: + pass finally: # Закрываем сокет передачи if transfer_socket: -- 2.49.1 From 6d2819a860797f512038f5b9bcb241a42cd739de Mon Sep 17 00:00:00 2001 From: Lowa Date: Sun, 16 Feb 2025 03:34:57 +0300 Subject: [PATCH 6/6] Add network adapter selection for TFTP server - Implement `get_network_adapters()` function to dynamically retrieve available network interfaces - Replace IP address entry with a combobox for network adapter selection - Add "Update" button to refresh network adapter list - Improve IP address validation and error handling for TFTP server configuration - Enhance UI with more user-friendly network interface selection --- ComConfigCopy.py | 86 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 61 insertions(+), 25 deletions(-) diff --git a/ComConfigCopy.py b/ComConfigCopy.py index e4218cd..0ed0818 100644 --- a/ComConfigCopy.py +++ b/ComConfigCopy.py @@ -39,6 +39,7 @@ from serial.serialutil import SerialException from about_window import AboutWindow from TFTPServer import TFTPServer # from TFTPServer import TFTPServerThread +import socket # Создаем необходимые папки os.makedirs("Logs", exist_ok=True) @@ -138,25 +139,37 @@ def list_serial_ports(): # Функции работы с сетевыми адаптерами (не используются) # ========================== -# def list_network_adapters(): -# """Возвращает список названий сетевых адаптеров (Windows).""" -# adapters = [] -# if platform.system() == "Windows": -# try: -# output = subprocess.check_output( -# 'wmic nic get NetConnectionID', -# shell=True, -# encoding="cp866" -# ) -# for line in output.splitlines(): -# line = line.strip() -# if line and line != "NetConnectionID": -# adapters.append(line) -# except Exception as e: -# logging.error(f"Ошибка при получении списка адаптеров: {e}", exc_info=True) -# else: -# adapters = ["eth0"] -# return adapters +def get_network_adapters(): + """Получение списка сетевых адаптеров и их IP-адресов.""" + adapters = [] + try: + # Получаем имя хоста + hostname = socket.gethostname() + # Получаем все адреса для данного хоста + addresses = socket.getaddrinfo(hostname, None) + + # Создаем множество для хранения уникальных IP-адресов + unique_ips = set() + + for addr in addresses: + ip = addr[4][0] + # Пропускаем IPv6 и локальные адреса + if ':' not in ip and not ip.startswith('127.'): + unique_ips.add(ip) + + # Добавляем все найденные IP-адреса в список + for ip in sorted(unique_ips): + adapters.append(f"{ip}") + + # Добавляем 0.0.0.0 для прослушивания всех интерфейсов + adapters.insert(0, "0.0.0.0") + + except Exception as e: + logging.error(f"Ошибка при получении списка сетевых адаптеров: {e}", exc_info=True) + # В случае ошибки возвращаем хотя бы 0.0.0.0 + adapters = ["0.0.0.0"] + + return adapters # ========================== # Функции работы с COM-соединением @@ -843,8 +856,12 @@ class SerialAppGUI(tk.Tk): 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") - self.tftp_ip_entry = ttk.Entry(ip_frame, textvariable=self.tftp_ip_var) - self.tftp_ip_entry.pack(fill=X, expand=True, padx=5) + self.tftp_ip_combo = ttk.Combobox(ip_frame, textvariable=self.tftp_ip_var, state="readonly") + self.tftp_ip_combo.pack(side=LEFT, fill=X, expand=True, padx=5) + ttk.Button(ip_frame, text="Обновить", command=self.update_network_adapters).pack(side=LEFT, padx=5) + + # Заполняем список адаптеров + self.update_network_adapters() # Порт port_frame = ttk.Frame(controls_frame) @@ -916,8 +933,20 @@ class SerialAppGUI(tk.Tk): def start_tftp_server(self): """Запуск TFTP сервера.""" try: + # Получаем выбранный IP-адрес ip = self.tftp_ip_var.get() - port = int(self.tftp_port_var.get()) + if not ip: + messagebox.showerror("Ошибка", "Выберите IP-адрес для TFTP сервера") + return + + # Проверяем корректность порта + try: + port = int(self.tftp_port_var.get()) + if port <= 0 or port > 65535: + raise ValueError("Порт должен быть в диапазоне 1-65535") + except ValueError as e: + messagebox.showerror("Ошибка", f"Некорректный порт: {str(e)}") + return # Создаем экземпляр TFTP сервера self.tftp_server = TFTPServer("Firmware") @@ -951,10 +980,10 @@ class SerialAppGUI(tk.Tk): ) 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_ip_combo.config(state="disabled") self.tftp_port_entry.config(state="disabled") # Запускаем периодическое обновление информации о передачах @@ -1001,7 +1030,7 @@ class SerialAppGUI(tk.Tk): # Обновляем состояние кнопок self.start_tftp_button.config(state="normal") self.stop_tftp_button.config(state="disabled") - self.tftp_ip_entry.config(state="normal") + self.tftp_ip_combo.config(state="normal") self.tftp_port_entry.config(state="normal") # Очищаем таблицу передач @@ -1058,6 +1087,13 @@ class SerialAppGUI(tk.Tk): # Планируем следующее обновление через 1 секунду self.after(1000, self.update_transfers_periodically) + def update_network_adapters(self): + """Обновление списка сетевых адаптеров.""" + adapters = get_network_adapters() + self.tftp_ip_combo["values"] = adapters + if not self.tftp_ip_var.get() in adapters: + self.tftp_ip_var.set(adapters[0]) + # ========================== # Парсер аргументов (не используется) # ========================== -- 2.49.1