Refactored IPC, fixes #663

This commit is contained in:
Sebastian Stenzel
2019-02-19 16:46:21 +01:00
parent 79306ea498
commit f1c332f455
11 changed files with 401 additions and 413 deletions

3
.idea/compiler.xml generated
View File

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

9
.idea/modules.xml generated Normal file
View File

@@ -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>

View File

@@ -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() {

View File

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

View File

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

View File

@@ -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
}
}
}

View File

@@ -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
}
}
}

View File

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

View File

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

View File

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

View File

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