From f088d05731f39687fe89253c0635badecf348d44 Mon Sep 17 00:00:00 2001 From: "A.Klivtsov" Date: Mon, 13 Apr 2026 21:54:43 +0300 Subject: [PATCH] Client: CommandValidator and initialRequest were added,fixed behaviour if server is unreachable; Server: GetCommandsCommand was added --- client/src/main/kotlin/Main.kt | 83 +++++++----------- .../src/main/kotlin/network/NetworkManager.kt | 84 ++++++++++--------- .../main/kotlin/runner/CommandValidator.kt | 36 ++++++++ data.csv | 1 + server/src/main/kotlin/Main.kt | 1 + .../kotlin/commands/GetCommandsCommand.kt | 15 ++++ 6 files changed, 128 insertions(+), 92 deletions(-) create mode 100644 data.csv create mode 100644 server/src/main/kotlin/commands/GetCommandsCommand.kt diff --git a/client/src/main/kotlin/Main.kt b/client/src/main/kotlin/Main.kt index 12f6fc5..07262b4 100644 --- a/client/src/main/kotlin/Main.kt +++ b/client/src/main/kotlin/Main.kt @@ -2,51 +2,43 @@ import network.NetworkManager import input.HumanBeingBuilder import input.IOManager import network.Request - - -// BEGIN ---- Move it to CommandValidator - -// Команды, которые требуют ввода HumanBeing -private val HUMAN_BEING_COMMANDS = setOf("add", "add_if_max", "add_if_min") - -// Команды с аргументом (UUID) в строке команды -private val ARG_COMMANDS = setOf("update", "remove_by_id", "execute_script") - -// Все команды, доступные клиенту (save — только серверная) -private val KNOWN_COMMANDS = setOf( - "help", "info", "show", "add", "update", - "remove_by_id", "clear", "execute_script", - "add_if_max", "add_if_min", "history", - "sum_of_minutes_of_waiting", "min_by_name", - "print_field_descending_minutes_of_waiting", - "exit" -) - -// END ---- Move it to CommandValidator +import runner.CommandValidator fun main(args: Array) { val host = if (args.isNotEmpty()) args[0] else "localhost" val port = if (args.size >= 2) args[1].toIntOrNull() ?: 8080 else 8080 - val io = IOManager() + val io = IOManager() // Name by Boris Bosenko val client = NetworkManager(host, port) + val validator = CommandValidator() - io.print(""" - |╔══════════════════════════════════════╗ - |║ Lab6 Client — подключение к $host:$port - |╚══════════════════════════════════════╝ - |Введите 'help' для справки. - """.trimMargin()) - - // Shutdown hook — закрыть канал при выходе + // Shutdown hook Runtime.getRuntime().addShutdownHook(Thread { client.close() }) - runClientRepl(io, client) + io.print(""" + |════════════════════════════════════════════ + |Lab6 Client — подключение к $host:$port + |════════════════════════════════════════════ + """.trimMargin()) + + runClientRepl(io, client, validator) } -fun runClientRepl(io: IOManager, client: NetworkManager) { +fun runClientRepl(io: IOManager, client: NetworkManager, validator: CommandValidator) { + + val initialRequest = Request( + commandName = "getCommandsCommand", + humanBeing = null + ) + + if (client.sendRequest(initialRequest) == null) { + io.print("[!] Сервер недоступен. Попробуйте позже.") + } else { + io.print("|Введите 'help' для справки.") + } + while (true) { val line = io.readLine(" ☭ ") ?: break if (line.isBlank()) continue @@ -55,9 +47,10 @@ fun runClientRepl(io: IOManager, client: NetworkManager) { val commandName = parts[0].lowercase() val args = parts.drop(1) - // Move to CommandValidate - if (commandName !in KNOWN_COMMANDS) { - io.print("Неизвестная команда: '$commandName'. Введите 'help' для справки.") + val (isValidCommand, message) = validator.validate(commandName, args) + + if (!isValidCommand) { + if (message != null) io.print(message) continue } @@ -66,26 +59,12 @@ fun runClientRepl(io: IOManager, client: NetworkManager) { break } - // In the event of using fucking script (don't) if (commandName == "execute_script") { - if (args.isEmpty()) { - io.print("[Ошибка] Укажите путь к файлу. Пример: execute_script script.txt") - continue - } io.addScriptScanner(args[0]) continue } - // MOVE TO FUCKING VALIDATE - // 4. Для update — нужен UUID аргумент ДО ввода HumanBeing - if (commandName == "update" && args.isEmpty()) { - io.print("[Ошибка] Укажите id элемента. Пример: update ") - continue - } - - // 5. Строим HumanBeing если нужно - val humanBeing = if (commandName in HUMAN_BEING_COMMANDS || - commandName == "update") { + val humanBeing = if (validator.isBuildsHumanBeing(commandName)) { try { HumanBeingBuilder(io).build() } catch (e: IllegalStateException) { @@ -94,7 +73,7 @@ fun runClientRepl(io: IOManager, client: NetworkManager) { } } else null - // 6. Формируем запрос и отправляем + // Формируем запрос и отправляем val request = Request( commandName = commandName, args = args, @@ -103,7 +82,7 @@ fun runClientRepl(io: IOManager, client: NetworkManager) { val response = client.sendRequest(request) - // 7. Обрабатываем ответ + // Обрабатываем ответ if (response == null) { io.print("[!] Сервер недоступен. Попробуйте позже.") } else { diff --git a/client/src/main/kotlin/network/NetworkManager.kt b/client/src/main/kotlin/network/NetworkManager.kt index d98f102..0c469c2 100644 --- a/client/src/main/kotlin/network/NetworkManager.kt +++ b/client/src/main/kotlin/network/NetworkManager.kt @@ -3,46 +3,43 @@ package network import kotlinx.serialization.SerializationException import lab6.prog.network.AppJson import java.net.InetSocketAddress +import java.net.PortUnreachableException import java.nio.ByteBuffer import java.nio.channels.DatagramChannel - -/** - * UDP-клиент на основе DatagramChannel в неблокирующем режиме. - * - * При недоступности сервера уведомляет пользователя и повторяет - * попытку каждые [retryDelayMS] мс, не более [maxRetries] раз. - * - * @property host адрес сервера - * @property port порт сервера - */ class NetworkManager( private val host: String, private val port: Int, - private val bufferSize: Int = 65507, - private val timeoutMS: Long = 3000, - private val retryDelayMS: Long = 5000, - private val maxRetries: Int = 3 + private val bufferSize: Int = 65507, + private val timeoutMS: Long = 3000, + private val retryDelayMS: Long = 5000, + private val maxRetries: Int = 3, ) { - private val channel: DatagramChannel = DatagramChannel.open().also { - it.configureBlocking(false) - it.connect(InetSocketAddress(host, port)) - } + private var channel: DatagramChannel = openChannel() + + private fun openChannel(): DatagramChannel = + DatagramChannel.open().also { + it.configureBlocking(false) + it.connect(InetSocketAddress(host, port)) + } - /** - * Отправляет [Request] и возвращает [Response]. - * При недоступности сервера возвращает null. - */ fun sendRequest(request: Request): Response? { - val requestBytes = AppJson.encodeToString(Request.serializer(), request).toByteArray(Charsets.UTF_8) + val requestBytes = AppJson.encodeToString(Request.serializer(), request) + .toByteArray(Charsets.UTF_8) repeat(maxRetries) { attempt -> + try { + channel.write(ByteBuffer.wrap(requestBytes)) + val response = waitForResponse() + if (response != null) return response - channel.write(ByteBuffer.wrap(requestBytes)) - - val response = waitForResponse() - if (response != null) return response + } catch (e: PortUnreachableException) { + reconnect() + } catch (e: Exception) { + System.err.println("[Client] Ошибка отправки: ${e.message}") + reconnect() + } println( "[Client] Сервер не отвечает " + @@ -56,29 +53,36 @@ class NetworkManager( return null } - /** - * Опрашивает канал до [timeoutMS] мс, возвращает ответ или null. - */ - // catch java.net.PortUnreachableException if server is no more reachable private fun waitForResponse(): Response? { val buf = ByteBuffer.allocate(bufferSize) val deadline = System.currentTimeMillis() + timeoutMS while (System.currentTimeMillis() < deadline) { - buf.clear() - if (channel.read(buf) > 0) { - buf.flip() - return try { - AppJson.decodeFromString(Charsets.UTF_8.decode(buf).toString()) - } catch (e: SerializationException) { - System.err.println("[Client] Некорректный ответ сервера: ${e.message}") - null + try { + buf.clear() + if (channel.read(buf) > 0) { + buf.flip() + return AppJson.decodeFromString( + Charsets.UTF_8.decode(buf).toString() + ) } + } catch (e: PortUnreachableException) { + return null + } catch (e: SerializationException) { + System.err.println("[Client] Некорректный ответ сервера: ${e.message}") + return null } Thread.sleep(50) } return null } - fun close() = channel.close() + private fun reconnect() { + try { channel.close() } catch (_: Exception) {} + channel = openChannel() + } + + fun close() { + try { channel.close() } catch (_: Exception) {} + } } diff --git a/client/src/main/kotlin/runner/CommandValidator.kt b/client/src/main/kotlin/runner/CommandValidator.kt index abff8e3..57b5e60 100644 --- a/client/src/main/kotlin/runner/CommandValidator.kt +++ b/client/src/main/kotlin/runner/CommandValidator.kt @@ -1,4 +1,40 @@ package runner class CommandValidator { + + // Команды, которые требуют ввода HumanBeing + private val humanBeingCommands = setOf("add", "add_if_max", "add_if_min") + + // Команды с аргументом + private val commandsWArgs = setOf("update", "remove_by_id", "execute_script") + + // Все команды, доступные клиенту + private val knownCommands = setOf( + "help", "info", "show", "add", "update", + "remove_by_id", "clear", "execute_script", + "add_if_max", "add_if_min", "history", + "sum_of_minutes_of_waiting", "min_by_name", + "print_field_descending_minutes_of_waiting", + "exit" + ) + + fun validate(command: String, args: List?): Pair { + if (command !in knownCommands){ + return Pair(false, "Неизвестная команда: '$command'. Введите 'help' для справки.") + } + + if (command in commandsWArgs && args?.isEmpty() == true) { + return if (command == "execute_script") { + Pair(false, "[Ошибка] Укажите путь к файлу. Пример: execute_script script.txt") + } else { + // maybe create another set w commands w file args and another for other args + Pair(false, "[Ошибка] Укажите id элемента. Пример: $command ") + } + } + return Pair(true, null) + } + + fun isBuildsHumanBeing(command: String): Boolean { + return command in humanBeingCommands + } } \ No newline at end of file diff --git a/data.csv b/data.csv new file mode 100644 index 0000000..f46a7a4 --- /dev/null +++ b/data.csv @@ -0,0 +1 @@ +id,name,coordX,coordY,creationDate,realHero,hasToothpick,impactSpeed,soundtrackName,minutesOfWaiting,weaponType,carCool diff --git a/server/src/main/kotlin/Main.kt b/server/src/main/kotlin/Main.kt index f82ed9f..c9b60b7 100644 --- a/server/src/main/kotlin/Main.kt +++ b/server/src/main/kotlin/Main.kt @@ -67,6 +67,7 @@ fun registerServerCommands( AddIfMaxCommand(manager), AddIfMinCommand(manager), HistoryCommand(invoker), + GetCommandsCommand(invoker), SumOfMinutesCommand(manager), MinByNameCommand(manager), PrintDescendingMinutesCommand(manager), diff --git a/server/src/main/kotlin/commands/GetCommandsCommand.kt b/server/src/main/kotlin/commands/GetCommandsCommand.kt new file mode 100644 index 0000000..132eb87 --- /dev/null +++ b/server/src/main/kotlin/commands/GetCommandsCommand.kt @@ -0,0 +1,15 @@ +package commands + +import models.HumanBeing +import runner.CommandInvoker + +class GetCommandsCommand ( + private val invoker: CommandInvoker +): Command { + override val name: String = "GetCommands" + override val description: String = "Получает все доступные с сервера команды" + + override fun execute(args: List, humanBeing: HumanBeing?): String { + return invoker.getCommands().toString() + } +} \ No newline at end of file