Prevents starting a second instance of the GUI and forwards

main-method-arguments to the running instance. Command line arguments
are treated by showing the corresponding folder in the GUI.

If an argument is a folder, it is shown directly. If an argument is a
.masterkey.json file, the parent directory is shown. If an argument does
not exist, but the folder can be created, the newly created folder is
shown.

It was necessary to move the main function away from the MainApplication
class because running the main method of a class, which extends the
javafx Application class, will start a non-daemon thread. This prevents
the VM from exiting naturally.

OSX needs its own mechanism, which is implemented in OS-specific code.
It is vital that the required handler is added in the main thread of the
application, not the Java FX thread, which is a bit awkward to
implement. Since it is possible to open .cryptomator packages on OSX,
this extension is now hidden in the folder list.
This commit is contained in:
Tillmann Gaida
2015-01-21 17:35:25 +01:00
parent ecf29a91b8
commit 0cfc3fb7f7
12 changed files with 963 additions and 12 deletions
@@ -0,0 +1,45 @@
package org.cryptomator.ui.util;
import static org.junit.Assert.*;
import java.util.Iterator;
import java.util.concurrent.ConcurrentSkipListMap;
import org.junit.Test;
public class ListenerRegistryTest {
/**
* This test looks at how concurrent modifications affect the iterator of a
* {@link ConcurrentSkipListMap}. It shows that concurrent modifications
* work just fine, however the state of the iterator including the next
* value are advanced during retrieval of a value, so it's not possible to
* remove the next value.
*
* @throws Exception
*/
@Test
public void testConcurrentSkipListMap() throws Exception {
ConcurrentSkipListMap<Integer, Integer> map = new ConcurrentSkipListMap<>();
map.put(1, 1);
map.put(2, 2);
map.put(3, 3);
map.put(4, 4);
map.put(5, 5);
final Iterator<Integer> iterator = map.values().iterator();
assertTrue(iterator.hasNext());
assertEquals((Integer) 1, iterator.next());
map.remove(2);
assertTrue(iterator.hasNext());
// iterator returns 2 anyway.
assertEquals((Integer) 2, iterator.next());
assertTrue(iterator.hasNext());
map.remove(4);
assertEquals((Integer) 3, iterator.next());
assertTrue(iterator.hasNext());
// this time we removed 4 before retrieving 3, so it is skipped.
assertEquals((Integer) 5, iterator.next());
}
}
@@ -0,0 +1,193 @@
package org.cryptomator.ui.util;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.TimeUnit;
import org.cryptomator.ui.util.SingleInstanceManager.LocalInstance;
import org.cryptomator.ui.util.SingleInstanceManager.MessageListener;
import org.cryptomator.ui.util.SingleInstanceManager.RemoteInstance;
import org.junit.Test;
public class SingleInstanceManagerTest {
@Test(timeout = 1000)
public void testTryFillTimeout() throws Exception {
try (final ServerSocket socket = new ServerSocket(0)) {
// we need to asynchronously accept the connection
final ForkJoinTask<?> forked = ForkJoinTask.adapt(() -> {
try {
socket.setSoTimeout(1000);
socket.accept();
} catch (Exception e) {
throw new RuntimeException(e);
}
}).fork();
try (SocketChannel channel = SocketChannel.open()) {
channel.configureBlocking(false);
channel.connect(new InetSocketAddress(InetAddress.getLoopbackAddress(), socket.getLocalPort()));
TimeoutTask.attempt(t -> channel.finishConnect(), 100, 1);
final ByteBuffer buffer = ByteBuffer.allocate(1);
SingleInstanceManager.tryFill(channel, buffer, 100);
assertTrue(buffer.hasRemaining());
}
forked.join();
}
}
@Test(timeout = 1000)
public void testTryFill() throws Exception {
try (final ServerSocket socket = new ServerSocket(0)) {
// we need to asynchronously accept the connection
final ForkJoinTask<?> forked = ForkJoinTask.adapt(() -> {
try {
socket.setSoTimeout(1000);
socket.accept().getOutputStream().write(1);
} catch (Exception e) {
throw new RuntimeException(e);
}
}).fork();
try (SocketChannel channel = SocketChannel.open()) {
channel.configureBlocking(false);
channel.connect(new InetSocketAddress(InetAddress.getLoopbackAddress(), socket.getLocalPort()));
TimeoutTask.attempt(t -> channel.finishConnect(), 100, 1);
final ByteBuffer buffer = ByteBuffer.allocate(1);
SingleInstanceManager.tryFill(channel, buffer, 100);
assertFalse(buffer.hasRemaining());
}
forked.join();
}
}
String appKey = "APPKEY";
@Test
public void testOneMessage() throws Exception {
ExecutorService exec = Executors.newCachedThreadPool();
try {
final LocalInstance server = SingleInstanceManager.startLocalInstance(appKey, exec);
final Optional<RemoteInstance> r = SingleInstanceManager.getRemoteInstance(appKey);
CountDownLatch latch = new CountDownLatch(1);
final MessageListener listener = spy(new MessageListener() {
@Override
public void handleMessage(String message) {
latch.countDown();
}
});
server.registerListener(listener);
assertTrue(r.isPresent());
String message = "Is this thing on?";
assertTrue(r.get().sendMessage(message, 100));
System.out.println("wrote message");
latch.await(10, TimeUnit.SECONDS);
verify(listener).handleMessage(message);
} finally {
exec.shutdownNow();
}
}
@Test(timeout = 60000)
public void testALotOfMessages() throws Exception {
final int connectors = 256;
final int messagesPerConnector = 256;
ExecutorService exec = Executors.newSingleThreadExecutor();
ExecutorService exec2 = Executors.newFixedThreadPool(16);
try (final LocalInstance server = SingleInstanceManager.startLocalInstance(appKey, exec)) {
Set<String> sentMessages = new ConcurrentSkipListSet<>();
Set<String> receivedMessages = new HashSet<>();
CountDownLatch sendLatch = new CountDownLatch(connectors);
CountDownLatch receiveLatch = new CountDownLatch(connectors * messagesPerConnector);
server.registerListener(message -> {
receivedMessages.add(message);
receiveLatch.countDown();
});
Set<RemoteInstance> instances = Collections.synchronizedSet(new HashSet<>());
for (int i = 0; i < connectors; i++) {
exec2.submit(() -> {
try {
final Optional<RemoteInstance> r = SingleInstanceManager.getRemoteInstance(appKey);
assertTrue(r.isPresent());
instances.add(r.get());
for (int j = 0; j < messagesPerConnector; j++) {
exec2.submit(() -> {
try {
for (;;) {
final String message = UUID.randomUUID().toString();
if (!sentMessages.add(message)) {
continue;
}
r.get().sendMessage(message, 100);
break;
}
} catch (Exception e) {
e.printStackTrace();
}
});
}
sendLatch.countDown();
} catch (Throwable e) {
e.printStackTrace();
}
});
}
assertTrue(sendLatch.await(1, TimeUnit.MINUTES));
exec2.shutdown();
assertTrue(exec2.awaitTermination(1, TimeUnit.MINUTES));
assertTrue(receiveLatch.await(1, TimeUnit.MINUTES));
assertEquals(sentMessages, receivedMessages);
for (RemoteInstance remoteInstance : instances) {
try {
remoteInstance.close();
} catch (Exception e) {
e.printStackTrace();
}
}
} finally {
exec.shutdownNow();
exec2.shutdownNow();
}
}
}