Browse Source

Refactored IPC, fixes #663

Sebastian Stenzel 6 years ago
parent
commit
f1c332f455

+ 3 - 0
.idea/compiler.xml

@@ -29,5 +29,8 @@
         <module name="ui" />
       </profile>
     </annotationProcessing>
+    <bytecodeTargetLevel>
+      <module name="buildkit" target="11" />
+    </bytecodeTargetLevel>
   </component>
 </project>

+ 9 - 0
.idea/modules.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/.idea/Desktop.iml" filepath="$PROJECT_DIR$/.idea/Desktop.iml" />
+      <module fileurl="file://$PROJECT_DIR$/main/buildkit/buildkit.iml" filepath="$PROJECT_DIR$/main/buildkit/buildkit.iml" />
+    </modules>
+  </component>
+</project>

+ 1 - 1
main/commons/src/main/java/org/cryptomator/common/Environment.java

@@ -28,7 +28,7 @@ public class Environment {
 	public Environment() {
 		LOG.debug("cryptomator.settingsPath: {}", System.getProperty("cryptomator.settingsPath"));
 		LOG.debug("cryptomator.ipcPortPath: {}", System.getProperty("cryptomator.ipcPortPath"));
-		LOG.debug("cryptomator.keychainPath: {}", System.getProperty("cryptomator.ipcPortPath"));
+		LOG.debug("cryptomator.keychainPath: {}", System.getProperty("cryptomator.keychainPath"));
 	}
 
 	public Stream<Path> getSettingsPath() {

+ 23 - 51
main/launcher/src/main/java/org/cryptomator/launcher/Cryptomator.java

@@ -13,21 +13,34 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.Arrays;
-import java.util.Optional;
-import java.util.concurrent.ArrayBlockingQueue;
-import java.util.concurrent.BlockingQueue;
 
 public class Cryptomator {
 
 	private static final Logger LOG = LoggerFactory.getLogger(Cryptomator.class);
-	private static final CryptomatorComponent CRYPTOMATOR_COMPONENT = DaggerCryptomatorComponent.create();
-	private static final Path DEFAULT_IPC_PATH = Paths.get(".ipcPort.tmp");
+	private static final CryptomatorComponent CRYPTOMATOR_COMPONENT = DaggerCryptomatorComponent.create(); // DaggerCryptomatorComponent gets generated by Dagger. Run Maven and include target/generated-sources/annotations in your IDE.
 
-	// We need a separate FX Application class.
-	// If org.cryptomator.launcher.Cryptomator simply extended Application, the module system magically kicks in and throws exceptions
+	public static void main(String[] args) {
+		LOG.info("Starting Cryptomator {} on {} {} ({})", CRYPTOMATOR_COMPONENT.applicationVersion().orElse("SNAPSHOT"), SystemUtils.OS_NAME, SystemUtils.OS_VERSION, SystemUtils.OS_ARCH);
+
+		try (IpcFactory.IpcEndpoint endpoint = CRYPTOMATOR_COMPONENT.ipcFactory().create()) {
+			endpoint.getRemote().handleLaunchArgs(args); // if we are the server, getRemote() returns self.
+			if (endpoint.isConnectedToRemote()) {
+				LOG.info("Found running application instance. Shutting down.");
+			} else {
+				CleanShutdownPerformer.registerShutdownHook();
+				Application.launch(MainApp.class, args);
+			}
+		} catch (IOException e) {
+			LOG.error("Failed to initiate inter-process communication.", e);
+			System.exit(2);
+		} catch (Throwable e) {
+			LOG.error("Error during startup", e);
+			System.exit(1);
+		}
+		System.exit(0); // end remaining non-daemon threads.
+	}
+
+	// We need a separate FX Application class, until we can use the module system. See https://stackoverflow.com/q/54756176/4014509
 	public static class MainApp extends Application {
 
 		private Stage primaryStage;
@@ -60,45 +73,4 @@ public class Cryptomator {
 
 	}
 
-	public static void main(String[] args) {
-		LOG.info("Starting Cryptomator {} on {} {} ({})", CRYPTOMATOR_COMPONENT.applicationVersion().orElse("SNAPSHOT"), SystemUtils.OS_NAME, SystemUtils.OS_VERSION, SystemUtils.OS_ARCH);
-
-		FileOpenRequestHandler fileOpenRequestHandler = CRYPTOMATOR_COMPONENT.fileOpenRequestHanlder();
-		Path ipcPortPath = CRYPTOMATOR_COMPONENT.environment().getIpcPortPath().findFirst().orElse(DEFAULT_IPC_PATH);
-		try (InterProcessCommunicator communicator = InterProcessCommunicator.start(ipcPortPath, new IpcProtocolImpl(fileOpenRequestHandler))) {
-			if (communicator.isServer()) {
-				fileOpenRequestHandler.handleLaunchArgs(args);
-				CleanShutdownPerformer.registerShutdownHook();
-				Application.launch(MainApp.class, args);
-			} else {
-				communicator.handleLaunchArgs(args);
-				LOG.info("Found running application instance. Shutting down.");
-			}
-			System.exit(0); // end remaining non-daemon threads.
-		} catch (IOException e) {
-			LOG.error("Failed to initiate inter-process communication.", e);
-			System.exit(2);
-		} catch (Throwable e) {
-			LOG.error("Error during startup", e);
-			System.exit(1);
-		}
-	}
-
-	private static class IpcProtocolImpl implements InterProcessCommunicationProtocol {
-
-		private final FileOpenRequestHandler fileOpenRequestHandler;
-
-		// TODO: inject?
-		public IpcProtocolImpl(FileOpenRequestHandler fileOpenRequestHandler) {
-			this.fileOpenRequestHandler = fileOpenRequestHandler;
-		}
-
-		@Override
-		public void handleLaunchArgs(String[] args) {
-			LOG.info("Received launch args: {}", Arrays.stream(args).reduce((a, b) -> a + ", " + b).orElse(""));
-			fileOpenRequestHandler.handleLaunchArgs(args);
-		}
-
-	}
-
 }

+ 1 - 5
main/launcher/src/main/java/org/cryptomator/launcher/CryptomatorComponent.java

@@ -6,17 +6,13 @@ import org.cryptomator.common.Environment;
 
 import javax.inject.Named;
 import javax.inject.Singleton;
-import java.nio.file.Path;
 import java.util.Optional;
-import java.util.concurrent.BlockingQueue;
 
 @Singleton
 @Component(modules = {CryptomatorModule.class, CommonsModule.class})
 public interface CryptomatorComponent {
 
-	Environment environment();
-
-	FileOpenRequestHandler fileOpenRequestHanlder();
+	IpcFactory ipcFactory();
 
 	@Named("applicationVersion")
 	Optional<String> applicationVersion();

+ 0 - 252
main/launcher/src/main/java/org/cryptomator/launcher/InterProcessCommunicator.java

@@ -1,252 +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.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.Paths;
-import java.nio.file.StandardOpenOption;
-import java.rmi.ConnectException;
-import java.rmi.ConnectIOException;
-import java.rmi.NotBoundException;
-import java.rmi.Remote;
-import java.rmi.RemoteException;
-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 org.apache.commons.lang3.SystemUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.common.io.MoreFiles;
-
-/**
- * First running application on a machine opens a server socket. Further processes will connect as clients.
- */
-abstract class InterProcessCommunicator implements InterProcessCommunicationProtocol, Closeable {
-
-	private static final Logger LOG = LoggerFactory.getLogger(InterProcessCommunicator.class);
-	private static final String RMI_NAME = "Cryptomator";
-
-	public abstract boolean isServer();
-
-	/**
-	 * @param portFilePath Path to a file containing the IPC port
-	 * @param endpoint The server-side communication endpoint.
-	 * @return Either a client or a server communicator.
-	 * @throws IOException In case of communication errors.
-	 */
-	public static InterProcessCommunicator start(Path portFilePath, InterProcessCommunicationProtocol endpoint) throws IOException {
-		System.setProperty("java.rmi.server.hostname", "localhost");
-		try {
-			// try to connect to existing server:
-			ClientCommunicator client = new ClientCommunicator(portFilePath);
-			LOG.trace("Connected to running process.");
-			return client;
-		} catch (ConnectException | ConnectIOException | NotBoundException e) {
-			LOG.debug("Could not connect to running process.");
-			// continue
-		}
-
-		// spawn a new server:
-		LOG.trace("Spawning new server...");
-		ServerCommunicator server = new ServerCommunicator(endpoint, portFilePath);
-		LOG.debug("Server listening on port {}.", server.getPort());
-		return server;
-	}
-
-	public static class ClientCommunicator extends InterProcessCommunicator {
-
-		private final IpcProtocolRemote remote;
-
-		private ClientCommunicator(Path portFilePath) throws ConnectException, NotBoundException, RemoteException {
-			if (Files.notExists(portFilePath)) {
-				throw new ConnectException("No IPC port file.");
-			}
-			try {
-				int port = ClientCommunicator.readPort(portFilePath);
-				LOG.debug("Connecting to port {}...", port);
-				Registry registry = LocateRegistry.getRegistry("localhost", port, new ClientSocketFactory());
-				this.remote = (IpcProtocolRemote) registry.lookup(RMI_NAME);
-			} catch (IOException e) {
-				throw new ConnectException("Error reading IPC port file.");
-			}
-		}
-
-		private static int readPort(Path portFilePath) throws IOException {
-			ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES);
-			try (ReadableByteChannel ch = Files.newByteChannel(portFilePath, StandardOpenOption.READ)) {
-				if (ch.read(buf) == Integer.BYTES) {
-					buf.flip();
-					return buf.getInt();
-				} else {
-					throw new IOException("Invalid IPC port file.");
-				}
-			}
-		}
-
-		@Override
-		public void handleLaunchArgs(String[] args) {
-			try {
-				remote.handleLaunchArgs(args);
-			} catch (RemoteException e) {
-				throw new RuntimeException(e);
-			}
-		}
-
-		@Override
-		public boolean isServer() {
-			return false;
-		}
-
-		@Override
-		public void close() {
-			// no-op
-		}
-
-	}
-
-	public static class ServerCommunicator extends InterProcessCommunicator {
-
-		private final ServerSocket socket;
-		private final Registry registry;
-		private final IpcProtocolRemoteImpl remote;
-		private final Path portFilePath;
-
-		private ServerCommunicator(InterProcessCommunicationProtocol delegate, Path portFilePath) throws IOException {
-			this.socket = new ServerSocket(0, Byte.MAX_VALUE, InetAddress.getByName("localhost"));
-			RMIClientSocketFactory csf = RMISocketFactory.getDefaultSocketFactory();
-			SingletonServerSocketFactory ssf = new SingletonServerSocketFactory(socket);
-			this.registry = LocateRegistry.createRegistry(0, csf, ssf);
-			this.remote = new IpcProtocolRemoteImpl(delegate);
-			UnicastRemoteObject.exportObject(remote, 0);
-			registry.rebind(RMI_NAME, remote);
-			this.portFilePath = portFilePath;
-			ServerCommunicator.writePort(portFilePath, socket.getLocalPort());
-		}
-
-		private static 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.");
-				}
-			}
-		}
-
-		@Override
-		public void handleLaunchArgs(String[] args) {
-			throw new UnsupportedOperationException("Server doesn't invoke methods.");
-		}
-
-		@Override
-		public boolean isServer() {
-			return true;
-		}
-
-		private int getPort() {
-			return socket.getLocalPort();
-		}
-
-		@Override
-		public void close() {
-			try {
-				registry.unbind(RMI_NAME);
-				UnicastRemoteObject.unexportObject(remote, true);
-				socket.close();
-				Files.deleteIfExists(portFilePath);
-				LOG.debug("Server shut down.");
-			} catch (NotBoundException | IOException e) {
-				LOG.warn("Failed to close IPC Server.", e);
-			}
-		}
-
-	}
-
-	private static interface IpcProtocolRemote extends Remote {
-		void handleLaunchArgs(String[] args) throws RemoteException;
-	}
-
-	private static class IpcProtocolRemoteImpl implements IpcProtocolRemote {
-
-		private final InterProcessCommunicationProtocol delegate;
-
-		protected IpcProtocolRemoteImpl(InterProcessCommunicationProtocol delegate) throws RemoteException {
-			this.delegate = delegate;
-		}
-
-		@Override
-		public void handleLaunchArgs(String[] args) {
-			delegate.handleLaunchArgs(args);
-		}
-
-	}
-
-	/**
-	 * 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
-		}
-
-	}
-
-}

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

@@ -0,0 +1,258 @@
+/*******************************************************************************
+ * 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 - 2
main/launcher/src/main/java/org/cryptomator/launcher/InterProcessCommunicationProtocol.java

@@ -5,6 +5,11 @@
  *******************************************************************************/
 package org.cryptomator.launcher;
 
-public interface InterProcessCommunicationProtocol {
-	void handleLaunchArgs(String[] args);
+import java.rmi.Remote;
+import java.rmi.RemoteException;
+
+interface IpcProtocol extends Remote {
+
+	void handleLaunchArgs(String[] args) throws RemoteException;
+
 }

+ 28 - 0
main/launcher/src/main/java/org/cryptomator/launcher/IpcProtocolImpl.java

@@ -0,0 +1,28 @@
+package org.cryptomator.launcher;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import java.util.Arrays;
+
+@Singleton
+class IpcProtocolImpl implements IpcProtocol {
+
+	private static final Logger LOG = LoggerFactory.getLogger(IpcProtocolImpl.class);
+
+	private final FileOpenRequestHandler fileOpenRequestHandler;
+
+	@Inject
+	public IpcProtocolImpl(FileOpenRequestHandler fileOpenRequestHandler) {
+		this.fileOpenRequestHandler = fileOpenRequestHandler;
+	}
+
+	@Override
+	public void handleLaunchArgs(String[] args) {
+		LOG.info("Received launch args: {}", Arrays.stream(args).reduce((a, b) -> a + ", " + b).orElse(""));
+		fileOpenRequestHandler.handleLaunchArgs(args);
+	}
+
+}

+ 0 - 102
main/launcher/src/test/java/org/cryptomator/launcher/InterProcessCommunicatorTest.java

@@ -1,102 +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.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.mockito.Mockito;
-
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.channels.SeekableByteChannel;
-import java.nio.file.FileSystem;
-import java.nio.file.NoSuchFileException;
-import java.nio.file.Path;
-import java.nio.file.attribute.BasicFileAttributes;
-import java.nio.file.spi.FileSystemProvider;
-import java.util.concurrent.atomic.AtomicInteger;
-
-public class InterProcessCommunicatorTest {
-
-	Path portFilePath = Mockito.mock(Path.class);
-	Path portFileParentPath = Mockito.mock(Path.class);
-	BasicFileAttributes portFileParentPathAttrs = Mockito.mock(BasicFileAttributes.class);
-	FileSystem fs = Mockito.mock(FileSystem.class);
-	FileSystemProvider provider = Mockito.mock(FileSystemProvider.class);
-	SeekableByteChannel portFileChannel = Mockito.mock(SeekableByteChannel.class);
-	AtomicInteger port = new AtomicInteger(-1);
-
-	@BeforeEach
-	public void setup() throws IOException {
-		Mockito.when(portFilePath.getFileSystem()).thenReturn(fs);
-		Mockito.when(portFilePath.toAbsolutePath()).thenReturn(portFilePath);
-		Mockito.when(portFilePath.normalize()).thenReturn(portFilePath);
-		Mockito.when(portFilePath.getParent()).thenReturn(portFileParentPath);
-		Mockito.when(portFileParentPath.getFileSystem()).thenReturn(fs);
-		Mockito.when(fs.provider()).thenReturn(provider);
-		Mockito.when(provider.readAttributes(portFileParentPath, BasicFileAttributes.class)).thenReturn(portFileParentPathAttrs);
-		Mockito.when(portFileParentPathAttrs.isDirectory()).thenReturn(false, true); // Guava's MoreFiles will check if dir exists before attempting to create them.
-		Mockito.when(provider.newByteChannel(Mockito.eq(portFilePath), Mockito.any(), Mockito.any())).thenReturn(portFileChannel);
-		Mockito.when(portFileChannel.read(Mockito.any())).then(invocation -> {
-			ByteBuffer buf = invocation.getArgument(0);
-			buf.putInt(port.get());
-			return Integer.BYTES;
-		});
-		Mockito.when(portFileChannel.write(Mockito.any())).then(invocation -> {
-			ByteBuffer buf = invocation.getArgument(0);
-			port.set(buf.getInt());
-			return Integer.BYTES;
-		});
-	}
-
-	@Test
-	public void testStartWithDummyPort1() throws IOException {
-		port.set(0);
-		InterProcessCommunicationProtocol protocol = Mockito.mock(InterProcessCommunicationProtocol.class);
-		try (InterProcessCommunicator result = InterProcessCommunicator.start(portFilePath, protocol)) {
-			Assertions.assertTrue(result.isServer());
-			Mockito.verify(provider).createDirectory(portFileParentPath);
-			Mockito.verifyZeroInteractions(protocol);
-			Assertions.assertThrows(UnsupportedOperationException.class, () -> {
-				result.handleLaunchArgs(new String[] {"foo"});
-			});
-		}
-	}
-
-	@Test
-	public void testStartWithDummyPort2() throws IOException {
-		Mockito.doThrow(new NoSuchFileException("port file")).when(provider).checkAccess(portFilePath);
-
-		InterProcessCommunicationProtocol protocol = Mockito.mock(InterProcessCommunicationProtocol.class);
-		try (InterProcessCommunicator result = InterProcessCommunicator.start(portFilePath, protocol)) {
-			Assertions.assertTrue(result.isServer());
-			Mockito.verify(provider).createDirectory(portFileParentPath);
-			Mockito.verifyZeroInteractions(protocol);
-		}
-	}
-
-	@Test
-	public void testInterProcessCommunication() throws IOException, InterruptedException {
-		port.set(-1);
-		InterProcessCommunicationProtocol protocol = Mockito.mock(InterProcessCommunicationProtocol.class);
-		try (InterProcessCommunicator result1 = InterProcessCommunicator.start(portFilePath, protocol)) {
-			Assertions.assertTrue(result1.isServer());
-			Mockito.verify(provider, Mockito.times(1)).createDirectory(portFileParentPath);
-			Mockito.verifyZeroInteractions(protocol);
-
-			try (InterProcessCommunicator result2 = InterProcessCommunicator.start(portFilePath, null)) {
-				Assertions.assertFalse(result2.isServer());
-				Mockito.verify(provider, Mockito.times(1)).createDirectory(portFileParentPath);
-				Assertions.assertNotSame(result1, result2);
-
-				result2.handleLaunchArgs(new String[] {"foo"});
-				Mockito.verify(protocol).handleLaunchArgs(new String[] {"foo"});
-			}
-		}
-	}
-
-}

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

@@ -0,0 +1,71 @@
+/*******************************************************************************
+ * 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("Wihout 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"});
+			}
+		}
+	}
+
+}