Browse Source

Use UNIX Sockets for IPC

Sebastian Stenzel 3 years ago
parent
commit
755eb70ae8
27 changed files with 606 additions and 380 deletions
  1. 1 1
      .idea/runConfigurations/Cryptomator_Linux.xml
  2. 1 1
      .idea/runConfigurations/Cryptomator_Linux_Dev.xml
  3. 1 1
      .idea/runConfigurations/Cryptomator_Windows.xml
  4. 1 1
      .idea/runConfigurations/Cryptomator_Windows_Dev.xml
  5. 1 1
      .idea/runConfigurations/Cryptomator_macOS.xml
  6. 1 1
      .idea/runConfigurations/Cryptomator_macOS_Dev.xml
  7. 3 3
      src/main/java/org/cryptomator/common/Environment.java
  8. 63 0
      src/main/java/org/cryptomator/ipc/Client.java
  9. 30 0
      src/main/java/org/cryptomator/ipc/HandleLaunchArgsMessage.java
  10. 73 0
      src/main/java/org/cryptomator/ipc/IpcCommunicator.java
  11. 68 0
      src/main/java/org/cryptomator/ipc/IpcMessage.java
  12. 19 0
      src/main/java/org/cryptomator/ipc/IpcMessageListener.java
  13. 50 0
      src/main/java/org/cryptomator/ipc/LoopbackCommunicator.java
  14. 20 0
      src/main/java/org/cryptomator/ipc/RevealRunningAppMessage.java
  15. 81 0
      src/main/java/org/cryptomator/ipc/Server.java
  16. 18 10
      src/main/java/org/cryptomator/launcher/Cryptomator.java
  17. 4 3
      src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java
  18. 0 258
      src/main/java/org/cryptomator/launcher/IpcFactory.java
  19. 7 6
      src/main/java/org/cryptomator/launcher/IpcProtocolImpl.java
  20. 0 17
      src/main/java/org/cryptomator/launcher/IpcProtocol.java
  21. 1 3
      src/test/java/org/cryptomator/common/EnvironmentTest.java
  22. 46 0
      src/test/java/org/cryptomator/ipc/HandleLaunchArgsMessageTest.java
  23. 46 0
      src/test/java/org/cryptomator/ipc/IpcCommunicatorTest.java
  24. 37 0
      src/test/java/org/cryptomator/ipc/LoopbackCommunicatorTest.java
  25. 30 0
      src/test/java/org/cryptomator/ipc/RevealRunningAppMessageTest.java
  26. 4 3
      src/test/java/org/cryptomator/launcher/FileOpenRequestHandlerTest.java
  27. 0 71
      src/test/java/org/cryptomator/launcher/IpcFactoryTest.java

+ 1 - 1
.idea/runConfigurations/Cryptomator_Linux.xml

@@ -2,7 +2,7 @@
   <configuration default="false" name="Cryptomator Linux" type="Application" factoryName="Application">
     <option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
     <module name="cryptomator" />
-    <option name="VM_PARAMETERS" value="-Djdk.gtk.version=2 -Duser.language=en -Dcryptomator.settingsPath=&quot;~/.config/Cryptomator/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/.config/Cryptomator/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/.local/share/Cryptomator/logs&quot; -Dcryptomator.mountPointsDir=&quot;~/.local/share/Cryptomator/mnt&quot; -Dcryptomator.showTrayIcon=true -Xss20m -Xmx512m" />
+    <option name="VM_PARAMETERS" value="-Djdk.gtk.version=2 -Duser.language=en -Dcryptomator.settingsPath=&quot;~/.config/Cryptomator/settings.json&quot; -Dcryptomator.ipcSocketPath=&quot;~/.config/Cryptomator/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/.local/share/Cryptomator/logs&quot; -Dcryptomator.mountPointsDir=&quot;~/.local/share/Cryptomator/mnt&quot; -Dcryptomator.showTrayIcon=true -Xss20m -Xmx512m" />
     <method v="2">
       <option name="Make" enabled="true" />
     </method>

+ 1 - 1
.idea/runConfigurations/Cryptomator_Linux_Dev.xml

@@ -2,7 +2,7 @@
   <configuration default="false" name="Cryptomator Linux Dev" type="Application" factoryName="Application">
     <option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
     <module name="cryptomator" />
-    <option name="VM_PARAMETERS" value="-Djdk.gtk.version=2 -Duser.language=en -Dcryptomator.settingsPath=&quot;~/.config/Cryptomator-Dev/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/.config/Cryptomator-Dev/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/.local/share/Cryptomator-Dev/logs&quot; -Dcryptomator.mountPointsDir=&quot;~/.local/share/Cryptomator-Dev/mnt&quot; -Dcryptomator.showTrayIcon=true -Dfuse.experimental=&quot;true&quot; -Xss20m -Xmx512m" />
+    <option name="VM_PARAMETERS" value="-Djdk.gtk.version=2 -Duser.language=en -Dcryptomator.settingsPath=&quot;~/.config/Cryptomator-Dev/settings.json&quot; -Dcryptomator.ipcSocketPath=&quot;~/.config/Cryptomator-Dev/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/.local/share/Cryptomator-Dev/logs&quot; -Dcryptomator.mountPointsDir=&quot;~/.local/share/Cryptomator-Dev/mnt&quot; -Dcryptomator.showTrayIcon=true -Dfuse.experimental=&quot;true&quot; -Xss20m -Xmx512m" />
     <method v="2">
       <option name="Make" enabled="true" />
     </method>

+ 1 - 1
.idea/runConfigurations/Cryptomator_Windows.xml

@@ -2,7 +2,7 @@
   <configuration default="false" name="Cryptomator Windows" type="Application" factoryName="Application">
     <option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
     <module name="cryptomator" />
-    <option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath=&quot;~/AppData/Roaming/Cryptomator/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/AppData/Roaming/Cryptomator/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/AppData/Roaming/Cryptomator&quot; -Dcryptomator.keychainPath=&quot;~/AppData/Roaming/Cryptomator/keychain.json&quot; -Dcryptomator.mountPointsDir=&quot;~/Cryptomator&quot; -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m" />
+    <option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath=&quot;~/AppData/Roaming/Cryptomator/settings.json&quot; -Dcryptomator.ipcSocketPath=&quot;~/AppData/Roaming/Cryptomator/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/AppData/Roaming/Cryptomator&quot; -Dcryptomator.keychainPath=&quot;~/AppData/Roaming/Cryptomator/keychain.json&quot; -Dcryptomator.mountPointsDir=&quot;~/Cryptomator&quot; -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m" />
     <method v="2">
       <option name="Make" enabled="true" />
     </method>

File diff suppressed because it is too large
+ 1 - 1
.idea/runConfigurations/Cryptomator_Windows_Dev.xml


+ 1 - 1
.idea/runConfigurations/Cryptomator_macOS.xml

@@ -5,7 +5,7 @@
     </envs>
     <option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
     <module name="cryptomator" />
-    <option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath=&quot;~/Library/Application Support/Cryptomator/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/Library/Application Support/Cryptomator/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/Library/Logs/Cryptomator&quot; -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m -ea" />
+    <option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath=&quot;~/Library/Application Support/Cryptomator/settings.json&quot; -Dcryptomator.ipcSocketPath=&quot;~/Library/Application Support/Cryptomator/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/Library/Logs/Cryptomator&quot; -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m -ea" />
     <method v="2">
       <option name="Make" enabled="true" />
     </method>

+ 1 - 1
.idea/runConfigurations/Cryptomator_macOS_Dev.xml

@@ -5,7 +5,7 @@
     </envs>
     <option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
     <module name="cryptomator" />
-    <option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath=&quot;~/Library/Application Support/Cryptomator-Dev/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/Library/Application Support/Cryptomator-Dev/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/Library/Logs/Cryptomator-Dev&quot; -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m -ea" />
+    <option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath=&quot;~/Library/Application Support/Cryptomator-Dev/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/Library/Application Support/Cryptomator-Dev/ipc.socket&quot; -Dcryptomator.logDir=&quot;~/Library/Logs/Cryptomator-Dev&quot; -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m -ea" />
     <method v="2">
       <option name="Make" enabled="true" />
     </method>

+ 3 - 3
src/main/java/org/cryptomator/common/Environment.java

@@ -33,7 +33,7 @@ public class Environment {
 		LOG.debug("user.region: {}", System.getProperty("user.region"));
 		LOG.debug("logback.configurationFile: {}", System.getProperty("logback.configurationFile"));
 		LOG.debug("cryptomator.settingsPath: {}", System.getProperty("cryptomator.settingsPath"));
-		LOG.debug("cryptomator.ipcPortPath: {}", System.getProperty("cryptomator.ipcPortPath"));
+		LOG.debug("cryptomator.ipcSocketPath: {}", System.getProperty("cryptomator.ipcSocketPath"));
 		LOG.debug("cryptomator.keychainPath: {}", System.getProperty("cryptomator.keychainPath"));
 		LOG.debug("cryptomator.logDir: {}", System.getProperty("cryptomator.logDir"));
 		LOG.debug("cryptomator.mountPointsDir: {}", System.getProperty("cryptomator.mountPointsDir"));
@@ -51,8 +51,8 @@ public class Environment {
 		return getPaths("cryptomator.settingsPath");
 	}
 
-	public Stream<Path> getIpcPortPath() {
-		return getPaths("cryptomator.ipcPortPath");
+	public Stream<Path> ipcSocketPath() {
+		return getPaths("cryptomator.ipcSocketPath");
 	}
 
 	public Stream<Path> getKeychainPath() {

+ 63 - 0
src/main/java/org/cryptomator/ipc/Client.java

@@ -0,0 +1,63 @@
+package org.cryptomator.ipc;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.net.UnixDomainSocketAddress;
+import java.nio.channels.SocketChannel;
+import java.nio.file.Path;
+import java.util.concurrent.Executor;
+
+class Client implements IpcCommunicator {
+
+	private static final Logger LOG = LoggerFactory.getLogger(Client.class);
+
+	private final SocketChannel socketChannel;
+
+	private Client(SocketChannel socketChannel) {
+		this.socketChannel = socketChannel;
+	}
+
+	public static Client create(Path socketPath) throws IOException {
+		var address = UnixDomainSocketAddress.of(socketPath);
+		var socketChannel = SocketChannel.open(address);
+		LOG.info("Connected to IPC server on UNIX socket {}", socketPath);
+		return new Client(socketChannel);
+	}
+
+	@Override
+	public boolean isClient() {
+		return true;
+	}
+
+	@Override
+	public void listen(IpcMessageListener listener, Executor executor) {
+		executor.execute(() -> {
+			try {
+				while (socketChannel.isConnected()) {
+					var msg = IpcMessage.receive(socketChannel);
+					listener.handleMessage(msg);
+				}
+			} catch (IOException e) {
+				LOG.error("Failed to read IPC message", e);
+			}
+		});
+	}
+
+	@Override
+	public void send(IpcMessage message, Executor executor) {
+		executor.execute(() -> {
+			try {
+				message.send(socketChannel);
+			} catch (IOException e) {
+				LOG.error("Failed to send IPC message", e);
+			}
+		});
+	}
+
+	@Override
+	public void close() throws IOException {
+		socketChannel.close();
+	}
+}

+ 30 - 0
src/main/java/org/cryptomator/ipc/HandleLaunchArgsMessage.java

@@ -0,0 +1,30 @@
+package org.cryptomator.ipc;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Splitter;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+record HandleLaunchArgsMessage(List<String> args) implements IpcMessage {
+
+	private static final char DELIMITER = '\n';
+
+	public static HandleLaunchArgsMessage decode(ByteBuffer encoded) {
+		var str = StandardCharsets.UTF_8.decode(encoded).toString();
+		var args = Splitter.on(DELIMITER).omitEmptyStrings().splitToList(str);
+		return new HandleLaunchArgsMessage(args);
+	}
+
+	@Override
+	public MessageType getMessageType() {
+		return MessageType.HANDLE_LAUNCH_ARGS;
+	}
+
+	@Override
+	public ByteBuffer encodePayload() {
+		var str = Joiner.on(DELIMITER).join(args);
+		return StandardCharsets.UTF_8.encode(str);
+	}
+}

+ 73 - 0
src/main/java/org/cryptomator/ipc/IpcCommunicator.java

@@ -0,0 +1,73 @@
+package org.cryptomator.ipc;
+
+import com.google.common.base.Preconditions;
+import com.google.common.util.concurrent.MoreExecutors;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Future;
+
+public interface IpcCommunicator extends Closeable {
+
+	Logger LOG = LoggerFactory.getLogger(IpcCommunicator.class);
+
+	/**
+	 * Attempts to establish a socket connection via one of the given paths.
+	 * <p>
+	 * If no connection to an existing sockets can be established, a new socket is created for the first given path.
+	 * <p>
+	 * If this fails as well, a fallback communicator is returned that allows process-internal communication mocking the API
+	 * that would have been used for IPC.
+	 *
+	 * @param socketPaths The socket path(s)
+	 * @return A communicator object that allows sending and receiving messages
+	 */
+	static IpcCommunicator create(Iterable<Path> socketPaths) {
+		Preconditions.checkArgument(socketPaths.iterator().hasNext(), "socketPaths must contain at least one element");
+		for (var p : socketPaths) {
+			try {
+				return Client.create(p);
+			} catch (IOException e) {
+				// attempt next socket path
+			}
+		}
+		// Didn't get any connection yet? I.e. we're the first app instance, so let's launch a server:
+		try {
+			return Server.create(socketPaths.iterator().next());
+		} catch (IOException e) {
+			LOG.error("Failed to create IPC server using UNIX sockets", e);
+			return new LoopbackCommunicator();
+		}
+	}
+
+	boolean isClient();
+
+	/**
+	 * Listens to incoming messages until the connection gets closed.
+	 *  @param listener The listener that should be notified of incoming messages
+	 * @param executor An executor on which to listen. Listening will block, so you might want to use a background thread.
+	 * @return
+	 */
+	void listen(IpcMessageListener listener, Executor executor);
+
+	/**
+	 * Sends the given message.
+	 *
+	 * @param message The message to send
+	 * @param executor An executor used to send the message. Sending will block, so you might want to use a background thread.
+	 */
+	void send(IpcMessage message, Executor executor);
+
+	default void sendRevealRunningApp() {
+		send(new RevealRunningAppMessage(), MoreExecutors.directExecutor());
+	}
+
+	default void sendHandleLaunchargs(List<String> args) {
+		send(new HandleLaunchArgsMessage(args), MoreExecutors.directExecutor());
+	}
+}

+ 68 - 0
src/main/java/org/cryptomator/ipc/IpcMessage.java

@@ -0,0 +1,68 @@
+package org.cryptomator.ipc;
+
+import org.cryptomator.cryptolib.common.ByteBuffers;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.WritableByteChannel;
+import java.util.function.Function;
+
+// TODO make sealed, remove enum
+interface IpcMessage {
+
+	enum MessageType {
+		REVEAL_RUNNING_APP(RevealRunningAppMessage::decode),
+		HANDLE_LAUNCH_ARGS(HandleLaunchArgsMessage::decode);
+
+		private final Function<ByteBuffer, IpcMessage> decoder;
+
+		MessageType(Function<ByteBuffer, IpcMessage> decoder) {
+			this.decoder = decoder;
+		}
+
+		static MessageType forOrdinal(int ordinal) {
+			try {
+				return values()[ordinal];
+			} catch (IndexOutOfBoundsException e) {
+				throw new IllegalArgumentException("No such message type: " + ordinal, e);
+			}
+		}
+
+		IpcMessage decodePayload(ByteBuffer payload) {
+			return decoder.apply(payload);
+		}
+	}
+
+	MessageType getMessageType();
+
+	ByteBuffer encodePayload();
+
+	static IpcMessage receive(ReadableByteChannel channel) throws IOException {
+		var header = ByteBuffer.allocate(2 * Integer.BYTES);
+		if (ByteBuffers.fill(channel, header) < header.capacity()) {
+			throw new EOFException();
+		}
+		header.flip();
+		int typeNo = header.getInt();
+		int length = header.getInt();
+		MessageType type = MessageType.forOrdinal(typeNo);
+		var payload = ByteBuffer.allocate(length);
+		ByteBuffers.fill(channel, payload);
+		payload.flip();
+		return type.decodePayload(payload);
+	}
+
+	default void send(WritableByteChannel channel) throws IOException {
+		var payload = encodePayload();
+		var buf = ByteBuffer.allocate(2 * Integer.BYTES + payload.remaining());
+		buf.putInt(getMessageType().ordinal()); // message type
+		buf.putInt(payload.remaining()); // message length
+		buf.put(payload); // message
+		buf.flip();
+		while (buf.hasRemaining()) {
+			channel.write(buf);
+		}
+	}
+}

+ 19 - 0
src/main/java/org/cryptomator/ipc/IpcMessageListener.java

@@ -0,0 +1,19 @@
+package org.cryptomator.ipc;
+
+import java.util.List;
+
+public interface IpcMessageListener {
+
+	default void handleMessage(IpcMessage message) {
+		if (message instanceof RevealRunningAppMessage) {
+			revealRunningApp();
+		} else if (message instanceof HandleLaunchArgsMessage m) {
+			handleLaunchArgs(m.args());
+		}
+	}
+
+	void revealRunningApp();
+
+	void handleLaunchArgs(List<String> args);
+
+}

+ 50 - 0
src/main/java/org/cryptomator/ipc/LoopbackCommunicator.java

@@ -0,0 +1,50 @@
+package org.cryptomator.ipc;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.LinkedTransferQueue;
+import java.util.concurrent.TransferQueue;
+
+class LoopbackCommunicator implements IpcCommunicator {
+
+	private static final Logger LOG = LoggerFactory.getLogger(LoopbackCommunicator.class);
+
+	private final TransferQueue<IpcMessage> transferQueue = new LinkedTransferQueue<>();
+
+	@Override
+	public boolean isClient() {
+		return false;
+	}
+
+	@Override
+	public void listen(IpcMessageListener listener, Executor executor) {
+		executor.execute(() -> {
+			try {
+				var msg = transferQueue.take();
+				listener.handleMessage(msg);
+			} catch (InterruptedException e) {
+				LOG.error("Failed to read IPC message", e);
+				Thread.currentThread().interrupt();
+			}
+		});
+	}
+
+	@Override
+	public void send(IpcMessage message, Executor executor) {
+		executor.execute(() -> {
+			try {
+				transferQueue.put(message);
+			} catch (InterruptedException e) {
+				LOG.error("Failed to send IPC message", e);
+				Thread.currentThread().interrupt();
+			}
+		});
+	}
+
+	@Override
+	public void close() {
+		// no-op
+	}
+}

+ 20 - 0
src/main/java/org/cryptomator/ipc/RevealRunningAppMessage.java

@@ -0,0 +1,20 @@
+package org.cryptomator.ipc;
+
+import java.nio.ByteBuffer;
+
+public record RevealRunningAppMessage() implements IpcMessage {
+
+	static RevealRunningAppMessage decode(ByteBuffer ignored) {
+		return new RevealRunningAppMessage();
+	}
+
+	@Override
+	public MessageType getMessageType() {
+		return MessageType.REVEAL_RUNNING_APP;
+	}
+
+	@Override
+	public ByteBuffer encodePayload() {
+		return ByteBuffer.allocate(0);
+	}
+}

+ 81 - 0
src/main/java/org/cryptomator/ipc/Server.java

@@ -0,0 +1,81 @@
+package org.cryptomator.ipc;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.net.StandardProtocolFamily;
+import java.net.UnixDomainSocketAddress;
+import java.nio.channels.AsynchronousCloseException;
+import java.nio.channels.ClosedChannelException;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.Executor;
+
+class Server implements IpcCommunicator {
+
+	private static final Logger LOG = LoggerFactory.getLogger(Server.class);
+
+	private final ServerSocketChannel serverSocketChannel;
+	private final Path socketPath;
+
+	private Server(ServerSocketChannel serverSocketChannel, Path socketPath) {
+		this.serverSocketChannel = serverSocketChannel;
+		this.socketPath = socketPath;
+	}
+
+	public static Server create(Path socketPath) throws IOException {
+		var address = UnixDomainSocketAddress.of(socketPath);
+		var serverSocketChannel = ServerSocketChannel.open(StandardProtocolFamily.UNIX);
+		serverSocketChannel.bind(address);
+		LOG.info("Spawning IPC server listening on UNIX socket {}", socketPath);
+		return new Server(serverSocketChannel, socketPath);
+	}
+
+	@Override
+	public boolean isClient() {
+		return false;
+	}
+
+	@Override
+	public void listen(IpcMessageListener listener, Executor executor) {
+		executor.execute(() -> {
+			while (serverSocketChannel.isOpen()) {
+				try (var ch = serverSocketChannel.accept()) {
+					while (ch.isConnected()) {
+						var msg = IpcMessage.receive(ch);
+						listener.handleMessage(msg);
+					}
+				} catch (AsynchronousCloseException e) {
+					return; // serverSocketChannel closed or listener interrupted
+				} catch (EOFException | ClosedChannelException e) {
+					// continue with next connected client
+				} catch (IOException e) {
+					LOG.error("Failed to read IPC message", e);
+				}
+			}
+		});
+	}
+
+	@Override
+	public void send(IpcMessage message, Executor executor) {
+		executor.execute(() -> {
+			try (var ch = serverSocketChannel.accept()) {
+				message.send(ch);
+			} catch (IOException e) {
+				LOG.error("Failed to send IPC message", e);
+			}
+		});
+	}
+
+	@Override
+	public void close() throws IOException {
+		try {
+			serverSocketChannel.close();
+		} finally {
+			Files.deleteIfExists(socketPath);
+		}
+	}
+}

+ 18 - 10
src/main/java/org/cryptomator/launcher/Cryptomator.java

@@ -6,6 +6,8 @@
 package org.cryptomator.launcher;
 
 import org.apache.commons.lang3.SystemUtils;
+import org.cryptomator.common.Environment;
+import org.cryptomator.ipc.IpcCommunicator;
 import org.cryptomator.logging.DebugMode;
 import org.cryptomator.logging.LoggerConfiguration;
 import org.cryptomator.ui.launcher.UiLauncher;
@@ -16,8 +18,10 @@ import javax.inject.Inject;
 import javax.inject.Named;
 import javax.inject.Singleton;
 import java.io.IOException;
+import java.util.List;
 import java.util.Optional;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
 
 @Singleton
 public class Cryptomator {
@@ -29,16 +33,18 @@ public class Cryptomator {
 
 	private final LoggerConfiguration logConfig;
 	private final DebugMode debugMode;
-	private final IpcFactory ipcFactory;
+	private final Environment env;
+	private final IpcMessageHandler ipcMessageHandler;
 	private final Optional<String> applicationVersion;
 	private final CountDownLatch shutdownLatch;
 	private final UiLauncher uiLauncher;
 
 	@Inject
-	Cryptomator(LoggerConfiguration logConfig, DebugMode debugMode, IpcFactory ipcFactory, @Named("applicationVersion") Optional<String> applicationVersion, @Named("shutdownLatch") CountDownLatch shutdownLatch, UiLauncher uiLauncher) {
+	Cryptomator(LoggerConfiguration logConfig, DebugMode debugMode, Environment env, IpcMessageHandler ipcMessageHandler, @Named("applicationVersion") Optional<String> applicationVersion, @Named("shutdownLatch") CountDownLatch shutdownLatch, UiLauncher uiLauncher) {
 		this.logConfig = logConfig;
 		this.debugMode = debugMode;
-		this.ipcFactory = ipcFactory;
+		this.env = env;
+		this.ipcMessageHandler = ipcMessageHandler;
 		this.applicationVersion = applicationVersion;
 		this.shutdownLatch = shutdownLatch;
 		this.uiLauncher = uiLauncher;
@@ -64,19 +70,21 @@ public class Cryptomator {
 		 * Attempts to create an IPC connection to a running Cryptomator instance and sends it the given args.
 		 * If no external process could be reached, the args will be handled by the loopback IPC endpoint.
 		 */
-		try (IpcFactory.IpcEndpoint endpoint = ipcFactory.create()) {
-			endpoint.getRemote().handleLaunchArgs(args); // if we are the server, getRemote() returns self.
-			if (endpoint.isConnectedToRemote()) {
-				endpoint.getRemote().revealRunningApp();
+		try (var communicator = IpcCommunicator.create(env.ipcSocketPath().toList())) {
+			if (communicator.isClient()) {
+				communicator.sendHandleLaunchargs(List.of(args));
+				communicator.sendRevealRunningApp();
 				LOG.info("Found running application instance. Shutting down...");
 				return 2;
 			} else {
+				ipcMessageHandler.handleLaunchArgs(List.of(args));
+				communicator.listen(ipcMessageHandler, Executors.newSingleThreadExecutor()); // TODO named thread
 				LOG.debug("Did not find running application instance. Launching GUI...");
 				return runGuiApplication();
 			}
-		} catch (IOException e) {
-			LOG.error("Failed to initiate inter-process communication.", e);
-			return runGuiApplication();
+		} catch (Throwable e) {
+			LOG.error("Running application failed", e);
+			return 1;
 		}
 	}
 

+ 4 - 3
src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java

@@ -22,6 +22,7 @@ import java.nio.file.InvalidPathException;
 import java.nio.file.Path;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.BlockingQueue;
 import java.util.stream.Collectors;
@@ -46,13 +47,13 @@ class FileOpenRequestHandler {
 		tryToEnqueueFileOpenRequest(launchEvent);
 	}
 
-	public void handleLaunchArgs(String[] args) {
+	public void handleLaunchArgs(List<String> args) {
 		handleLaunchArgs(FileSystems.getDefault(), args);
 	}
 
 	// visible for testing
-	void handleLaunchArgs(FileSystem fs, String[] args) {
-		Collection<Path> pathsToOpen = Arrays.stream(args).map(str -> {
+	void handleLaunchArgs(FileSystem fs, List<String> args) {
+		Collection<Path> pathsToOpen = args.stream().map(str -> {
 			try {
 				return fs.getPath(str);
 			} catch (InvalidPathException e) {

+ 0 - 258
src/main/java/org/cryptomator/launcher/IpcFactory.java

@@ -1,258 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the accompanying LICENSE file.
- *******************************************************************************/
-package org.cryptomator.launcher;
-
-import com.google.common.io.MoreFiles;
-import org.cryptomator.common.Environment;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.inject.Inject;
-import javax.inject.Singleton;
-import java.io.Closeable;
-import java.io.IOException;
-import java.net.InetAddress;
-import java.net.ServerSocket;
-import java.net.Socket;
-import java.net.SocketException;
-import java.net.UnknownHostException;
-import java.nio.ByteBuffer;
-import java.nio.channels.ReadableByteChannel;
-import java.nio.channels.WritableByteChannel;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.StandardOpenOption;
-import java.rmi.NotBoundException;
-import java.rmi.registry.LocateRegistry;
-import java.rmi.registry.Registry;
-import java.rmi.server.RMIClientSocketFactory;
-import java.rmi.server.RMIServerSocketFactory;
-import java.rmi.server.RMISocketFactory;
-import java.rmi.server.UnicastRemoteObject;
-import java.util.List;
-import java.util.Optional;
-import java.util.stream.Collectors;
-
-/**
- * First running application on a machine opens a server socket. Further processes will connect as clients.
- */
-@Singleton
-class IpcFactory {
-
-	private static final Logger LOG = LoggerFactory.getLogger(IpcFactory.class);
-	private static final String RMI_NAME = "Cryptomator";
-
-	private final List<Path> portFilePaths;
-	private final IpcProtocolImpl ipcHandler;
-
-	@Inject
-	public IpcFactory(Environment env, IpcProtocolImpl ipcHandler) {
-		this.portFilePaths = env.getIpcPortPath().collect(Collectors.toUnmodifiableList());
-		this.ipcHandler = ipcHandler;
-	}
-
-	public IpcEndpoint create() {
-		if (portFilePaths.isEmpty()) {
-			LOG.warn("No IPC port file path specified.");
-			return new SelfEndpoint(ipcHandler);
-		} else {
-			System.setProperty("java.rmi.server.hostname", "localhost");
-			return attemptClientConnection().or(this::createServerEndpoint).orElseGet(() -> new SelfEndpoint(ipcHandler));
-		}
-	}
-
-	private Optional<IpcEndpoint> attemptClientConnection() {
-		for (Path portFilePath : portFilePaths) {
-			try {
-				int port = readPort(portFilePath);
-				LOG.debug("[Client] Connecting to port {}...", port);
-				Registry registry = LocateRegistry.getRegistry("localhost", port, new ClientSocketFactory());
-				IpcProtocol remoteInterface = (IpcProtocol) registry.lookup(RMI_NAME);
-				return Optional.of(new ClientEndpoint(remoteInterface));
-			} catch (NotBoundException | IOException e) {
-				LOG.debug("[Client] Failed to connect.");
-				// continue with next portFilePath...
-			}
-		}
-		return Optional.empty();
-	}
-
-	private int readPort(Path portFilePath) throws IOException {
-		try (ReadableByteChannel ch = Files.newByteChannel(portFilePath, StandardOpenOption.READ)) {
-			LOG.debug("[Client] Reading IPC port from {}", portFilePath);
-			ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES);
-			if (ch.read(buf) == Integer.BYTES) {
-				buf.flip();
-				return buf.getInt();
-			} else {
-				throw new IOException("Invalid IPC port file.");
-			}
-		}
-	}
-
-	private Optional<IpcEndpoint> createServerEndpoint() {
-		assert !portFilePaths.isEmpty();
-		Path portFilePath = portFilePaths.get(0);
-		try {
-			ServerSocket socket = new ServerSocket(0, Byte.MAX_VALUE, InetAddress.getByName("localhost"));
-			RMIClientSocketFactory csf = RMISocketFactory.getDefaultSocketFactory();
-			SingletonServerSocketFactory ssf = new SingletonServerSocketFactory(socket);
-			Registry registry = LocateRegistry.createRegistry(0, csf, ssf);
-			UnicastRemoteObject.exportObject(ipcHandler, 0);
-			registry.rebind(RMI_NAME, ipcHandler);
-			writePort(portFilePath, socket.getLocalPort());
-			return Optional.of(new ServerEndpoint(ipcHandler, socket, registry, portFilePath));
-		} catch (IOException e) {
-			LOG.warn("[Server] Failed to create IPC server.", e);
-			return Optional.empty();
-		}
-	}
-
-	private void writePort(Path portFilePath, int port) throws IOException {
-		ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES);
-		buf.putInt(port);
-		buf.flip();
-		MoreFiles.createParentDirectories(portFilePath);
-		try (WritableByteChannel ch = Files.newByteChannel(portFilePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
-			if (ch.write(buf) != Integer.BYTES) {
-				throw new IOException("Did not write expected number of bytes.");
-			}
-		}
-		LOG.debug("[Server] Wrote IPC port {} to {}", port, portFilePath);
-	}
-
-	interface IpcEndpoint extends Closeable {
-
-		boolean isConnectedToRemote();
-
-		IpcProtocol getRemote();
-
-	}
-
-	static class SelfEndpoint implements IpcEndpoint {
-
-		protected final IpcProtocol remoteObject;
-
-		SelfEndpoint(IpcProtocol remoteObject) {
-			this.remoteObject = remoteObject;
-		}
-
-		@Override
-		public boolean isConnectedToRemote() {
-			return false;
-		}
-
-		@Override
-		public IpcProtocol getRemote() {
-			return remoteObject;
-		}
-
-		@Override
-		public void close() {
-			// no-op
-		}
-	}
-
-	static class ClientEndpoint implements IpcEndpoint {
-
-		private final IpcProtocol remoteInterface;
-
-		public ClientEndpoint(IpcProtocol remoteInterface) {
-			this.remoteInterface = remoteInterface;
-		}
-
-		public IpcProtocol getRemote() {
-			return remoteInterface;
-		}
-
-		@Override
-		public boolean isConnectedToRemote() {
-			return true;
-		}
-
-		@Override
-		public void close() {
-			// no-op
-		}
-
-	}
-
-	class ServerEndpoint extends SelfEndpoint {
-
-		private final ServerSocket socket;
-		private final Registry registry;
-		private final Path portFilePath;
-
-		private ServerEndpoint(IpcProtocol remoteObject, ServerSocket socket, Registry registry, Path portFilePath) {
-			super(remoteObject);
-			this.socket = socket;
-			this.registry = registry;
-			this.portFilePath = portFilePath;
-		}
-
-		@Override
-		public void close() {
-			try {
-				registry.unbind(RMI_NAME);
-				UnicastRemoteObject.unexportObject(remoteObject, true);
-				socket.close();
-				Files.deleteIfExists(portFilePath);
-				LOG.debug("[Server] Shut down");
-			} catch (NotBoundException | IOException e) {
-				LOG.warn("[Server] Error shutting down:", e);
-			}
-		}
-
-	}
-
-	/**
-	 * Always returns the same pre-constructed server socket.
-	 */
-	private static class SingletonServerSocketFactory implements RMIServerSocketFactory {
-
-		private final ServerSocket socket;
-
-		public SingletonServerSocketFactory(ServerSocket socket) {
-			this.socket = socket;
-		}
-
-		@Override
-		public synchronized ServerSocket createServerSocket(int port) throws IOException {
-			if (port != 0) {
-				throw new IllegalArgumentException("This factory doesn't support specific ports.");
-			}
-			return this.socket;
-		}
-
-	}
-
-	/**
-	 * Creates client sockets with short timeouts.
-	 */
-	private static class ClientSocketFactory implements RMIClientSocketFactory {
-
-		@Override
-		public Socket createSocket(String host, int port) throws IOException {
-			return new SocketWithFixedTimeout(host, port, 1000);
-		}
-
-	}
-
-	private static class SocketWithFixedTimeout extends Socket {
-
-		public SocketWithFixedTimeout(String host, int port, int timeoutInMs) throws UnknownHostException, IOException {
-			super(host, port);
-			super.setSoTimeout(timeoutInMs);
-		}
-
-		@Override
-		public synchronized void setSoTimeout(int timeout) throws SocketException {
-			// do nothing, timeout is fixed
-		}
-
-	}
-
-}

+ 7 - 6
src/main/java/org/cryptomator/launcher/IpcProtocolImpl.java

@@ -1,5 +1,6 @@
 package org.cryptomator.launcher;
 
+import org.cryptomator.ipc.IpcMessageListener;
 import org.cryptomator.ui.launcher.AppLaunchEvent;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -7,20 +8,20 @@ import org.slf4j.LoggerFactory;
 import javax.inject.Inject;
 import javax.inject.Named;
 import javax.inject.Singleton;
-import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
 import java.util.concurrent.BlockingQueue;
 
 @Singleton
-class IpcProtocolImpl implements IpcProtocol {
+class IpcMessageHandler implements IpcMessageListener {
 
-	private static final Logger LOG = LoggerFactory.getLogger(IpcProtocolImpl.class);
+	private static final Logger LOG = LoggerFactory.getLogger(IpcMessageHandler.class);
 
 	private final FileOpenRequestHandler fileOpenRequestHandler;
 	private final BlockingQueue<AppLaunchEvent> launchEventQueue;
 
 	@Inject
-	public IpcProtocolImpl(FileOpenRequestHandler fileOpenRequestHandler, @Named("launchEventQueue") BlockingQueue<AppLaunchEvent> launchEventQueue) {
+	public IpcMessageHandler(FileOpenRequestHandler fileOpenRequestHandler, @Named("launchEventQueue") BlockingQueue<AppLaunchEvent> launchEventQueue) {
 		this.fileOpenRequestHandler = fileOpenRequestHandler;
 		this.launchEventQueue = launchEventQueue;
 	}
@@ -31,8 +32,8 @@ class IpcProtocolImpl implements IpcProtocol {
 	}
 
 	@Override
-	public void handleLaunchArgs(String... args) {
-		LOG.debug("Received launch args: {}", Arrays.stream(args).reduce((a, b) -> a + ", " + b).orElse(""));
+	public void handleLaunchArgs(List<String> args) {
+		LOG.debug("Received launch args: {}", args.stream().reduce((a, b) -> a + ", " + b).orElse(""));
 		fileOpenRequestHandler.handleLaunchArgs(args);
 	}
 

+ 0 - 17
src/main/java/org/cryptomator/launcher/IpcProtocol.java

@@ -1,17 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the accompanying LICENSE file.
- *******************************************************************************/
-package org.cryptomator.launcher;
-
-import java.rmi.Remote;
-import java.rmi.RemoteException;
-
-interface IpcProtocol extends Remote {
-
-	void revealRunningApp() throws RemoteException;
-
-	void handleLaunchArgs(String... args) throws RemoteException;
-
-}

+ 1 - 3
src/test/java/org/cryptomator/common/EnvironmentTest.java

@@ -3,7 +3,6 @@ package org.cryptomator.common;
 import org.hamcrest.MatcherAssert;
 import org.hamcrest.Matchers;
 import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
@@ -14,7 +13,6 @@ import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.List;
 import java.util.Optional;
-import java.util.stream.Collectors;
 
 @DisplayName("Environment Variables Test")
 public class EnvironmentTest {
@@ -43,7 +41,7 @@ public class EnvironmentTest {
 	public void testIpcPortPath() {
 		System.setProperty("cryptomator.ipcPortPath", "~/.config/Cryptomator/ipcPort.bin:~/.Cryptomator/ipcPort.bin");
 
-		List<Path> result = env.getIpcPortPath().toList();
+		List<Path> result = env.ipcSocketPath().toList();
 		MatcherAssert.assertThat(result, Matchers.hasSize(2));
 		MatcherAssert.assertThat(result, Matchers.contains(Paths.get("/home/testuser/.config/Cryptomator/ipcPort.bin"), //
 				Paths.get("/home/testuser/.Cryptomator/ipcPort.bin")));

+ 46 - 0
src/test/java/org/cryptomator/ipc/HandleLaunchArgsMessageTest.java

@@ -0,0 +1,46 @@
+package org.cryptomator.ipc;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.IOException;
+import java.nio.channels.FileChannel;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.List;
+
+public class HandleLaunchArgsMessageTest {
+
+	@Test
+	public void testSendAndReceive(@TempDir Path tmpDir) throws IOException {
+		var message = new HandleLaunchArgsMessage(List.of("hello world", "foo bar"));
+
+		var file = tmpDir.resolve("tmp.file");
+		try (var ch = FileChannel.open(file, StandardOpenOption.CREATE_NEW, StandardOpenOption.READ, StandardOpenOption.WRITE)) {
+			message.send(ch);
+			ch.position(0);
+			if (IpcMessage.receive(ch) instanceof HandleLaunchArgsMessage received) {
+				Assertions.assertArrayEquals(message.args().toArray(), received.args().toArray());
+			} else {
+				Assertions.fail("Received message of unexpected class");
+			}
+		}
+	}
+
+	@Test
+	public void testSendAndReceiveEmpty(@TempDir Path tmpDir) throws IOException {
+		var message = new HandleLaunchArgsMessage(List.of());
+
+		var file = tmpDir.resolve("tmp.file");
+		try (var ch = FileChannel.open(file, StandardOpenOption.CREATE_NEW, StandardOpenOption.READ, StandardOpenOption.WRITE)) {
+			message.send(ch);
+			ch.position(0);
+			if (IpcMessage.receive(ch) instanceof HandleLaunchArgsMessage received) {
+				Assertions.assertArrayEquals(message.args().toArray(), received.args().toArray());
+			} else {
+				Assertions.fail("Received message of unexpected class");
+			}
+		}
+	}
+}

+ 46 - 0
src/test/java/org/cryptomator/ipc/IpcCommunicatorTest.java

@@ -0,0 +1,46 @@
+package org.cryptomator.ipc;
+
+import com.google.common.util.concurrent.MoreExecutors;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.Executable;
+import org.junit.jupiter.api.io.TempDir;
+import org.mockito.Mockito;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+
+public class IpcCommunicatorTest {
+
+	@Test
+	public void testSendAndReceive(@TempDir Path tmpDir) throws IOException, InterruptedException {
+		var socketPath = tmpDir.resolve("foo.sock");
+		try (var server = IpcCommunicator.create(List.of(socketPath));
+			 var client = IpcCommunicator.create(List.of(socketPath))) {
+			Assertions.assertNotSame(server, client);
+
+			var cdl = new CountDownLatch(1);
+			var executor = Executors.newSingleThreadExecutor();
+			server.listen(new IpcMessageListener() {
+				@Override
+				public void revealRunningApp() {
+					cdl.countDown();
+				}
+
+				@Override
+				public void handleLaunchArgs(List<String> args) {
+
+				}
+			}, executor);
+			client.sendRevealRunningApp();
+
+			Assertions.assertTimeoutPreemptively(Duration.ofMillis(300), (Executable) cdl::await);
+			executor.shutdown();
+		}
+	}
+
+}

+ 37 - 0
src/test/java/org/cryptomator/ipc/LoopbackCommunicatorTest.java

@@ -0,0 +1,37 @@
+package org.cryptomator.ipc;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.Executable;
+
+import java.time.Duration;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+
+public class LoopbackCommunicatorTest {
+
+	@Test
+	public void testSendAndReceive() {
+		try (var communicator = new LoopbackCommunicator()) {
+			var cdl = new CountDownLatch(1);
+			var executor = Executors.newSingleThreadExecutor();
+			communicator.listen(new IpcMessageListener() {
+				@Override
+				public void revealRunningApp() {
+					cdl.countDown();
+				}
+
+				@Override
+				public void handleLaunchArgs(List<String> args) {
+
+				}
+			}, executor);
+			communicator.sendRevealRunningApp();
+
+			Assertions.assertTimeoutPreemptively(Duration.ofMillis(300), (Executable) cdl::await);
+			executor.shutdown();
+		}
+	}
+
+}

+ 30 - 0
src/test/java/org/cryptomator/ipc/RevealRunningAppMessageTest.java

@@ -0,0 +1,30 @@
+package org.cryptomator.ipc;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.IOException;
+import java.nio.channels.FileChannel;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.List;
+
+public class RevealRunningAppMessageTest {
+
+	@Test
+	public void testSendAndReceive(@TempDir Path tmpDir) throws IOException {
+		var message = new RevealRunningAppMessage();
+
+		var file = tmpDir.resolve("tmp.file");
+		try (var ch = FileChannel.open(file, StandardOpenOption.CREATE_NEW, StandardOpenOption.READ, StandardOpenOption.WRITE)) {
+			message.send(ch);
+			ch.position(0);
+			if (IpcMessage.receive(ch) instanceof RevealRunningAppMessage received) {
+				Assertions.assertNotNull(received);
+			} else {
+				Assertions.fail("Received message of unexpected class");
+			}
+		}
+	}
+}

+ 4 - 3
src/test/java/org/cryptomator/launcher/FileOpenRequestHandlerTest.java

@@ -21,6 +21,7 @@ import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.List;
 import java.util.concurrent.ArrayBlockingQueue;
 import java.util.concurrent.BlockingQueue;
 
@@ -38,7 +39,7 @@ public class FileOpenRequestHandlerTest {
 	@Test
 	@DisplayName("./cryptomator.exe foo bar")
 	public void testOpenArgsWithCorrectPaths() {
-		inTest.handleLaunchArgs(new String[]{"foo", "bar"});
+		inTest.handleLaunchArgs(List.of("foo", "bar"));
 
 		AppLaunchEvent evt = queue.poll();
 		Assertions.assertNotNull(evt);
@@ -51,7 +52,7 @@ public class FileOpenRequestHandlerTest {
 	public void testOpenArgsWithIncorrectPaths() {
 		FileSystem fs = Mockito.mock(FileSystem.class);
 		Mockito.when(fs.getPath("foo")).thenThrow(new InvalidPathException("foo", "foo is not a path"));
-		inTest.handleLaunchArgs(fs, new String[]{"foo"});
+		inTest.handleLaunchArgs(fs, List.of("foo"));
 
 		AppLaunchEvent evt = queue.poll();
 		Assertions.assertNull(evt);
@@ -63,7 +64,7 @@ public class FileOpenRequestHandlerTest {
 		queue.add(new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, Collections.emptyList()));
 		Assumptions.assumeTrue(queue.remainingCapacity() == 0);
 
-		inTest.handleLaunchArgs(new String[]{"foo"});
+		inTest.handleLaunchArgs(List.of("foo"));
 	}
 
 }

+ 0 - 71
src/test/java/org/cryptomator/launcher/IpcFactoryTest.java

@@ -1,71 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the accompanying LICENSE file.
- *******************************************************************************/
-package org.cryptomator.launcher;
-
-import org.cryptomator.common.Environment;
-import org.cryptomator.launcher.IpcFactory.IpcEndpoint;
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.io.TempDir;
-import org.mockito.Mockito;
-
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.stream.Stream;
-
-public class IpcFactoryTest {
-
-	private Environment environment = Mockito.mock(Environment.class);
-	private IpcProtocolImpl protocolHandler = Mockito.mock(IpcProtocolImpl.class);
-
-	@Test
-	@DisplayName("Without IPC port files")
-	public void testNoIpcWithoutPortFile() throws IOException {
-		IpcFactory inTest = new IpcFactory(environment, protocolHandler);
-
-		Mockito.when(environment.getIpcPortPath()).thenReturn(Stream.empty());
-		try (IpcEndpoint endpoint1 = inTest.create()) {
-			Assertions.assertEquals(IpcFactory.SelfEndpoint.class, endpoint1.getClass());
-			Assertions.assertFalse(endpoint1.isConnectedToRemote());
-			Assertions.assertSame(protocolHandler, endpoint1.getRemote());
-			try (IpcEndpoint endpoint2 = inTest.create()) {
-				Assertions.assertEquals(IpcFactory.SelfEndpoint.class, endpoint2.getClass());
-				Assertions.assertNotSame(endpoint1, endpoint2);
-				Assertions.assertFalse(endpoint2.isConnectedToRemote());
-				Assertions.assertSame(protocolHandler, endpoint2.getRemote());
-			}
-		}
-	}
-
-	@Test
-	@DisplayName("Start server and client with port shared via file")
-	public void testInterProcessCommunication(@TempDir Path tmpDir) throws IOException {
-		Path portFile = tmpDir.resolve("testPortFile");
-		Mockito.when(environment.getIpcPortPath()).thenReturn(Stream.of(portFile));
-		IpcFactory inTest = new IpcFactory(environment, protocolHandler);
-
-		Assertions.assertFalse(Files.exists(portFile));
-		try (IpcEndpoint endpoint1 = inTest.create()) {
-			Assertions.assertEquals(IpcFactory.ServerEndpoint.class, endpoint1.getClass());
-			Assertions.assertFalse(endpoint1.isConnectedToRemote());
-			Assertions.assertTrue(Files.exists(portFile));
-			Assertions.assertSame(protocolHandler, endpoint1.getRemote());
-			Mockito.verifyZeroInteractions(protocolHandler);
-			try (IpcEndpoint endpoint2 = inTest.create()) {
-				Assertions.assertEquals(IpcFactory.ClientEndpoint.class, endpoint2.getClass());
-				Assertions.assertNotSame(endpoint1, endpoint2);
-				Assertions.assertTrue(endpoint2.isConnectedToRemote());
-				Assertions.assertNotSame(protocolHandler, endpoint2.getRemote());
-				Mockito.verifyZeroInteractions(protocolHandler);
-				endpoint2.getRemote().handleLaunchArgs(new String[]{"foo"});
-				Mockito.verify(protocolHandler).handleLaunchArgs(new String[]{"foo"});
-			}
-		}
-	}
-
-}