Client: CommandValidator and initialRequest were added,fixed behaviour if server is unreachable; Server: GetCommandsCommand was added
This commit is contained in:
@@ -2,51 +2,43 @@ import network.NetworkManager
|
|||||||
import input.HumanBeingBuilder
|
import input.HumanBeingBuilder
|
||||||
import input.IOManager
|
import input.IOManager
|
||||||
import network.Request
|
import network.Request
|
||||||
|
import runner.CommandValidator
|
||||||
|
|
||||||
// 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
|
|
||||||
|
|
||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
val host = if (args.isNotEmpty()) args[0] else "localhost"
|
val host = if (args.isNotEmpty()) args[0] else "localhost"
|
||||||
val port = if (args.size >= 2) args[1].toIntOrNull() ?: 8080 else 8080
|
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 client = NetworkManager(host, port)
|
||||||
|
val validator = CommandValidator()
|
||||||
|
|
||||||
io.print("""
|
// Shutdown hook
|
||||||
|╔══════════════════════════════════════╗
|
|
||||||
|║ Lab6 Client — подключение к $host:$port
|
|
||||||
|╚══════════════════════════════════════╝
|
|
||||||
|Введите 'help' для справки.
|
|
||||||
""".trimMargin())
|
|
||||||
|
|
||||||
// Shutdown hook — закрыть канал при выходе
|
|
||||||
Runtime.getRuntime().addShutdownHook(Thread {
|
Runtime.getRuntime().addShutdownHook(Thread {
|
||||||
client.close()
|
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) {
|
while (true) {
|
||||||
val line = io.readLine(" ☭ ") ?: break
|
val line = io.readLine(" ☭ ") ?: break
|
||||||
if (line.isBlank()) continue
|
if (line.isBlank()) continue
|
||||||
@@ -55,9 +47,10 @@ fun runClientRepl(io: IOManager, client: NetworkManager) {
|
|||||||
val commandName = parts[0].lowercase()
|
val commandName = parts[0].lowercase()
|
||||||
val args = parts.drop(1)
|
val args = parts.drop(1)
|
||||||
|
|
||||||
// Move to CommandValidate
|
val (isValidCommand, message) = validator.validate(commandName, args)
|
||||||
if (commandName !in KNOWN_COMMANDS) {
|
|
||||||
io.print("Неизвестная команда: '$commandName'. Введите 'help' для справки.")
|
if (!isValidCommand) {
|
||||||
|
if (message != null) io.print(message)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,26 +59,12 @@ fun runClientRepl(io: IOManager, client: NetworkManager) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// In the event of using fucking script (don't)
|
|
||||||
if (commandName == "execute_script") {
|
if (commandName == "execute_script") {
|
||||||
if (args.isEmpty()) {
|
|
||||||
io.print("[Ошибка] Укажите путь к файлу. Пример: execute_script script.txt")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
io.addScriptScanner(args[0])
|
io.addScriptScanner(args[0])
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// MOVE TO FUCKING VALIDATE
|
val humanBeing = if (validator.isBuildsHumanBeing(commandName)) {
|
||||||
// 4. Для update — нужен UUID аргумент ДО ввода HumanBeing
|
|
||||||
if (commandName == "update" && args.isEmpty()) {
|
|
||||||
io.print("[Ошибка] Укажите id элемента. Пример: update <uuid>")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Строим HumanBeing если нужно
|
|
||||||
val humanBeing = if (commandName in HUMAN_BEING_COMMANDS ||
|
|
||||||
commandName == "update") {
|
|
||||||
try {
|
try {
|
||||||
HumanBeingBuilder(io).build()
|
HumanBeingBuilder(io).build()
|
||||||
} catch (e: IllegalStateException) {
|
} catch (e: IllegalStateException) {
|
||||||
@@ -94,7 +73,7 @@ fun runClientRepl(io: IOManager, client: NetworkManager) {
|
|||||||
}
|
}
|
||||||
} else null
|
} else null
|
||||||
|
|
||||||
// 6. Формируем запрос и отправляем
|
// Формируем запрос и отправляем
|
||||||
val request = Request(
|
val request = Request(
|
||||||
commandName = commandName,
|
commandName = commandName,
|
||||||
args = args,
|
args = args,
|
||||||
@@ -103,7 +82,7 @@ fun runClientRepl(io: IOManager, client: NetworkManager) {
|
|||||||
|
|
||||||
val response = client.sendRequest(request)
|
val response = client.sendRequest(request)
|
||||||
|
|
||||||
// 7. Обрабатываем ответ
|
// Обрабатываем ответ
|
||||||
if (response == null) {
|
if (response == null) {
|
||||||
io.print("[!] Сервер недоступен. Попробуйте позже.")
|
io.print("[!] Сервер недоступен. Попробуйте позже.")
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -3,47 +3,44 @@ package network
|
|||||||
import kotlinx.serialization.SerializationException
|
import kotlinx.serialization.SerializationException
|
||||||
import lab6.prog.network.AppJson
|
import lab6.prog.network.AppJson
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
|
import java.net.PortUnreachableException
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.channels.DatagramChannel
|
import java.nio.channels.DatagramChannel
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UDP-клиент на основе DatagramChannel в неблокирующем режиме.
|
|
||||||
*
|
|
||||||
* При недоступности сервера уведомляет пользователя и повторяет
|
|
||||||
* попытку каждые [retryDelayMS] мс, не более [maxRetries] раз.
|
|
||||||
*
|
|
||||||
* @property host адрес сервера
|
|
||||||
* @property port порт сервера
|
|
||||||
*/
|
|
||||||
class NetworkManager(
|
class NetworkManager(
|
||||||
private val host: String,
|
private val host: String,
|
||||||
private val port: Int,
|
private val port: Int,
|
||||||
private val bufferSize: Int = 65507,
|
private val bufferSize: Int = 65507,
|
||||||
private val timeoutMS: Long = 3000,
|
private val timeoutMS: Long = 3000,
|
||||||
private val retryDelayMS: Long = 5000,
|
private val retryDelayMS: Long = 5000,
|
||||||
private val maxRetries: Int = 3
|
private val maxRetries: Int = 3,
|
||||||
) {
|
) {
|
||||||
private val channel: DatagramChannel = DatagramChannel.open().also {
|
private var channel: DatagramChannel = openChannel()
|
||||||
|
|
||||||
|
private fun openChannel(): DatagramChannel =
|
||||||
|
DatagramChannel.open().also {
|
||||||
it.configureBlocking(false)
|
it.configureBlocking(false)
|
||||||
it.connect(InetSocketAddress(host, port))
|
it.connect(InetSocketAddress(host, port))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Отправляет [Request] и возвращает [Response].
|
|
||||||
* При недоступности сервера возвращает null.
|
|
||||||
*/
|
|
||||||
fun sendRequest(request: Request): Response? {
|
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 ->
|
repeat(maxRetries) { attempt ->
|
||||||
|
try {
|
||||||
channel.write(ByteBuffer.wrap(requestBytes))
|
channel.write(ByteBuffer.wrap(requestBytes))
|
||||||
|
|
||||||
val response = waitForResponse()
|
val response = waitForResponse()
|
||||||
if (response != null) return response
|
if (response != null) return response
|
||||||
|
|
||||||
|
} catch (e: PortUnreachableException) {
|
||||||
|
reconnect()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
System.err.println("[Client] Ошибка отправки: ${e.message}")
|
||||||
|
reconnect()
|
||||||
|
}
|
||||||
|
|
||||||
println(
|
println(
|
||||||
"[Client] Сервер не отвечает " +
|
"[Client] Сервер не отвечает " +
|
||||||
"(попытка ${attempt + 1}/$maxRetries). " +
|
"(попытка ${attempt + 1}/$maxRetries). " +
|
||||||
@@ -56,29 +53,36 @@ class NetworkManager(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Опрашивает канал до [timeoutMS] мс, возвращает ответ или null.
|
|
||||||
*/
|
|
||||||
// catch java.net.PortUnreachableException if server is no more reachable
|
|
||||||
private fun waitForResponse(): Response? {
|
private fun waitForResponse(): Response? {
|
||||||
val buf = ByteBuffer.allocate(bufferSize)
|
val buf = ByteBuffer.allocate(bufferSize)
|
||||||
val deadline = System.currentTimeMillis() + timeoutMS
|
val deadline = System.currentTimeMillis() + timeoutMS
|
||||||
|
|
||||||
while (System.currentTimeMillis() < deadline) {
|
while (System.currentTimeMillis() < deadline) {
|
||||||
|
try {
|
||||||
buf.clear()
|
buf.clear()
|
||||||
if (channel.read(buf) > 0) {
|
if (channel.read(buf) > 0) {
|
||||||
buf.flip()
|
buf.flip()
|
||||||
return try {
|
return AppJson.decodeFromString<Response>(
|
||||||
AppJson.decodeFromString<Response>(Charsets.UTF_8.decode(buf).toString())
|
Charsets.UTF_8.decode(buf).toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: PortUnreachableException) {
|
||||||
|
return null
|
||||||
} catch (e: SerializationException) {
|
} catch (e: SerializationException) {
|
||||||
System.err.println("[Client] Некорректный ответ сервера: ${e.message}")
|
System.err.println("[Client] Некорректный ответ сервера: ${e.message}")
|
||||||
null
|
return null
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Thread.sleep(50)
|
Thread.sleep(50)
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun close() = channel.close()
|
private fun reconnect() {
|
||||||
|
try { channel.close() } catch (_: Exception) {}
|
||||||
|
channel = openChannel()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun close() {
|
||||||
|
try { channel.close() } catch (_: Exception) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,40 @@
|
|||||||
package runner
|
package runner
|
||||||
|
|
||||||
class CommandValidator {
|
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<String>?): Pair<Boolean, String?> {
|
||||||
|
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 <uuid>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Pair(true, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isBuildsHumanBeing(command: String): Boolean {
|
||||||
|
return command in humanBeingCommands
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
id,name,coordX,coordY,creationDate,realHero,hasToothpick,impactSpeed,soundtrackName,minutesOfWaiting,weaponType,carCool
|
||||||
|
@@ -67,6 +67,7 @@ fun registerServerCommands(
|
|||||||
AddIfMaxCommand(manager),
|
AddIfMaxCommand(manager),
|
||||||
AddIfMinCommand(manager),
|
AddIfMinCommand(manager),
|
||||||
HistoryCommand(invoker),
|
HistoryCommand(invoker),
|
||||||
|
GetCommandsCommand(invoker),
|
||||||
SumOfMinutesCommand(manager),
|
SumOfMinutesCommand(manager),
|
||||||
MinByNameCommand(manager),
|
MinByNameCommand(manager),
|
||||||
PrintDescendingMinutesCommand(manager),
|
PrintDescendingMinutesCommand(manager),
|
||||||
|
|||||||
@@ -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<String>, humanBeing: HumanBeing?): String {
|
||||||
|
return invoker.getCommands().toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user