diff --git a/.idea/compiler.xml b/.idea/compiler.xml index eb6e630bd..90cd3c60e 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -29,5 +29,8 @@ + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 000000000..bca3878d6 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/main/commons/src/main/java/org/cryptomator/common/Environment.java b/main/commons/src/main/java/org/cryptomator/common/Environment.java index 6b582d7f0..b45b743f2 100644 --- a/main/commons/src/main/java/org/cryptomator/common/Environment.java +++ b/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 getSettingsPath() { diff --git a/main/launcher/src/main/java/org/cryptomator/launcher/Cryptomator.java b/main/launcher/src/main/java/org/cryptomator/launcher/Cryptomator.java index 11a49112d..12d1e5a65 100644 --- a/main/launcher/src/main/java/org/cryptomator/launcher/Cryptomator.java +++ b/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); - } - - } - } diff --git a/main/launcher/src/main/java/org/cryptomator/launcher/CryptomatorComponent.java b/main/launcher/src/main/java/org/cryptomator/launcher/CryptomatorComponent.java index 6cd57024e..eaedf7585 100644 --- a/main/launcher/src/main/java/org/cryptomator/launcher/CryptomatorComponent.java +++ b/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 applicationVersion(); diff --git a/main/launcher/src/main/java/org/cryptomator/launcher/InterProcessCommunicator.java b/main/launcher/src/main/java/org/cryptomator/launcher/InterProcessCommunicator.java deleted file mode 100644 index 9363b6246..000000000 --- a/main/launcher/src/main/java/org/cryptomator/launcher/InterProcessCommunicator.java +++ /dev/null @@ -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 - } - - } - -} diff --git a/main/launcher/src/main/java/org/cryptomator/launcher/IpcFactory.java b/main/launcher/src/main/java/org/cryptomator/launcher/IpcFactory.java new file mode 100644 index 000000000..112050d26 --- /dev/null +++ b/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 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 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 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 + } + + } + +} diff --git a/main/launcher/src/main/java/org/cryptomator/launcher/InterProcessCommunicationProtocol.java b/main/launcher/src/main/java/org/cryptomator/launcher/IpcProtocol.java similarity index 71% rename from main/launcher/src/main/java/org/cryptomator/launcher/InterProcessCommunicationProtocol.java rename to main/launcher/src/main/java/org/cryptomator/launcher/IpcProtocol.java index 1b3cc1fc8..40b4ded51 100644 --- a/main/launcher/src/main/java/org/cryptomator/launcher/InterProcessCommunicationProtocol.java +++ b/main/launcher/src/main/java/org/cryptomator/launcher/IpcProtocol.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; + } \ No newline at end of file diff --git a/main/launcher/src/main/java/org/cryptomator/launcher/IpcProtocolImpl.java b/main/launcher/src/main/java/org/cryptomator/launcher/IpcProtocolImpl.java new file mode 100644 index 000000000..158ec290d --- /dev/null +++ b/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); + } + +} diff --git a/main/launcher/src/test/java/org/cryptomator/launcher/InterProcessCommunicatorTest.java b/main/launcher/src/test/java/org/cryptomator/launcher/InterProcessCommunicatorTest.java deleted file mode 100644 index 2b7d553b9..000000000 --- a/main/launcher/src/test/java/org/cryptomator/launcher/InterProcessCommunicatorTest.java +++ /dev/null @@ -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"}); - } - } - } - -} diff --git a/main/launcher/src/test/java/org/cryptomator/launcher/IpcFactoryTest.java b/main/launcher/src/test/java/org/cryptomator/launcher/IpcFactoryTest.java new file mode 100644 index 000000000..e4fef5556 --- /dev/null +++ b/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"}); + } + } + } + +}