From 1260371d40a75ea7e6108d8e7c3683d85e746ee1 Mon Sep 17 00:00:00 2001 From: Felipe <139709626+FelipeKobra@users.noreply.github.com> Date: Tue, 14 Jan 2025 00:45:22 -0300 Subject: [PATCH] - Fix showing terminal buffered messages a lot of times on closing the server - No more Exceptions when exiting of the application via `CTRL + C` - Use GraalVM instead of Launch4j for creating Windows native images. Launch4j made necessary for the user to have JRE - Add JLine in bundles for better compatibility with the native image build - Change Deprecated JLine Jansi to JNI Version --- .idea/artifacts/ChatClient_jar.xml | 2 +- .idea/artifacts/ChatServer_jar.xml | 2 +- .idea/inspectionProfiles/Project_Default.xml | 10 ++ CHANGELOG.MD | 13 ++ .../{CLIENT => artifacts/client}/MANIFEST.MF | 0 .../{SERVER => artifacts/server}/MANIFEST.MF | 0 README.md | 25 +++- TODO.md | 7 +- pom.xml | 127 +++++++++--------- src/main/org/gladiator/app/client/Client.java | 23 +--- .../gladiator/app/client/ClientConfig.java | 7 +- .../org/gladiator/app/client/ClientMain.java | 1 + src/main/org/gladiator/app/server/Server.java | 33 ++--- .../server/config/ServerConfigFactory.java | 3 +- src/main/org/gladiator/app/util/Chat.java | 8 +- .../native-image/reachability-metadata.json | 7 + .../native-image/resource-config.json | 9 ++ 17 files changed, 163 insertions(+), 114 deletions(-) create mode 100644 .idea/inspectionProfiles/Project_Default.xml rename META-INF/{CLIENT => artifacts/client}/MANIFEST.MF (100%) rename META-INF/{SERVER => artifacts/server}/MANIFEST.MF (100%) create mode 100644 src/main/resources/META-INF/native-image/reachability-metadata.json create mode 100644 src/main/resources/META-INF/native-image/resource-config.json diff --git a/.idea/artifacts/ChatClient_jar.xml b/.idea/artifacts/ChatClient_jar.xml index e7ec94f..3ad88d1 100644 --- a/.idea/artifacts/ChatClient_jar.xml +++ b/.idea/artifacts/ChatClient_jar.xml @@ -3,7 +3,7 @@ $PROJECT_DIR$/out/artifacts/ChatClient_jar - + diff --git a/.idea/artifacts/ChatServer_jar.xml b/.idea/artifacts/ChatServer_jar.xml index 11ce362..de42a92 100644 --- a/.idea/artifacts/ChatServer_jar.xml +++ b/.idea/artifacts/ChatServer_jar.xml @@ -3,7 +3,7 @@ $PROJECT_DIR$/out/artifacts/ChatServer_jar - + diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..58ad3a5 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/CHANGELOG.MD b/CHANGELOG.MD index dc0cd53..441eb19 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -1,5 +1,18 @@ # Change Log +## [0.0.3](https://github.com/FelipeKobra/JMessenger/tree/v0.0.3) - To Release + +### Enhancements + +- Use GraalVM instead of Launch4j for creating Windows native images. Launch4j made necessary for the user to have JRE +- Added JLine in bundles for better compatibility with the native image build +- Change Deprecated JLine Jansi to JNI Version + +### Bug Fixes + +- Fixed showing terminal buffered messages a lot of times on closing the server +- No more Exceptions when exiting of the application via `CTRL + C` + ## [0.0.2](https://github.com/FelipeKobra/JMessenger/tree/v0.0.2) - January 12, 2025 ### Enhancements diff --git a/META-INF/CLIENT/MANIFEST.MF b/META-INF/artifacts/client/MANIFEST.MF similarity index 100% rename from META-INF/CLIENT/MANIFEST.MF rename to META-INF/artifacts/client/MANIFEST.MF diff --git a/META-INF/SERVER/MANIFEST.MF b/META-INF/artifacts/server/MANIFEST.MF similarity index 100% rename from META-INF/SERVER/MANIFEST.MF rename to META-INF/artifacts/server/MANIFEST.MF diff --git a/README.md b/README.md index 73dac15..0548a0c 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,15 @@

command

JMessenger

-**JMessenger** is a terminal-based chat application that allows you to communicate seamlessly with others over a -network. It supports both client and server functionalities, making it easy to set up and use. +**JMessenger** is a terminal-based chat application that allows you to communicate seamlessly with others over +the internet. It supports both client and server functionalities, making it easy to set up and use. ## Table of Contents - [Notes](#notes) - [Installation](#installation) - [Usage](#usage) + - [UTF-8 Support](#utf-8-support) - [Windows Executable](#windows-executable) - [JAR](#jar) - [Building](#building) @@ -34,6 +35,24 @@ network. It supports both client and server functionalities, making it easy to s ## Usage +### UTF-8 Support + +For being able to see emojis and different symbols on your terminal, you need to check it's encoding. For enabling +the UTF-8 Charset on the current terminal instance check some tips + +- Powershell + +```powershell +$OutputEncoding = [Console]::InputEncoding = [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding +``` + +- Bash + +```bash +export LANG=en_US.UTF-8 +export LC_ALL=en_US.UTF-8 +``` + ### Windows Executable Download the executable (`.exe`) from the [Releases](https://github.com/FelipeKobra/JavaTerminalChat/releases) section. @@ -100,6 +119,8 @@ To build the artifacts, run this command in the root folder: ## Requirements for Developers - JDK 21 or higher +- GraalVM JDK 21 or Higher +- Powershell ## Contributing diff --git a/TODO.md b/TODO.md index 4ef4e21..8f8e0f1 100644 --- a/TODO.md +++ b/TODO.md @@ -4,10 +4,6 @@ - [ ] Create communication with cryptography -## Configuration - -- [ ] Add a file for server config - ## QOL - [ ] Add colors to the terminal @@ -16,6 +12,9 @@ ## Basic - [ ] Notify clients when someone connects/disconnects +- [ ] Better warning when server not found on client instead of throwing an exception +- [ ] Make user terminal automatically support UTF8 Characters +- [x] Disable Debug on native-image ## Administration diff --git a/pom.xml b/pom.xml index 411641b..6e28e9b 100644 --- a/pom.xml +++ b/pom.xml @@ -16,12 +16,15 @@ org.gladiator - + 21 21 UTF-8 ${project.build.directory}/artifacts + + 3.28.0 + 0.10.4 @@ -88,79 +91,51 @@ - - + + - com.akathist.maven.plugins.launch4j - launch4j-maven-plugin - 2.5.2 + org.graalvm.buildtools + native-maven-plugin + ${native.maven.plugin.version} + false + + target/artifacts + true + false + false + false + true + + -J-Dfile.encoding=UTF-8 + + - - executable-server-main - package + build-native-server - launch4j + compile-no-fork + package - src/main/resources/images/server.ico - console - ${artifacts.output.directory}/Server.exe - ${artifacts.output.directory}/Server.jar - ${project.artifactId} - - %JAVA_HOME%;%PATH% - false - false - - - 0.${project.version} - ${project.version} - Server executable for ${project.artifactId} - ${project.groupId} - 0.${project.version} - ${project.version - ${project.artifactId} Server - ${project.artifactId} Server - Server.exe - + Server + app.server.ServerMain - - executable-client-main - package + build-native-client - launch4j + compile-no-fork + package - src/main/resources/images/client.ico - console - ${artifacts.output.directory}/Client.exe - ${artifacts.output.directory}/Client.jar - ${project.artifactId} - - %JAVA_HOME%;%PATH% - false - false - - - 0.${project.version} - ${project.version} - Client executable for ${project.artifactId} - ${project.groupId} - 0.${project.version} - ${project.version - ${project.artifactId} Client - ${project.artifactId} Client - Client.exe - + Client + app.client.ClientMain - + org.codehaus.mojo exec-maven-plugin @@ -203,17 +178,41 @@ 1.0.2 - + - org.fusesource.jansi - jansi - 2.4.1 + org.jline + jline-terminal + ${jline.bundle.version} + + + + org.jline + jline-terminal-jni + ${jline.bundle.version} + + + + org.jline + jline-native + ${jline.bundle.version} + + + + org.jline + jline-console + ${jline.bundle.version} + + + + org.jline + jline-style + ${jline.bundle.version} - + org.jline - jline - 3.28.0 + jline-reader + ${jline.bundle.version} diff --git a/src/main/org/gladiator/app/client/Client.java b/src/main/org/gladiator/app/client/Client.java index f2873ee..6707224 100644 --- a/src/main/org/gladiator/app/client/Client.java +++ b/src/main/org/gladiator/app/client/Client.java @@ -63,7 +63,7 @@ public static Logger getLogger() { public void run() { final AsyncSocketIO socketIO = AsyncSocketIO.getAsyncSocketIO(socket, executor); - final var reader = new BufferedReader(new InputStreamReader(socketIO.getInputStream())); + final var reader = new BufferedReader(new InputStreamReader(socketIO.getInputStream(), StandardCharsets.UTF_8)); final var writer = new PrintWriter(socketIO.getOutputStream(), true, StandardCharsets.UTF_8); String serverName = exchangeNames(writer, reader); @@ -79,10 +79,7 @@ private CompletableFuture receiveMessages(BufferedReader reader) { return CompletableFuture.runAsync(() -> { try { reader.lines() - .map(msg -> { - msg = new String(msg.getBytes(), StandardCharsets.UTF_8); - return ConnectionMessage.fromRawString(msg); - }) + .map(ConnectionMessage::fromRawString) .forEach(msg -> { chat.showConnectionMessage(msg); chat.showUserPrompt(); @@ -102,18 +99,13 @@ private CompletableFuture sendMessages(PrintWriter writer) { try { while ((line = SingletonTerminal.TERMINAL.getLineReader().readLine(chat.userPrompt() + " ")) != null) { if (line.equals("quit")) { - throw new UserInterruptException("User typed `quit`"); + break; } String msg = ConnectionMessage.toRawString(config.getName(), line); writer.println(msg); } } catch (EndOfFileException | UserInterruptException e) { - LOGGER.debug("User stopped the console reading, probably by pressing CTRL + C"); - try { - socket.close(); - } catch (IOException ex) { - LOGGER.error("Error during socket closing after shutdown of sending message: {}", e, e); - } + LOGGER.debug("User stopped the console reading, probably by pressing CTRL + C", e); } finally { executor.shutdownNow(); } @@ -150,12 +142,11 @@ private void reconnectPrompt() { try { String choice = SingletonTerminal.TERMINAL.getLineReader().readLine("Connect to another server? y/N: "); if (!choice.equalsIgnoreCase("Y")) { - chat.prettyPrint("Chat Ended"); - isRunning.set(false); + throw new UserInterruptException("Reconnection choice not made"); } - } catch (UserInterruptException | EndOfFileException e) { - LOGGER.debug("Reconnection choice not made: {}", e.getMessage()); + isRunning.set(false); + chat.prettyPrint("Chat Ended"); } } diff --git a/src/main/org/gladiator/app/client/ClientConfig.java b/src/main/org/gladiator/app/client/ClientConfig.java index 27be84e..3e2aac5 100644 --- a/src/main/org/gladiator/app/client/ClientConfig.java +++ b/src/main/org/gladiator/app/client/ClientConfig.java @@ -3,7 +3,6 @@ import app.util.SingletonTerminal; import environment.Port; import org.apache.commons.lang3.Validate; -import org.jline.reader.UserInterruptException; public class ClientConfig { private final String name; @@ -24,7 +23,7 @@ public static ClientConfig createClientConfig() { return new ClientConfig(clientName, serverAddress, serverPort); } - private static String receiveName() throws UserInterruptException { + private static String receiveName() { String name = ""; while (name.isBlank()) { name = SingletonTerminal.TERMINAL.getLineReader().readLine("Choose your name: "); @@ -32,11 +31,11 @@ private static String receiveName() throws UserInterruptException { return name; } - private static String receiveAddress() throws UserInterruptException { + private static String receiveAddress() { return SingletonTerminal.TERMINAL.getLineReader().readLine("Type the Server IP (default = localhost): "); } - private static int receivePort() throws UserInterruptException { + private static int receivePort() { int serverPort; try { String serverPortString = SingletonTerminal.TERMINAL.getLineReader().readLine("Type the server port (default = " + Port.PORT_DEFAULT + "): "); diff --git a/src/main/org/gladiator/app/client/ClientMain.java b/src/main/org/gladiator/app/client/ClientMain.java index 3b16dfe..817dbf7 100644 --- a/src/main/org/gladiator/app/client/ClientMain.java +++ b/src/main/org/gladiator/app/client/ClientMain.java @@ -13,6 +13,7 @@ public static void main(final String... args) { try (Client client = Client.createClient(isRunning)) { client.run(); } catch (UserInterruptException e) { + isRunning.set(false); Server.getLogger().debug("User didn't finished typing during a line read, probably by pressing CTRL+C", e); } } diff --git a/src/main/org/gladiator/app/server/Server.java b/src/main/org/gladiator/app/server/Server.java index 139c83f..e09fff8 100644 --- a/src/main/org/gladiator/app/server/Server.java +++ b/src/main/org/gladiator/app/server/Server.java @@ -26,21 +26,22 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Predicate; public final class Server implements AutoCloseable { private static final Logger LOGGER = LoggerFactory.getLogger(Server.class.getName()); + private final List clientConnections = new CopyOnWriteArrayList<>(); + private final AtomicBoolean isClosingManually = new AtomicBoolean(false); - private final List clientConnections; private final ServerConfig serverConfig; private final ServerSocket serverSocket; private final ExecutorService executor; private final Chat chat; private final PortMapper portMapper; - private Server(List clientConnections, ServerConfig serverConfig, - ServerSocket serverSocket, ExecutorService executor, Chat chat, PortMapper portMapper) { - this.clientConnections = clientConnections; + private Server(ServerConfig serverConfig, ServerSocket serverSocket, + ExecutorService executor, Chat chat, PortMapper portMapper) { this.serverConfig = serverConfig; this.serverSocket = serverSocket; this.executor = executor; @@ -55,7 +56,6 @@ public static Logger getLogger() { public static Server createServer() throws UserInterruptException { Server server = null; try { - List clientConnections = new CopyOnWriteArrayList<>(); ServerConfig serverConfig = new ServerConfigFactory().create(); ServerSocket serverSocket = ServerSocketFactory.getDefault().createServerSocket(serverConfig.port()); ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); @@ -64,7 +64,7 @@ public static Server createServer() throws UserInterruptException { PortMapper portMapper = PortMapper.createDefault(); portMapper.openPort(serverConfig.port()); - server = new Server(clientConnections, serverConfig, serverSocket, executor, chat, portMapper); + server = new Server(serverConfig, serverSocket, executor, chat, portMapper); } catch (BindException e) { LOGGER.error("Address already in use, check if you have another server opened in the same port"); } catch (IOException e) { @@ -91,7 +91,7 @@ private CompletableFuture listenToConnections() { final AsyncSocketIO asyncSocketIO = AsyncSocketIO.getAsyncSocketIO(clientSocket, executor); - final var reader = new BufferedReader(new InputStreamReader(asyncSocketIO.getInputStream())); + final var reader = new BufferedReader(new InputStreamReader(asyncSocketIO.getInputStream(), StandardCharsets.UTF_8)); final var writer = new PrintWriter(asyncSocketIO.getOutputStream(), true, StandardCharsets.UTF_8); String clientName = exchangeNames(writer, reader); @@ -113,7 +113,7 @@ private CompletableFuture listenToConnections() { private CompletableFuture broadcastToConnections() { LOGGER.info("Broadcasting to connections..."); - + System.out.println("Type `quit` to exit"); return CompletableFuture.runAsync(() -> { String line; @@ -145,10 +145,7 @@ private void receiveMessages(BufferedReader reader, Connection clientConnection) executor.execute(() -> { try { reader.lines() - .map(msg -> { - msg = new String(msg.getBytes(), StandardCharsets.UTF_8); - return ConnectionMessage.fromRawString(msg); - }) + .map(ConnectionMessage::fromRawString) .forEach(msg -> { chat.showConnectionMessage(msg); chat.showBufferedMessage(SingletonTerminal.TERMINAL.getLineReader().getBuffer().toString()); @@ -162,10 +159,12 @@ private void receiveMessages(BufferedReader reader, Connection clientConnection) } catch (UncheckedIOException ignored) { LOGGER.debug("Connection with {} ended abruptly", clientConnection.name()); } finally { - LOGGER.info("User Disconnected: {}", clientConnection.name()); - chat.showBufferedMessage(SingletonTerminal.TERMINAL.getLineReader().getBuffer().toString()); - clientConnection.close(); - clientConnections.remove(clientConnection); + if (!isClosingManually.get()) { + LOGGER.info("User Disconnected: {}", clientConnection.name()); + chat.showBufferedMessage(SingletonTerminal.TERMINAL.getLineReader().getBuffer().toString()); + clientConnection.close(); + clientConnections.remove(clientConnection); + } } }); } @@ -203,6 +202,8 @@ private String exchangeNames(PrintWriter writer, BufferedReader reader) { @Override public void close() throws IOException { LOGGER.info("Closing all connections..."); + + isClosingManually.set(true); final var futures = clientConnections.stream().map(connection -> CompletableFuture.runAsync(connection::close)).toArray(CompletableFuture[]::new); CompletableFuture.allOf(futures).join(); diff --git a/src/main/org/gladiator/app/server/config/ServerConfigFactory.java b/src/main/org/gladiator/app/server/config/ServerConfigFactory.java index aad2949..e1908bd 100644 --- a/src/main/org/gladiator/app/server/config/ServerConfigFactory.java +++ b/src/main/org/gladiator/app/server/config/ServerConfigFactory.java @@ -5,12 +5,11 @@ import environment.Port; import org.apache.commons.lang3.Validate; import org.jline.reader.LineReader; -import org.jline.reader.UserInterruptException; public class ServerConfigFactory { - public ServerConfig create() throws UserInterruptException { + public ServerConfig create() { ServerConfig serverConfig; String isCustom; diff --git a/src/main/org/gladiator/app/util/Chat.java b/src/main/org/gladiator/app/util/Chat.java index 88796d9..aca6de1 100644 --- a/src/main/org/gladiator/app/util/Chat.java +++ b/src/main/org/gladiator/app/util/Chat.java @@ -9,10 +9,6 @@ public Chat(String userPrompt) { this.userPrompt = userPrompt; } - public void cleanLine() { - System.out.print("\r\033[K"); - } - public void prettyPrint(String str) { String division = "=".repeat(str.length()); System.out.println("\n" + division); @@ -20,6 +16,10 @@ public void prettyPrint(String str) { System.out.println(division + "\n"); } + public void cleanLine() { + System.out.print("\r\033[K"); + } + public void showUserPrompt() { System.out.print(userPrompt + " "); } diff --git a/src/main/resources/META-INF/native-image/reachability-metadata.json b/src/main/resources/META-INF/native-image/reachability-metadata.json new file mode 100644 index 0000000..52eaf38 --- /dev/null +++ b/src/main/resources/META-INF/native-image/reachability-metadata.json @@ -0,0 +1,7 @@ +{ + "resources": [ + { + "glob": "**/logback.xml" + } + ] +} \ No newline at end of file diff --git a/src/main/resources/META-INF/native-image/resource-config.json b/src/main/resources/META-INF/native-image/resource-config.json new file mode 100644 index 0000000..fb3f8d2 --- /dev/null +++ b/src/main/resources/META-INF/native-image/resource-config.json @@ -0,0 +1,9 @@ +{ + "resources": { + "includes": [ + { + "pattern": "logback\\.xml" + } + ] + } +} \ No newline at end of file