initial commit2

This commit is contained in:
2026-06-12 12:28:00 +03:00
parent 13447b858e
commit 59d33f8061
595 changed files with 11459 additions and 1 deletions
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
+69
View File
@@ -0,0 +1,69 @@
import commands.AuthCommand
import commands.ExecuteScriptCommand
import commands.ExitCommand
import commands.HelpCommand
import commands.LogoutCommand
import commands.SyncCommand
import network.NetworkManager
import input.IOManager
import runner.CommandInvoker
fun main(args: Array<String>) {
val host = if (args.isNotEmpty()) args[0] else "localhost"
val port = if (args.size >= 2) args[1].toIntOrNull() ?: 8080 else 8080
val io = IOManager() // Name by Boris Bosenko
val client = NetworkManager(host, port)
val invoker = CommandInvoker(io, client)
// Shutdown hook
Runtime.getRuntime().addShutdownHook(Thread {
client.close()
})
// do auth stuff
listOf(
HelpCommand(invoker),
AuthCommand(networkManager = client),
).forEach { invoker.register(it)}
while (client.getToken() == "no token") {
val line = io.readLine(" ☭ ") ?: break
if (line.isBlank()) continue
invoker.execute(line)
}
io.print("""
|════════════════════════════════════════════
|Lab6 Client — подключение к $host:$port
|════════════════════════════════════════════
""".trimMargin())
registerBasicCommands(invoker, client, io)
runClientRepl(io, invoker)
}
fun registerBasicCommands(invoker: CommandInvoker,client: NetworkManager, io: IOManager) {
listOf(
ExecuteScriptCommand(io),
SyncCommand(invoker, client),
LogoutCommand(client)
).forEach { invoker.register(it)}
}
fun runClientRepl(io: IOManager, invoker : CommandInvoker) {
// sync commands w server
invoker.execute("sync")
var isRunning = true
invoker.register(ExitCommand {isRunning = false})
while (isRunning) {
val line = io.readLine(" ☭ ") ?: break
if (line.isBlank()) continue
invoker.execute(line)
}
io.print("До свидания!")
}
@@ -0,0 +1,43 @@
package commands
import models.HumanBeing
import network.AuthRequest
import network.AuthResponse
import network.AuthType
import network.NetworkManager
import java.security.MessageDigest
class AuthCommand(
private val networkManager: NetworkManager,
): ClientCommand {
override val name: String = "auth"
override val description: String = "вход или регистрация. auth login <username> <password> or auth register <username> <password>"
override val args: List<CommandArgType> = listOf(CommandArgType.NONE)
override val type: CommandType = CommandType.SIMPLE
override fun execute(args: List<String>, humanBeing: HumanBeing?): String {
if (args.isEmpty()) return "[Ошибка] Укажите действие, логин и пароль. Пример: auth login <username> <password>"
if (args[1].isEmpty() || args[2].isEmpty()) return "[Ошибка] Укажите логин и пароль. Пример: auth login <username> <password>"
val res = if (args[0] == "login") {
networkManager.sendRequest(AuthRequest(args[1], md5Hash(args[2]), AuthType.LOGIN))
} else if (args[0] == "register") {
networkManager.sendRequest(AuthRequest(args[1], md5Hash(args[2]), AuthType.REGISTER))
} else {
null
}
if (res == null) return "[!] Сервер не отвечает."
if (res.success) {
networkManager.setToken(res.accessToken!!)
networkManager.setUUID(res.id!!)
}
return res.message
}
private fun md5Hash(input: String): String {
val md = MessageDigest.getInstance("MD5")
val hashBytes = md.digest(input.toByteArray())
return hashBytes.joinToString("") { String.format("%02x", it) }
}
}
@@ -0,0 +1,13 @@
package commands
import models.HumanBeing
interface ClientCommand {
val name: String
val description: String
val args: List<CommandArgType>
val type: CommandType
fun execute(args: List<String>, humanBeing: HumanBeing? = null): String
}
@@ -0,0 +1,16 @@
package commands
import input.IOManager
import models.HumanBeing
class ExecuteScriptCommand(private val io: IOManager) : ClientCommand {
override val name = "execute_script"
override val description = "Выполняет скрипт из файла"
override val args = listOf(CommandArgType.STRING)
override val type = CommandType.SIMPLE
override fun execute(args: List<String>, humanBeing: HumanBeing?): String {
io.addScriptScanner(args[0])
return ""
}
}
@@ -0,0 +1,15 @@
package commands
import models.HumanBeing
class ExitCommand(private val onExit: () -> Unit) : ClientCommand {
override val name = "exit"
override val description = "завершить клиент"
override val args: List<CommandArgType> = listOf(CommandArgType.NONE)
override val type = CommandType.SIMPLE
override fun execute(args: List<String>, humanBeing: HumanBeing?): String {
onExit()
return ""
}
}
@@ -0,0 +1,16 @@
package commands
import models.HumanBeing
import runner.CommandInvoker
class HelpCommand(private val invoker: CommandInvoker) : ClientCommand {
override val name = "help"
override val description = "вывести справку по доступным командам"
override val args = listOf(CommandArgType.NONE)
override val type = CommandType.SIMPLE
override fun execute(args: List<String>, humanBeing: HumanBeing?): String =
invoker.getCommands().values
.sortedBy { it.name }
.joinToString("\n") { " ${it.name.padEnd(40)} — ${it.description}" }
}
@@ -0,0 +1,16 @@
package commands
import models.HumanBeing
import network.NetworkManager
class LogoutCommand(private val networkManager: NetworkManager): ClientCommand {
override val name: String = "logout"
override val description: String = "выход из аккаунта"
override val args: List<CommandArgType> = listOf(CommandArgType.NONE)
override val type: CommandType = CommandType.SIMPLE
override fun execute(args: List<String>, humanBeing: HumanBeing?): String {
networkManager.clearToken()
return "Пользователь вышел. Войдите заново"
}
}
@@ -0,0 +1,25 @@
package commands
import models.HumanBeing
import network.Request
import network.NetworkManager
class ServerCommand(
override var name: String,
override var description: String,
override var args: List<CommandArgType>,
override var type: CommandType,
private val networkManager: NetworkManager,
): ClientCommand {
override fun execute(args: List<String>, humanBeing: HumanBeing?): String {
val request = Request(
commandName = this.name,
args = args,
humanBeing = humanBeing,
jwt = networkManager.getToken()
)
val response = networkManager.sendRequest(request)
return response?.message ?: "[!] Сервер не ответил"
}
}
@@ -0,0 +1,42 @@
package commands
import models.HumanBeing
import network.NetworkManager
import network.Request
import runner.CommandInvoker
class SyncCommand (
private val invoker: CommandInvoker,
private val client: NetworkManager,
) : ClientCommand {
override val name = "sync"
override val description = "получить команды с сервера"
override val args = listOf(CommandArgType.NONE)
override val type = CommandType.SIMPLE
override fun execute(args: List<String>, humanBeing: HumanBeing?): String {
val initialRequest = Request(
commandName = "sync",
humanBeing = null,
jwt = client.getToken(),
)
val res = client.sendRequest(initialRequest)
if (res == null) {
return ("[!] Сервер недоступен. Попробуйте позже.")
} else {
res.commands?.forEach { serverCommand ->
invoker.register(
ServerCommand(
name = serverCommand.name,
description = serverCommand.description,
type = serverCommand.type,
args = serverCommand.args,
networkManager = client
)
)
}
return ("| Подключен. | Введите 'help' для справки.")
}
}
}
@@ -0,0 +1,174 @@
package input
import models.Car
import models.Coordinates
import models.HumanBeing
import models.WeaponType
import network.NetworkManager
import java.util.UUID
/**
* Интерактивный построитель объекта [HumanBeing].
*
* Читает поля по одному из [inputManager], показывает приглашения,
* валидирует ввод и повторяет запрос при ошибке.
*
* Поля [HumanBeing.id] и [HumanBeing.creationDate] генерируются
* автоматически и не запрашиваются у пользователя.
*
* @property inputManager источник ввода
*/
class HumanBeingBuilder(
private val inputManager: IOManager,
private val networkManager: NetworkManager
) {
/**
* Запускает интерактивный ввод всех полей и возвращает готовый [HumanBeing].
*
* @throws IllegalStateException если EOF
*/
fun build(): HumanBeing {
val name = readNonBlankString("Введите имя:")
val coordinates = readCoordinates()
val realHero = readBoolean("Герой? (true/false):")
val hasToothpick = readBoolean("Есть зубочистка? (true/false):")
val impactSpeed = readDouble("Введите скорость удара (impactSpeed):")
val soundtrackName = readNonBlankString("Введите название саундтрека:")
val minutesOfWaiting = readFloat("Введите минуты ожидания (minutesOfWaiting):")
val weaponType = readNullableWeaponType()
val car = readCar()
return HumanBeing(
name = name, coordinates = coordinates, realHero = realHero,
hasToothpick = hasToothpick, impactSpeed = impactSpeed,
soundtrackName = soundtrackName, minutesOfWaiting = minutesOfWaiting,
weaponType = weaponType, car = car, ownerId = networkManager.getUUID()
)
}
private fun readCoordinates(): Coordinates {
val x = readLong("Введите координату X (тип Long):")
val y = readInt("Введите координату Y (тип Int):")
return Coordinates(x, y)
}
private fun readCar(): Car {
val cool = readBoolean("Крутой автомобиль ? (true/false):")
return Car(cool)
}
/**
* Читает непустую строку. Повторяет запрос при пустом вводе.
*/
private fun readNonBlankString(prompt: String): String {
while (true) {
val input = inputManager.readLine(" > $prompt")
?: throw IllegalStateException("EOF")
if (input.isNotBlank()) return input.trim()
inputManager.print("[Ошибка] Строка не может быть пустой.")
}
}
/**
* Читает Long. Повторяет запрос при некорректном вводе.
*/
private fun readLong(prompt: String): Long {
while (true) {
val input = inputManager.readLine(" > $prompt")
?: throw IllegalStateException("EOF")
try {
return input.trim().toLong()
} catch (e: NumberFormatException) {
inputManager.print("[Ошибка] Ожидается целое число (Long). Попробуйте снова.")
}
}
}
/**
* Читает Int. Повторяет запрос при некорректном вводе.
*/
private fun readInt(prompt: String): Int {
while (true) {
val input = inputManager.readLine(" > $prompt")
?: throw IllegalStateException("EOF")
try {
return input.trim().toInt()
} catch (e: NumberFormatException) {
inputManager.print("[Ошибка] Ожидается целое число (Int). Попробуйте снова.")
}
}
}
/**
* Читает Double. Повторяет запрос при некорректном вводе.
*/
private fun readDouble(prompt: String): Double {
while (true) {
val input = inputManager.readLine(" > $prompt")
?: throw IllegalStateException("EOF")
try {
return input.trim().toDouble()
} catch (e: NumberFormatException) {
inputManager.print("[Ошибка] Ожидается число с плавающей точкой (Double). Попробуйте снова.")
}
}
}
/**
* Читает Float. Повторяет запрос при некорректном вводе.
*/
private fun readFloat(prompt: String): Float {
while (true) {
val input = inputManager.readLine(" > $prompt")
?: throw IllegalStateException("EOF")
try {
return input.trim().toFloat()
} catch (e: NumberFormatException) {
inputManager.print("[Ошибка] Ожидается число (Float). Попробуйте снова.")
}
}
}
/**
* Читает Boolean (true/false). Повторяет запрос при некорректном вводе.
*/
private fun readBoolean(prompt: String): Boolean {
while (true) {
val input = inputManager.readLine(" > $prompt")
?: throw IllegalStateException("EOF")
when (input.trim().lowercase()) {
"true" -> return true
"false" -> return false
else -> inputManager.print("[Ошибка] Ожидается 'true' или 'false'. Попробуйте снова.")
}
}
}
/**
* Читает [WeaponType] или null (пустая строка).
* Перед вводом выводит список допустимых констант.
*/
private fun readNullableWeaponType(): WeaponType? {
val constants = WeaponType.entries.joinToString(", ")
while (true) {
val input = inputManager.readLine(" > Введите тип оружия (доступные: $constants) или оставьте пустым для null:")
?: throw IllegalStateException("EOF")
if (input.isBlank()) return null
try {
return WeaponType.valueOf(input.trim().uppercase())
} catch (e: IllegalArgumentException) {
inputManager.print("[Ошибка] Неизвестный тип оружия '$input'. Допустимые значения: $constants")
}
}
}
/**
* Выводит приглашение к вводу только в интерактивном режиме.
* В режиме скрипта приглашения не выводятся.
*/
private fun printPrompt(message: String) {
if (inputManager.isInteractive) inputManager.print(" > $message ")
}
}
+76
View File
@@ -0,0 +1,76 @@
package input
import org.jline.reader.EndOfFileException
import org.jline.reader.LineReader
import org.jline.reader.LineReaderBuilder
import org.jline.reader.UserInterruptException
import org.jline.reader.impl.history.DefaultHistory
import org.jline.terminal.TerminalBuilder
import java.io.File
import java.io.FileNotFoundException
import java.util.ArrayDeque
import java.util.Scanner
class IOManager {
val isInteractive: Boolean = true
private val terminal = TerminalBuilder.builder()
.system(true)
.build()
private val history = DefaultHistory()
private val lineReader: LineReader = LineReaderBuilder.builder()
.terminal(terminal)
.history(history)
// .completer(StringsCompleter(invoker.getCommandNames()))
.option(LineReader.Option.HISTORY_IGNORE_DUPS, true)
.option(LineReader.Option.DISABLE_EVENT_EXPANSION, true)
.build()
private val scriptQueue = ArrayDeque<Scanner>()
private val fileHistory = ArrayDeque<String>()
/**
* Читает строку.
* Если есть активный скрипт — читает из него.
* Иначе — интерактивный ввод через JLine.
*/
fun readLine(prompt: String): String? {
// Режим скрипта
while (scriptQueue.isNotEmpty()) {
val scanner = scriptQueue.peek()
if (scanner.hasNextLine()) {
return scanner.nextLine()
}
scanner.close()
scriptQueue.pop()
fileHistory.pop()
}
return try {
lineReader.readLine(prompt)
} catch (e: UserInterruptException) {
""
} catch (e: EndOfFileException) {
null
}
}
fun addScriptScanner(filePath: String) {
if (fileHistory.contains(filePath)) {
printErrConsole("Скрипт уже был выполнен: $filePath")
return
}
try {
fileHistory.push(filePath)
scriptQueue.push(Scanner(File(filePath), Charsets.UTF_8))
} catch (e: FileNotFoundException) {
printErrConsole("Файл не найден: $filePath")
}
}
fun print(text: String) = println(text)
fun printErrConsole(text: String) = System.err.println(text)
}
@@ -0,0 +1,158 @@
package network
import kotlinx.serialization.SerializationException
import java.net.InetSocketAddress
import java.net.PortUnreachableException
import java.nio.ByteBuffer
import java.nio.channels.DatagramChannel
import java.util.UUID
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 var token: String = "no token"
private var id: String = " no id"
private var channel: DatagramChannel = openChannel()
private fun openChannel(): DatagramChannel =
DatagramChannel.open().also {
it.configureBlocking(false)
it.connect(InetSocketAddress(host, port))
}
fun sendRequest(authRequest: AuthRequest): AuthResponse? {
val requestBytes = AppJson.encodeToString(AuthRequest.serializer(), authRequest)
.toByteArray(Charsets.UTF_8)
repeat(maxRetries) { attempt ->
try {
channel.write(ByteBuffer.wrap(requestBytes))
val response = waitForAuthResponse()
if (response != null) return response
} catch (e: PortUnreachableException) {
reconnect()
} catch (e: Exception) {
reconnect()
}
println(
"[Client] Сервер не отвечает " +
"(попытка ${attempt + 1}/$maxRetries). " +
"Повтор через ${retryDelayMS / 1000} сек..."
)
Thread.sleep(retryDelayMS)
}
println("[Client] Сервер недоступен. Вход не выполнен.")
return null
}
fun sendRequest(request: Request): Response? {
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
} catch (e: PortUnreachableException) {
reconnect()
} catch (e: Exception) {
reconnect()
}
println(
"[Client] Сервер не отвечает " +
"(попытка ${attempt + 1}/$maxRetries). " +
"Повтор через ${retryDelayMS / 1000} сек..."
)
Thread.sleep(retryDelayMS)
}
println("[Client] Сервер недоступен. Команда не выполнена.")
return null
}
private fun waitForResponse(): Response? {
val buf = ByteBuffer.allocate(bufferSize)
val deadline = System.currentTimeMillis() + timeoutMS
while (System.currentTimeMillis() < deadline) {
try {
buf.clear()
if (channel.read(buf) > 0) {
buf.flip()
return AppJson.decodeFromString<Response>(
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
}
private fun waitForAuthResponse(): AuthResponse? {
val buf = ByteBuffer.allocate(bufferSize)
val deadline = System.currentTimeMillis() + timeoutMS
while (System.currentTimeMillis() < deadline) {
try {
buf.clear()
if (channel.read(buf) > 0) {
buf.flip()
return AppJson.decodeFromString<AuthResponse>(
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
}
private fun reconnect() {
try { channel.close() } catch (_: Exception) {}
channel = openChannel()
}
fun close() {
try { channel.close() } catch (_: Exception) {}
}
fun setToken(newToken: String) {
token = newToken
}
fun getToken(): String = token
fun clearToken() {
token = "no token"
}
fun setUUID(newToken: String) {
id = newToken
}
fun getUUID(): UUID {
return UUID.fromString(id)
}
}
@@ -0,0 +1,56 @@
package runner
import commands.ClientCommand
import commands.CommandType
import input.HumanBeingBuilder
import input.IOManager
import network.NetworkManager
class CommandInvoker(
private val io: IOManager,
private val networkManager: NetworkManager
) {
private val commands = mutableMapOf<String, ClientCommand>()
private val history = ArrayDeque<String>(12)
fun register(command: ClientCommand) {
commands[command.name] = command
}
fun execute(input: String) {
val parts = input.trim().split(" ")
val name = parts[0].lowercase().trim()
val args = parts.drop(1)
val command = commands[name]
if (command == null) {
io.print("Неизвестная команда: '$name'. Введите 'help' для справки.")
return
}
addToHistory(name)
try {
if (command.type == CommandType.MODEL) {
val model = HumanBeingBuilder(io, networkManager).build()
val res = command.execute(args, model)
io.print(res)
} else {
val res = command.execute(args)
io.print(res)
}
} catch (e: IllegalStateException) {
io.print("[Ошибка выполнения] ${e.message}")
}
}
fun getHistory(): List<String> = history.toList()
fun getCommands(): Map<String, ClientCommand> = commands.toMap()
private fun addToHistory(name: String) {
if (history.size >= 12) history.removeFirst()
history.addLast(name)
}
fun getCommandNames(): List<String> = commands.keys.toList()
}