Skip to content

Commit

Permalink
adds logo, adds username colors, safety improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
maxdeliso committed Jan 20, 2025
1 parent a314864 commit 1906c6d
Show file tree
Hide file tree
Showing 15 changed files with 474 additions and 244 deletions.
8 changes: 6 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@
<artifactId>log4j-core</artifactId>
<version>3.0.0-alpha1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.13.0</version>
</dependency>
</dependencies>

<build>
Expand All @@ -43,8 +48,7 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.target}</target>
<release>${java.version}</release>
</configuration>
</plugin>
<plugin>
Expand Down
1 change: 1 addition & 0 deletions src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
requires com.google.gson;
requires org.apache.logging.log4j;
requires java.desktop;
requires org.apache.commons.text;
opens name.maxdeliso.teflon.data;
}
79 changes: 48 additions & 31 deletions src/main/java/name/maxdeliso/teflon/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,44 +11,61 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import javax.swing.SwingUtilities;
import java.util.UUID;
import java.util.concurrent.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TransferQueue;

import static java.util.UUID.randomUUID;

public class Main {
private static final Logger LOG = LogManager.getLogger(Main.class);
private static final Gson GSON = new GsonBuilder().create();
public static final String MULTICAST_IPV6_BIND_ADDRESS = "FF02::77";
public static final String MULTICAST_IPV4_BIND_ADDRESS = "224.0.0.122";
public static final int DEFAULT_UDP_PORT = 1337;
public static final int BUFFER_LENGTH = 4096;
public static final TransferQueue<Message> TRANSFER_QUEUE = new LinkedTransferQueue<>();
private static final Logger LOG = LogManager.getLogger(Main.class);
private static final Gson GSON = new GsonBuilder().create();
public static final MessageMarshaller MESSAGE_MARSHALLER = new JsonMessageMarshaller(GSON);
private static final UUID INSTANCE_ID = randomUUID();
private static final NetworkInterfaceManager INTERFACE_MANAGER = new NetworkInterfaceManager();
private static final ConnectionManager CONNECTION_MANAGER = new ConnectionManager();

public static final String MULTICAST_IPV6_BIND_ADDRESS = "FF02::77";
public static final String MULTICAST_IPV4_BIND_ADDRESS = "224.0.0.122";
public static final int DEFAULT_UDP_PORT = 1337;
public static final int BUFFER_LENGTH = 4096;
private static final UUID UUID = randomUUID();
public static void main(String[] args) {
var netExecutor = Executors.newSingleThreadExecutor();
SwingUtilities.invokeLater(() -> {
var mainFrame = new MainFrame(
INSTANCE_ID,
TRANSFER_QUEUE::add,
netExecutor,
CONNECTION_MANAGER,
INTERFACE_MANAGER
);
mainFrame.setVisible(true);
});

public static final MessageMarshaller MESSAGE_MARSHALLER = new JsonMessageMarshaller(GSON);
public static final TransferQueue<Message> TRANSFER_QUEUE = new LinkedTransferQueue<>();
private static final NetworkInterfaceManager nim = new NetworkInterfaceManager();
private static final ConnectionManager cm = new ConnectionManager();
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
shutdownExecutor(netExecutor);
}));
}

public static void main(String[] args) {
final ExecutorService netExecutor = Executors.newSingleThreadExecutor();
var mainFrame = new MainFrame(UUID, TRANSFER_QUEUE::add, netExecutor, cm, nim);
mainFrame.setVisible(true);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
netExecutor.shutdown();
LOG.info("net executor shutdown initiated...");
if (!netExecutor.awaitTermination(1, TimeUnit.SECONDS)) {
netExecutor.shutdownNow();
private static void shutdownExecutor(ExecutorService executorService) {
try {
executorService.shutdown();
LOG.info("Shutting down netExecutor...");
if (!executorService.awaitTermination(2, TimeUnit.SECONDS)) {
LOG.warn("Forcing netExecutor shutdownNow() after timeout.");
executorService.shutdownNow();
}
} catch (InterruptedException e) {
LOG.warn("Shutdown interrupted, forcing netExecutor shutdown.");
executorService.shutdownNow();
Thread.currentThread().interrupt(); // Restore interrupt status
} finally {
LOG.info("netExecutor shutdown complete.");
}
} catch (InterruptedException e) {
netExecutor.shutdownNow();
} finally {
LOG.info("net executor shutdown complete");
}
}));

LOG.info("main thread joining");
}
}
}
34 changes: 17 additions & 17 deletions src/main/java/name/maxdeliso/teflon/data/JsonMessageMarshaller.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,26 @@

public class JsonMessageMarshaller implements MessageMarshaller {

private static final Charset MESSAGE_CHARSET = StandardCharsets.UTF_8;
private static final Charset MESSAGE_CHARSET = StandardCharsets.UTF_8;

private final Gson gson;
private final Gson gson;

public JsonMessageMarshaller(Gson gson) {
this.gson = gson;
}
public JsonMessageMarshaller(Gson gson) {
this.gson = gson;
}

@Override
public Optional<Message> bufferToMessage(final ByteBuffer bb) {
try {
var buffer = MESSAGE_CHARSET.decode(bb).toString();
return Optional.ofNullable(gson.fromJson(buffer, Message.class));
} catch (JsonSyntaxException exc) {
return Optional.empty();
@Override
public Optional<Message> bufferToMessage(final ByteBuffer bb) {
try {
var buffer = MESSAGE_CHARSET.decode(bb).toString();
return Optional.ofNullable(gson.fromJson(buffer, Message.class));
} catch (JsonSyntaxException exc) {
return Optional.empty();
}
}
}

@Override
public ByteBuffer messageToBuffer(final Message message) {
return ByteBuffer.wrap(gson.toJson(message).getBytes(MESSAGE_CHARSET));
}
@Override
public ByteBuffer messageToBuffer(final Message message) {
return ByteBuffer.wrap(gson.toJson(message).getBytes(MESSAGE_CHARSET));
}
}
49 changes: 21 additions & 28 deletions src/main/java/name/maxdeliso/teflon/data/Message.java
Original file line number Diff line number Diff line change
@@ -1,35 +1,28 @@
package name.maxdeliso.teflon.data;

/**
* A simple message class with a sender id and a string body.
*/
public final class Message {
private static final String MESSAGE_SEPARATOR = " >> ";
import java.util.UUID;

private final String senderId;
private final String body;
import static org.apache.commons.text.StringEscapeUtils.escapeHtml4;

public Message() {
this.senderId = "";
this.body = "";
// NOTE: for Gson
}
public record Message(String senderId, String body) {
public boolean isValidSenderId() {
try {
UUID.fromString(senderId);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}

public Message(String senderId, String body) {
this.senderId = senderId;
this.body = body;
}
public String generateColor() {
if (!isValidSenderId()) {
throw new IllegalArgumentException("Invalid UUID format for senderId: " + senderId);
}
int colorInt = senderId.hashCode() & 0xFFFFFF;
return String.format("#%06X", colorInt);
}

public String senderId() {
return this.senderId;
}

private String body() {
return this.body;
}

@Override
public String toString() {
return String.join(MESSAGE_SEPARATOR, senderId(), body());
}
public String htmlSafeBody() {
return escapeHtml4(body);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import java.util.Optional;

public interface MessageMarshaller {
Optional<Message> bufferToMessage(final ByteBuffer bb);
Optional<Message> bufferToMessage(final ByteBuffer bb);

ByteBuffer messageToBuffer(final Message message);
ByteBuffer messageToBuffer(final Message message);
}
99 changes: 67 additions & 32 deletions src/main/java/name/maxdeliso/teflon/net/ConnectionManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,61 +4,96 @@
import org.apache.logging.log4j.Logger;

import java.io.IOException;
import java.net.*;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.ProtocolFamily;
import java.net.StandardProtocolFamily;
import java.net.StandardSocketOptions;
import java.net.UnknownHostException;
import java.nio.channels.DatagramChannel;
import java.nio.channels.MembershipKey;
import java.nio.channels.UnsupportedAddressTypeException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;

public class ConnectionManager {
private static final Logger LOG = LogManager.getLogger(ConnectionManager.class);

/**
* Asynchronously connects to the given multicast address and port on
* the specified network interface, returning a ConnectionResult.
*/
public CompletableFuture<ConnectionResult> connectMulticast(
String ipAddress,
int port,
NetworkInterface networkInterface
) {
return CompletableFuture.supplyAsync(() -> {
try {
var addr = InetAddress.getByName(ipAddress);
if (!addr.isMulticastAddress()) {
throw new IllegalArgumentException(addr + " must be a multicast address");
}
return addr;
} catch (UnknownHostException uhe) {
throw new RuntimeException(uhe);
}
}).thenCompose(inetAddress -> openBind(networkInterface, port, inetAddress).thenApply(datagramChannel -> {
try {
var key = datagramChannel.join(inetAddress, networkInterface);
return new ConnectionResult(port, datagramChannel, key);
} catch (IOException e) {
throw new RuntimeException(e);
var inetAddress = resolveMulticastAddress(ipAddress);
var datagramChannel = openAndBindChannel(inetAddress, port, networkInterface);
var membershipKey = joinGroup(datagramChannel, inetAddress, networkInterface);

return new ConnectionResult(port, datagramChannel, membershipKey);
});
}

private InetAddress resolveMulticastAddress(String ipAddress) {
try {
var address = InetAddress.getByName(ipAddress);
if (!address.isMulticastAddress()) {
throw new IllegalArgumentException(address + " must be a multicast address");
}
}));
return address;
} catch (UnknownHostException e) {
throw new CompletionException("Failed to resolve host: " + ipAddress, e);
}
}

private CompletableFuture<DatagramChannel> openBind(NetworkInterface iface, int udpPort, InetAddress addr) {
return CompletableFuture.supplyAsync(() -> {
private DatagramChannel openAndBindChannel(InetAddress addr, int port, NetworkInterface netIf) {
try {
var family = protocolFamilyForAddress(addr);
var dc = DatagramChannel.open(family);
dc.setOption(StandardSocketOptions.SO_REUSEADDR, true);
dc.setOption(StandardSocketOptions.IP_MULTICAST_IF, netIf);
dc.configureBlocking(false);
dc.bind(new InetSocketAddress(port));
return dc;
} catch (IOException e) {
String msg = String.format("Failed to open or bind channel for %s on interface %s", addr, netIf);
LOG.error(msg, e);
throw new CompletionException(msg, e);
}
}

private MembershipKey joinGroup(DatagramChannel channel, InetAddress groupAddr, NetworkInterface netIf) {
try {
return channel.join(groupAddr, netIf);
} catch (IOException e) {
String msg = String.format("Failed to join multicast group %s on interface %s", groupAddr, netIf);
LOG.error(msg, e);

try {
final var dc = DatagramChannel.open(reflectProtocolFamily(addr));
dc.setOption(StandardSocketOptions.IP_MULTICAST_IF, iface);
dc.setOption(StandardSocketOptions.SO_REUSEADDR, true);
dc.configureBlocking(false);
return dc.bind(new InetSocketAddress(udpPort));
} catch (IOException ioe) {
LOG.warn("failed to join {} {}", addr, iface, ioe);
throw new RuntimeException(ioe);
channel.close();
} catch (IOException closeEx) {
LOG.debug("Error closing channel after join failure", closeEx);
}
});

throw new CompletionException(msg, e);
}
}

private ProtocolFamily reflectProtocolFamily(InetAddress inetAddress) {
private ProtocolFamily protocolFamilyForAddress(InetAddress inetAddress) {
if (inetAddress instanceof Inet4Address) {
return StandardProtocolFamily.INET;
} else if (inetAddress instanceof Inet6Address) {
return StandardProtocolFamily.INET6;
} else {
LOG.error("invalid candidate address with unrecognized type: {}", inetAddress);
throw new RuntimeException("invalid address type: " + inetAddress.getClass());
String msg = "Unrecognized address type: " + inetAddress.getClass();
LOG.error(msg);
throw new UnsupportedAddressTypeException();
}
}
}
}
Loading

0 comments on commit 1906c6d

Please sign in to comment.