Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update UI allow selection #16

Merged
merged 3 commits into from
Jul 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions Readme.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
## Teflon ##

Teflon is a desktop chat application.
Teflon is a peer to peer desktop chat application built using Java Swing.

#### Network Interfaces ####

This program uses heuristics to automatically select an appropriate network interface.
If there are multiple viable interfaces detected, a warning will be printed to the log.

There is a dialog to select an appropriate pair of network interface and bind address.
Other instances of the program that are bound to the same address on the same LAN should be able to communicate directly.
#### Multicast Groups ####

There are two reference addresses, one IPv6 and one IPV4.
The candidate IPv6 address is a "Link-Local Scope Multicast Addresses" range as it begins with FF02.
The candidate IPv4 address is a class D address specifically in the designated multicast range (224.0.0.0 - 239.255.255.255).
This program will attempt to use IPv6 and fallback to IPv4 when establishing a multicast membership.
If you would like to use a different multicast address that is supported also.

### Related ###

* https://www.iana.org/assignments/ipv6-multicast-addresses/ipv6-multicast-addresses.xhtml
* https://en.wikipedia.org/wiki/Link-local_address#IPv6
* https://www.rfc-editor.org/rfc/rfc3171
* https://www.rfc-editor.org/rfc/rfc3171
* https://en.wikipedia.org/wiki/Swing_(Java)
46 changes: 26 additions & 20 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@

<groupId>name.maxdeliso</groupId>
<artifactId>teflon</artifactId>
<version>1.0.12</version>
<version>1.1.0</version>
<packaging>jar</packaging>
<name>teflon</name>
<url>https://maxdeliso.name</url>
<url>https://github.com/maxdeliso/teflon</url>

<properties>
<java.version>21</java.version>
<java.target>21</java.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<maven.compiler.target>${java.target}</maven.compiler.target>
</properties>

<dependencies>
Expand Down Expand Up @@ -43,7 +44,7 @@
<version>3.11.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<target>${java.target}</target>
</configuration>
</plugin>
<plugin>
Expand All @@ -55,26 +56,33 @@
<goals>
<goal>single</goal>
</goals>
<configuration>
<archive>
<manifest>
<mainClass>name.maxdeliso.teflon.Main</mainClass>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
<manifestEntries>
<Implementation-Version>${project.version}</Implementation-Version>
</manifestEntries>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</execution>
</executions>
<configuration>
<archive>
<manifest>
<mainClass>name.maxdeliso.teflon.Main</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<outputDirectory>
${project.build.directory}/modules
</outputDirectory>
<outputDirectory>${project.build.directory}/modules</outputDirectory>
<archive>
<manifestEntries>
<Implementation-Version>${project.version}</Implementation-Version>
</manifestEntries>
</archive>
</configuration>
</plugin>
<plugin>
Expand All @@ -88,9 +96,7 @@
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>
${project.build.directory}/modules
</outputDirectory>
<outputDirectory>${project.build.directory}/modules</outputDirectory>
<includeScope>runtime</includeScope>
</configuration>
</execution>
Expand Down
127 changes: 30 additions & 97 deletions src/main/java/name/maxdeliso/teflon/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,117 +5,50 @@
import name.maxdeliso.teflon.data.JsonMessageMarshaller;
import name.maxdeliso.teflon.data.Message;
import name.maxdeliso.teflon.data.MessageMarshaller;
import name.maxdeliso.teflon.net.InterfaceChooser;
import name.maxdeliso.teflon.net.NetSelector;
import name.maxdeliso.teflon.net.ConnectionManager;
import name.maxdeliso.teflon.net.NetworkInterfaceManager;
import name.maxdeliso.teflon.ui.MainFrame;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import javax.swing.JFrame;
import javax.swing.JOptionPane;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.TransferQueue;
import java.util.concurrent.*;

import static java.util.UUID.randomUUID;

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

private static final Gson GSON = new GsonBuilder().create();

private static final MessageMarshaller MESSAGE_MARSHALLER = new JsonMessageMarshaller(GSON);

private static final String MULTICAST_IPV6_BIND_ADDRESS = "FF02::77";

private static final String MULTICAST_IPV4_BIND_ADDRESS = "224.0.0.122";

private static final int UDP_PORT = 1337;

private static final int BUFFER_LENGTH = 4096;

private static final TransferQueue<Message> TRANSFER_QUEUE = new LinkedTransferQueue<>();

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();

private static final CompletableFuture<MainFrame> MAIN_FRAME_F =
CompletableFuture
.supplyAsync(() -> new MainFrame(UUID, TRANSFER_QUEUE::add))
.thenApply(mainFrame -> {
mainFrame.setVisible(true);
return mainFrame;
});

private static CompletableFuture<List<InetAddress>> lookupAddressesAsync(String... addressLiterals) {
return CompletableFuture
.supplyAsync(() -> {
List<InetAddress> results = new ArrayList<>();

for (String address : addressLiterals) {
try {
results.add(InetAddress.getByName(address));
} catch (UnknownHostException uhe) {
LOG.warn("failed to get configured address by name: {}", address, uhe);
}
}

if (results.isEmpty()) {
throw new RuntimeException("failed to lookup any of the supplied addresses: " +
String.join(", ", addressLiterals));
}

return results;
});
}
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();

public static void main(String[] args) {
try {
LOG.info("starting up with UUID {}", UUID);

lookupAddressesAsync(MULTICAST_IPV6_BIND_ADDRESS, MULTICAST_IPV4_BIND_ADDRESS)
.thenCompose(addresses -> MAIN_FRAME_F
.thenCompose(mainFrame ->
CompletableFuture.supplyAsync(() -> new NetSelector(
UDP_PORT,
BUFFER_LENGTH,
addresses,
new InterfaceChooser().queryInterfaces(),
(_address, bb) -> MESSAGE_MARSHALLER
.bufferToMessage(bb)
.ifPresent(mainFrame::queueMessageDisplay),
() -> Optional
.ofNullable(TRANSFER_QUEUE.poll())
.map(MESSAGE_MARSHALLER::messageToBuffer)
.orElse(null)
)
).thenCompose(NetSelector::selectLoop)))
.get();
} catch (InterruptedException ie) {
LOG.error("synchronization error", ie);
System.exit(1);
} catch (ExecutionException ee) {
LOG.error("error", ee.getCause());

var dialogFrame = new JFrame();
JOptionPane.showMessageDialog(
dialogFrame,
"A fatal error has occurred and logs have been written to "
+ Paths.get(".").toAbsolutePath().normalize().toString()
+ " : "
+ ee.getCause().getMessage()
+ ".",
"Error",
JOptionPane.ERROR_MESSAGE);
dialogFrame.dispose();
System.exit(2);
}
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();
}
} catch (InterruptedException e) {
netExecutor.shutdownNow();
} finally {
LOG.info("net executor shutdown complete");
}
}));

LOG.info("main thread joining");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;

import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
Expand Down
64 changes: 64 additions & 0 deletions src/main/java/name/maxdeliso/teflon/net/ConnectionManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package name.maxdeliso.teflon.net;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.IOException;
import java.net.*;
import java.nio.channels.DatagramChannel;
import java.util.concurrent.CompletableFuture;

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

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);
}
}));
}

private CompletableFuture<DatagramChannel> openBind(NetworkInterface iface, int udpPort, InetAddress addr) {
return CompletableFuture.supplyAsync(() -> {
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);
}
});
}

private ProtocolFamily reflectProtocolFamily(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());
}
}
}
52 changes: 52 additions & 0 deletions src/main/java/name/maxdeliso/teflon/net/ConnectionResult.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package name.maxdeliso.teflon.net;

import java.nio.channels.DatagramChannel;
import java.nio.channels.MembershipKey;
import java.util.Objects;

public class ConnectionResult {
private final int port;
private final DatagramChannel dc;

public DatagramChannel getDc() {
return dc;
}

public int getPort() {
return port;
}

public MembershipKey getMembershipKey() {
return membershipKey;
}

private final MembershipKey membershipKey;

public ConnectionResult(int port,
DatagramChannel dc,
MembershipKey mk) {
this.port = port;
this.dc = dc;
this.membershipKey = mk;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ConnectionResult that = (ConnectionResult) o;
return port == that.port && Objects.equals(dc, that.dc) && Objects.equals(membershipKey, that.membershipKey);
}

@Override
public int hashCode() {
return Objects.hash(port, dc, membershipKey);
}

@Override
public String toString() {
return "{" +
membershipKey + " " + port +
'}';
}
}
Loading
Loading