Refactored Cryptomator UI. Extracted Launcher to its own Maven module.

This commit is contained in:
Sebastian Stenzel
2017-04-18 13:40:59 +02:00
parent ada1195a26
commit 93b2a4e07a
55 changed files with 1234 additions and 1324 deletions

View File

@@ -1 +0,0 @@
/target/

View File

@@ -1,43 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2015 Markus Kreusch
This file is licensed under the terms of the MIT license.
See the LICENSE.txt file for more info.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.3.0-SNAPSHOT</version>
</parent>
<artifactId>commons-test</artifactId>
<name>Cryptomator common test dependencies</name>
<description>Shared utilities for tests</description>
<dependencies>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>commons</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
</dependency>
<dependency>
<groupId>de.bechte.junit</groupId>
<artifactId>junit-hierarchicalcontextrunner</artifactId>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -13,7 +13,7 @@
<version>1.3.0-SNAPSHOT</version>
</parent>
<artifactId>commons</artifactId>
<name>Cryptomator common</name>
<name>Cryptomator Commons</name>
<description>Shared utilities</description>
<dependencies>
@@ -26,6 +26,14 @@
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>org.fxmisc.easybind</groupId>
<artifactId>easybind</artifactId>
</dependency>
<!-- DI -->
<dependency>
@@ -38,25 +46,10 @@
<scope>provided</scope>
</dependency>
<!-- Test -->
<!-- Logging -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>de.bechte.junit</groupId>
<artifactId>junit-hierarchicalcontextrunner</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

View File

@@ -6,7 +6,7 @@
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.ui.settings;
package org.cryptomator.common.settings;
import java.util.function.Consumer;

View File

@@ -3,7 +3,7 @@
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.ui.settings;
package org.cryptomator.common.settings;
import java.io.IOException;
import java.util.ArrayList;

View File

@@ -6,7 +6,7 @@
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.ui.settings;
package org.cryptomator.common.settings;
import java.io.IOException;
import java.io.InputStream;

View File

@@ -3,7 +3,7 @@
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.ui.settings;
package org.cryptomator.common.settings;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;

View File

@@ -3,7 +3,7 @@
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.ui.settings;
package org.cryptomator.common.settings;
import java.io.IOException;
import java.nio.file.Paths;

View File

@@ -3,7 +3,7 @@
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.ui.settings;
package org.cryptomator.common.settings;
import java.io.IOException;

View File

@@ -3,7 +3,7 @@
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.ui.settings;
package org.cryptomator.common.settings;
import java.io.IOException;
import java.io.StringReader;

View File

@@ -6,7 +6,7 @@
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.ui.settings;
package org.cryptomator.common.settings;
import static org.junit.Assert.assertEquals;

View File

@@ -9,18 +9,7 @@
</parent>
<artifactId>jacoco-report</artifactId>
<name>Cryptomator Code Coverage Report</name>
<dependencies>
<!-- Commons -->
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>commons</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>commons-test</artifactId>
</dependency>
</dependencies>
<packaging>pom</packaging>
<build>
<plugins>

View File

@@ -37,10 +37,11 @@
<scope>provided</scope>
</dependency>
<!-- Test -->
<!-- Logging -->
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>commons-test</artifactId>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

57
main/launcher/pom.xml Normal file
View File

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.3.0-SNAPSHOT</version>
</parent>
<artifactId>launcher</artifactId>
<name>Cryptomator Launcher</name>
<dependencies>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>commons</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>ui</artifactId>
</dependency>
<!-- Libs -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- DI -->
<dependency>
<groupId>com.google.dagger</groupId>
<artifactId>dagger</artifactId>
</dependency>
<dependency>
<groupId>com.google.dagger</groupId>
<artifactId>dagger-compiler</artifactId>
<scope>provided</scope>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-jul</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -3,12 +3,10 @@
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.ui.util;
package org.cryptomator.launcher;
import java.util.Optional;
import org.cryptomator.ui.Cryptomator;
public class ApplicationVersion {
public static String orElse(String other) {

View File

@@ -0,0 +1,35 @@
package org.cryptomator.launcher;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class CleanShutdownPerformer extends Thread {
private static final Logger LOG = LoggerFactory.getLogger(CleanShutdownPerformer.class);
static final ConcurrentMap<Runnable, Boolean> SHUTDOWN_TASKS = new ConcurrentHashMap<>();
@Override
public void run() {
LOG.debug("Running graceful shutdown tasks...");
SHUTDOWN_TASKS.keySet().forEach(r -> {
try {
r.run();
} catch (RuntimeException e) {
LOG.error("Exception while shutting down.", e);
}
});
SHUTDOWN_TASKS.clear();
LOG.info("Goodbye.");
}
static void scheduleShutdownTask(Runnable task) {
SHUTDOWN_TASKS.put(task, Boolean.TRUE);
}
static void registerShutdownHook() {
Runtime.getRuntime().addShutdownHook(new CleanShutdownPerformer());
}
}

View File

@@ -0,0 +1,54 @@
package org.cryptomator.launcher;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import org.apache.commons.lang3.SystemUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javafx.application.Application;
public class Cryptomator {
private static final Logger LOG = LoggerFactory.getLogger(Cryptomator.class);
static final BlockingQueue<Path> FILE_OPEN_REQUESTS = new ArrayBlockingQueue<>(10);
public static void main(String[] args) throws IOException {
LOG.info("Starting Cryptomator {} on {} {} ({})", ApplicationVersion.orElse("SNAPSHOT"), SystemUtils.OS_NAME, SystemUtils.OS_VERSION, SystemUtils.OS_ARCH);
FileOpenRequestHandler fileOpenRequestHandler = new FileOpenRequestHandler(FILE_OPEN_REQUESTS);
try (InterProcessCommunicator communicator = InterProcessCommunicator.start(new IpcProtocolImpl(fileOpenRequestHandler))) {
if (communicator.isServer()) {
fileOpenRequestHandler.handleLaunchArgs(args);
CleanShutdownPerformer.registerShutdownHook();
Application.launch(MainApplication.class, args);
} else {
communicator.handleLaunchArgs(args);
LOG.info("Found running application instance. Shutting down.");
}
}
System.exit(0); // end remaining non-daemon threads.
}
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

@@ -0,0 +1,102 @@
/*******************************************************************************
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
* All rights reserved.
*
* This class is licensed under the LGPL 3.0 (https://www.gnu.org/licenses/lgpl-3.0.de.html).
*******************************************************************************/
package org.cryptomator.launcher;
import java.io.File;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import org.apache.commons.lang3.SystemUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class FileOpenRequestHandler {
private static final Logger LOG = LoggerFactory.getLogger(FileOpenRequestHandler.class);
private final BlockingQueue<Path> fileOpenRequests;
public FileOpenRequestHandler(BlockingQueue<Path> fileOpenRequests) {
this.fileOpenRequests = fileOpenRequests;
if (SystemUtils.IS_OS_MAC_OSX) {
addOsxFileOpenHandler();
}
}
public void handleLaunchArgs(String[] args) {
handleLaunchArgs(FileSystems.getDefault(), args);
}
// visible for testing
void handleLaunchArgs(FileSystem fs, String[] args) {
for (String arg : args) {
try {
Path path = fs.getPath(arg);
tryToEnqueueFileOpenRequest(path);
} catch (InvalidPathException e) {
LOG.trace("{} not a valid path", arg);
}
}
}
private void tryToEnqueueFileOpenRequest(Path path) {
if (!fileOpenRequests.offer(path)) {
LOG.warn("{} could not be enqueued for opening.", path);
}
}
/**
* Event subscription code inspired by https://gitlab.com/axet/desktop/blob/master/java/src/main/java/com/github/axet/desktop/os/mac/AppleHandlers.java
*/
private void addOsxFileOpenHandler() {
try {
final Class<?> applicationClass = Class.forName("com.apple.eawt.Application");
final Class<?> openFilesHandlerClass = Class.forName("com.apple.eawt.OpenFilesHandler");
final Method getApplication = applicationClass.getMethod("getApplication");
final Object application = getApplication.invoke(null);
final Method setOpenFileHandler = applicationClass.getMethod("setOpenFileHandler", openFilesHandlerClass);
final ClassLoader openFilesHandlerClassLoader = openFilesHandlerClass.getClassLoader();
final OpenFilesEventInvocationHandler openFilesHandler = new OpenFilesEventInvocationHandler();
final Object openFilesHandlerObject = Proxy.newProxyInstance(openFilesHandlerClassLoader, new Class<?>[] {openFilesHandlerClass}, openFilesHandler);
setOpenFileHandler.invoke(application, openFilesHandlerObject);
} catch (ReflectiveOperationException | RuntimeException e) {
// Since we're trying to call OS-specific code, we'll just have to hope for the best.
LOG.error("Exception adding OS X file open handler", e);
}
}
/**
* Handler class inspired by https://gitlab.com/axet/desktop/blob/master/java/src/main/java/com/github/axet/desktop/os/mac/AppleHandlers.java
*/
private class OpenFilesEventInvocationHandler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("openFiles")) {
final Class<?> openFilesEventClass = Class.forName("com.apple.eawt.AppEvent$OpenFilesEvent");
final Method getFiles = openFilesEventClass.getMethod("getFiles");
Object e = args[0];
try {
@SuppressWarnings("unchecked")
final List<File> ff = (List<File>) getFiles.invoke(e);
ff.stream().map(File::toPath).forEach(fileOpenRequests::add);
} catch (RuntimeException ee) {
throw ee;
} catch (Exception ee) {
throw new RuntimeException(ee);
}
}
return null;
}
}
}

View File

@@ -0,0 +1,5 @@
package org.cryptomator.launcher;
public interface InterProcessCommunicationProtocol {
void handleLaunchArgs(String[] args);
}

View File

@@ -0,0 +1,232 @@
package org.cryptomator.launcher;
import java.io.Closeable;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
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.NoSuchObjectException;
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;
/**
* 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 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(InterProcessCommunicationProtocol endpoint) throws IOException {
return start(getIpcPortPath(), endpoint);
}
// visible for testing
static InterProcessCommunicator start(Path portFilePath, InterProcessCommunicationProtocol endpoint) throws IOException {
// try to connect to existing server:
int port = readPort(portFilePath);
LOG.debug("Connecting to running process on TCP port {}...", port);
try {
ClientCommunicator client = new ClientCommunicator(port);
LOG.trace("Connected to running process.");
return client;
} catch (ConnectException | NotBoundException e) {
LOG.debug("Did not find running process.");
// continue
}
// spawn a new server:
LOG.trace("Spawning new server...");
ServerCommunicator server = new ServerCommunicator(endpoint);
writePort(portFilePath, server.getPort());
LOG.debug("Server listening on port {}.", server.getPort());
return server;
}
private static Path getIpcPortPath() {
final String settingsPathProperty = System.getProperty("cryptomator.ipcPortPath");
if (settingsPathProperty == null) {
LOG.warn("System property cryptomator.ipcPortPath not set.");
return Paths.get("ipcPort.tmp");
} else {
return Paths.get(replaceHomeDir(settingsPathProperty));
}
}
private static String replaceHomeDir(String path) {
if (path.startsWith("~/")) {
return SystemUtils.USER_HOME + path.substring(1);
} else {
return path;
}
}
public static class ClientCommunicator extends InterProcessCommunicator {
private final IpcProtocolRemote remote;
private ClientCommunicator(int port) throws ConnectException, NotBoundException, RemoteException {
if (port == 0) {
throw new ConnectException("Can not connect to port 0.");
}
Registry registry = LocateRegistry.getRegistry(port);
this.remote = (IpcProtocolRemote) registry.lookup(RMI_NAME);
}
@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() throws IOException {
// no-op
}
}
public static class ServerCommunicator extends InterProcessCommunicator {
private final ServerSocket socket;
private final Registry registry;
private final IpcProtocolRemoteImpl remote;
private ServerCommunicator(InterProcessCommunicationProtocol delegate) throws IOException {
this.socket = new ServerSocket(0, Byte.MAX_VALUE, InetAddress.getLocalHost());
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);
}
@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() throws IOException {
try {
registry.unbind(RMI_NAME);
UnicastRemoteObject.unexportObject(remote, true);
socket.close();
LOG.debug("Server shut down.");
} catch (NotBoundException | NoSuchObjectException e) {
// ignore
}
}
}
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;
}
}
private static int readPort(Path path) throws IOException {
if (Files.notExists(path)) {
return 0;
}
ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES);
try (ReadableByteChannel ch = Files.newByteChannel(path, StandardOpenOption.READ)) {
if (ch.read(buf) == Integer.BYTES) {
buf.flip();
return buf.getInt();
} else {
return 0;
}
}
}
private static void writePort(Path path, int port) throws IOException {
ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES);
buf.putInt(port);
buf.flip();
try (WritableByteChannel ch = Files.newByteChannel(path, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
if (ch.write(buf) != Integer.BYTES) {
throw new IOException("Did not write expected number of bytes.");
}
}
}
}

View File

@@ -0,0 +1,18 @@
package org.cryptomator.launcher;
import javax.inject.Singleton;
import org.cryptomator.logging.DebugMode;
import org.cryptomator.ui.controllers.MainController;
import dagger.Component;
@Singleton
@Component(modules = LauncherModule.class)
interface LauncherComponent {
MainController mainController();
DebugMode debugMode();
}

View File

@@ -0,0 +1,63 @@
package org.cryptomator.launcher;
import java.nio.file.Path;
import java.util.Optional;
import java.util.concurrent.BlockingQueue;
import java.util.function.Consumer;
import javax.inject.Named;
import javax.inject.Singleton;
import org.cryptomator.ui.UiModule;
import dagger.Module;
import dagger.Provides;
import javafx.application.Application;
import javafx.stage.Stage;
@Module(includes = {UiModule.class})
class LauncherModule {
private final Application application;
private final Stage mainWindow;
public LauncherModule(Application application, Stage mainWindow) {
this.application = application;
this.mainWindow = mainWindow;
}
@Provides
@Singleton
Application provideApplication() {
return application;
}
@Provides
@Singleton
@Named("applicationVersion")
Optional<String> provideApplicationVersion() {
return ApplicationVersion.get();
}
@Provides
@Singleton
@Named("mainWindow")
Stage provideMainWindow() {
return mainWindow;
}
@Provides
@Singleton
@Named("fileOpenRequests")
BlockingQueue<Path> provideFileOpenRequests() {
return Cryptomator.FILE_OPEN_REQUESTS;
}
@Provides
@Singleton
@Named("shutdownTaskScheduler")
Consumer<Runnable> provideShutdownTaskScheduler() {
return CleanShutdownPerformer::scheduleShutdownTask;
}
}

View File

@@ -0,0 +1,58 @@
package org.cryptomator.launcher;
import org.cryptomator.ui.controllers.MainController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.stage.Stage;
public class MainApplication extends Application {
private static final Logger LOG = LoggerFactory.getLogger(MainApplication.class);
private Stage primaryStage;
@Override
public void start(Stage primaryStage) throws Exception {
LOG.info("JavaFX application started.");
this.primaryStage = primaryStage;
setupFXMLClassLoader();
LauncherModule launcherModule = new LauncherModule(this, primaryStage);
LauncherComponent launcherComponent = DaggerLauncherComponent.builder() //
.launcherModule(launcherModule) //
.build();
launcherComponent.debugMode().initialize();
MainController mainCtrl = launcherComponent.mainController();
mainCtrl.initStage(primaryStage);
primaryStage.show();
}
@Override
public void stop() throws Exception {
assert primaryStage != null;
primaryStage.hide();
LOG.info("JavaFX application stopped.");
}
// fix discussed in https://github.com/cryptomator/cryptomator/pull/29
private void setupFXMLClassLoader() {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
FXMLLoader.setDefaultClassLoader(contextClassLoader);
Platform.runLater(() -> {
/*
* This fixes a bug on OSX where the magic file open handler leads to no context class loader being set in the AppKit (event)
* thread if the application is not started opening a file.
*/
if (Thread.currentThread().getContextClassLoader() == null) {
Thread.currentThread().setContextClassLoader(contextClassLoader);
}
});
}
}

View File

@@ -0,0 +1,132 @@
/*******************************************************************************
* Copyright (c) 2016 Sebastian Stenzel and others.
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.logging;
import java.io.IOException;
import java.io.Serializable;
import java.net.URISyntaxException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.regex.Pattern;
import org.apache.commons.lang3.SystemUtils;
import org.apache.logging.log4j.core.Appender;
import org.apache.logging.log4j.core.Core;
import org.apache.logging.log4j.core.Filter;
import org.apache.logging.log4j.core.Layout;
import org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender;
import org.apache.logging.log4j.core.appender.FileManager;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
import org.apache.logging.log4j.core.config.plugins.validation.constraints.Required;
import org.apache.logging.log4j.util.Strings;
/**
* A preconfigured FileAppender only relying on a configurable system property, e.g. <code>-Dcryptomator.logPath=/var/log/cryptomator.log</code>.<br/>
* Other than the normal {@link org.apache.logging.log4j.core.appender.FileAppender} paths can be resolved relative to the users home directory.
*/
@Plugin(name = ConfigurableFileAppender.PLUGIN_NAME, category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE, printObject = true)
public class ConfigurableFileAppender extends AbstractOutputStreamAppender<FileManager> {
static final String PLUGIN_NAME = "ConfigurableFile";
private static final Pattern DRIVE_LETTER_WITH_PRECEEDING_SLASH = Pattern.compile("^/[A-Z]:", Pattern.CASE_INSENSITIVE);
private ConfigurableFileAppender(String name, Layout<? extends Serializable> layout, Filter filter, boolean ignoreExceptions, boolean immediateFlush, FileManager manager) {
super(name, layout, filter, ignoreExceptions, immediateFlush, manager);
LOGGER.info("Logging to " + manager.getFileName());
}
@PluginBuilderFactory
public static <B extends Builder<B>> B newBuilder() {
return new Builder<B>().asBuilder();
}
/**
* Builds ConfigurableFileAppender instances.
*
* @param <B>
* The type to build
*/
public static class Builder<B extends Builder<B>> extends AbstractOutputStreamAppender.Builder<B> //
implements org.apache.logging.log4j.core.util.Builder<ConfigurableFileAppender> {
@Required(message = "No system property name containing the log file path provided.")
@PluginBuilderAttribute("pathPropertyName")
private String pathPropertyName;
@PluginBuilderAttribute
private boolean append = true;
@Override
public ConfigurableFileAppender build() {
final String pathProperty = System.getProperty(pathPropertyName);
if (Strings.isEmpty(pathProperty)) {
LOGGER.warn("No log file location provided in system property \"" + pathPropertyName + "\"");
return null;
}
final Path filePath = parsePath(pathProperty);
if (filePath == null) {
LOGGER.warn("Invalid path \"" + pathProperty + "\"");
return null;
}
if (!Files.exists(filePath.getParent())) {
try {
Files.createDirectories(filePath.getParent());
} catch (IOException e) {
LOGGER.error("Could not create parent directories for log file located at " + filePath.toString(), e);
return null;
}
}
FileManager manager = FileManager.getFileManager(filePath.toString(), append, false, isBufferedIo(), true, null, getOrCreateLayout(), getBufferSize(), getConfiguration());
return new ConfigurableFileAppender(getName(), getLayout(), getFilter(), isIgnoreExceptions(), isImmediateFlush(), manager);
}
public B withPathPropertyName(String pathPropertyName) {
this.pathPropertyName = pathPropertyName;
return asBuilder();
}
public B withAppend(boolean append) {
this.append = append;
return asBuilder();
}
}
private static Path parsePath(String path) {
if (path.startsWith("~/")) {
// home-dir-relative Path:
final Path userHome = FileSystems.getDefault().getPath(SystemUtils.USER_HOME);
return userHome.resolve(path.substring(2));
} else if (path.startsWith("/")) {
// absolute Path:
return FileSystems.getDefault().getPath(path);
} else {
// relative Path:
try {
String jarFileLocation = ConfigurableFileAppender.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath();
if (SystemUtils.IS_OS_WINDOWS && DRIVE_LETTER_WITH_PRECEEDING_SLASH.matcher(jarFileLocation).find()) {
// on windows we need to remove a preceeding slash from "/C:/foo/bar":
jarFileLocation = jarFileLocation.substring(1);
}
final Path workingDir = FileSystems.getDefault().getPath(jarFileLocation).getParent();
return workingDir.resolve(path);
} catch (URISyntaxException e) {
LOGGER.error("Unable to resolve working directory ", e);
return null;
}
}
}
}

View File

@@ -3,7 +3,7 @@
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.ui;
package org.cryptomator.logging;
import static java.util.Arrays.asList;
import static org.apache.logging.log4j.LogManager.ROOT_LOGGER_NAME;
@@ -18,7 +18,7 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.LoggerConfig;
import org.cryptomator.ui.settings.Settings;
import org.cryptomator.common.settings.Settings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

View File

@@ -7,7 +7,7 @@
Contributors:
Markus Kreusch - switched to log4j 2
-->
<Configuration status="WARN" packages="org.cryptomator.ui.logging">
<Configuration status="WARN" packages="org.cryptomator.logging">
<Appenders>
<Console name="StdOut" target="SYSTEM_OUT">

View File

@@ -0,0 +1,72 @@
package org.cryptomator.launcher;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.spi.FileSystemProvider;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.Mockito;
public class FileOpenRequestHandlerTest {
@Test
public void testOpenArgsWithCorrectPaths() throws IOException {
Path p1 = Mockito.mock(Path.class);
Path p2 = Mockito.mock(Path.class);
FileSystem fs = Mockito.mock(FileSystem.class);
FileSystemProvider provider = Mockito.mock(FileSystemProvider.class);
BasicFileAttributes attrs = Mockito.mock(BasicFileAttributes.class);
Mockito.when(p1.getFileSystem()).thenReturn(fs);
Mockito.when(p2.getFileSystem()).thenReturn(fs);
Mockito.when(fs.provider()).thenReturn(provider);
Mockito.when(fs.getPath(Mockito.anyString())).thenReturn(p1, p2);
Mockito.when(provider.readAttributes(Mockito.any(), Mockito.eq(BasicFileAttributes.class))).thenReturn(attrs);
Mockito.when(attrs.isRegularFile()).thenReturn(true);
BlockingQueue<Path> queue = new ArrayBlockingQueue<>(10);
FileOpenRequestHandler handler = new FileOpenRequestHandler(queue);
handler.handleLaunchArgs(fs, new String[] {"foo", "bar"});
Assert.assertEquals(p1, queue.poll());
Assert.assertEquals(p2, queue.poll());
}
@Test
public void testOpenArgsWithIncorrectPaths() throws IOException {
FileSystem fs = Mockito.mock(FileSystem.class);
Mockito.when(fs.getPath(Mockito.anyString())).thenThrow(new InvalidPathException("foo", "foo is not a path"));
@SuppressWarnings("unchecked")
BlockingQueue<Path> queue = Mockito.mock(BlockingQueue.class);
FileOpenRequestHandler handler = new FileOpenRequestHandler(queue);
handler.handleLaunchArgs(fs, new String[] {"foo"});
Mockito.verifyNoMoreInteractions(queue);
}
@Test
public void testOpenArgsWithFullQueue() throws IOException {
Path p = Mockito.mock(Path.class);
FileSystem fs = Mockito.mock(FileSystem.class);
FileSystemProvider provider = Mockito.mock(FileSystemProvider.class);
BasicFileAttributes attrs = Mockito.mock(BasicFileAttributes.class);
Mockito.when(p.getFileSystem()).thenReturn(fs);
Mockito.when(fs.provider()).thenReturn(provider);
Mockito.when(fs.getPath(Mockito.anyString())).thenReturn(p);
Mockito.when(provider.readAttributes(Mockito.eq(p), Mockito.eq(BasicFileAttributes.class))).thenReturn(attrs);
Mockito.when(attrs.isRegularFile()).thenReturn(true);
@SuppressWarnings("unchecked")
BlockingQueue<Path> queue = Mockito.mock(BlockingQueue.class);
Mockito.when(queue.offer(Mockito.any())).thenReturn(false);
FileOpenRequestHandler handler = new FileOpenRequestHandler(queue);
handler.handleLaunchArgs(fs, new String[] {"foo"});
}
}

View File

@@ -0,0 +1,82 @@
package org.cryptomator.launcher;
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.spi.FileSystemProvider;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
public class InterProcessCommunicatorTest {
Path portFilePath = Mockito.mock(Path.class);
FileSystem fs = Mockito.mock(FileSystem.class);
FileSystemProvider provider = Mockito.mock(FileSystemProvider.class);
SeekableByteChannel portFileChannel = Mockito.mock(SeekableByteChannel.class);
AtomicInteger port = new AtomicInteger(-1);
@Before
public void setup() throws IOException {
Mockito.when(portFilePath.getFileSystem()).thenReturn(fs);
Mockito.when(fs.provider()).thenReturn(provider);
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(expected = UnsupportedOperationException.class)
public void testStartWithDummyPort1() throws IOException {
port.set(0);
InterProcessCommunicationProtocol protocol = Mockito.mock(InterProcessCommunicationProtocol.class);
try (InterProcessCommunicator result = InterProcessCommunicator.start(portFilePath, protocol)) {
Assert.assertTrue(result.isServer());
Mockito.verifyZeroInteractions(protocol);
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)) {
Assert.assertTrue(result.isServer());
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)) {
Assert.assertTrue(result1.isServer());
Mockito.verifyZeroInteractions(protocol);
try (InterProcessCommunicator result2 = InterProcessCommunicator.start(portFilePath, null)) {
Assert.assertFalse(result2.isServer());
Assert.assertNotSame(result1, result2);
result2.handleLaunchArgs(new String[] {"foo"});
Mockito.verify(protocol).handleLaunchArgs(new String[] {"foo"});
}
}
}
}

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright (c) 2014 Markus Kreusch
This file is licensed under the terms of the MIT license.
See the LICENSE.txt file for more info.
Contributors:
Markus Kreusch - switched to log4j 2
-->
<Configuration status="WARN" packages="org.cryptomator.logging">
<Appenders>
<Console name="StdOut" target="SYSTEM_OUT">
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="ACCEPT" />
</Console>
<Console name="StdErr" target="SYSTEM_ERR">
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY" />
</Console>
</Appenders>
<Loggers>
<Logger name="org.cryptomator" level="DEBUG" />
<!-- defaults: -->
<Root level="INFO">
<AppenderRef ref="StdOut" />
<AppenderRef ref="StdErr" />
</Root>
</Loggers>
</Configuration>

View File

@@ -31,7 +31,7 @@
<cryptomator.cryptofs.version>1.2.0</cryptomator.cryptofs.version>
<cryptomator.webdav.version>0.4.0</cryptomator.webdav.version>
<cryptomator.jni.version>1.0.0</cryptomator.jni.version>
<log4j.version>2.1</log4j.version>
<log4j.version>2.8.1</log4j.version> <!-- keep in sync with https://github.com/edwgiz/maven-shaded-log4j-transformer (used in uber-jar), or wait for https://issues.apache.org/jira/browse/LOG4J2-954 fix -->
<slf4j.version>1.7.25</slf4j.version>
<junit.version>4.12</junit.version>
<junit.hierarchicalrunner.version>4.12.1</junit.hierarchicalrunner.version>
@@ -69,12 +69,6 @@
<artifactId>commons</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>commons-test</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>keychain</artifactId>
@@ -85,7 +79,12 @@
<artifactId>ui</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>launcher</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Cryptomator Libs -->
<dependency>
<groupId>org.cryptomator</groupId>
@@ -114,6 +113,11 @@
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
@@ -212,12 +216,6 @@
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
<exclusions>
<exclusion>
<artifactId>hamcrest-core</artifactId>
<groupId>org.hamcrest</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
@@ -228,25 +226,37 @@
</dependencyManagement>
<dependencies>
<!-- common dependencies for all modules -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-jul</artifactId>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>de.bechte.junit</groupId>
<artifactId>junit-hierarchicalcontextrunner</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<modules>
<module>commons</module>
<module>commons-test</module>
<module>keychain</module>
<module>ui</module>
<module>launcher</module>
</modules>
<profiles>

View File

@@ -1,12 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2014 Sebastian Stenzel
This file is licensed under the terms of the MIT license.
See the LICENSE.txt file for more info.
Contributors:
Sebastian Stenzel - initial API and implementation
-->
<!-- Copyright (c) 2014 Sebastian Stenzel This file is licensed under the terms of the MIT license. See the LICENSE.txt file for more info. Contributors: Sebastian Stenzel - initial API and implementation -->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
@@ -15,43 +8,50 @@
<version>1.3.0-SNAPSHOT</version>
</parent>
<artifactId>uber-jar</artifactId>
<packaging>pom</packaging>
<name>Single über jar with all dependencies</name>
<dependencies>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>ui</artifactId>
<artifactId>launcher</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
<goal>shade</goal>
</goals>
</execution>
</executions>
<configuration>
<finalName>Cryptomator-${project.parent.version}</finalName>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<appendAssemblyId>false</appendAssemblyId>
<archive>
<manifestEntries>
<Main-Class>org.cryptomator.ui.Cryptomator</Main-Class>
<Implementation-Version>${project.version}</Implementation-Version>
</manifestEntries>
</archive>
<finalName>Cryptomator-${project.version}</finalName>
<createDependencyReducedPom>false</createDependencyReducedPom>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Main-Class>org.cryptomator.launcher.Cryptomator</Main-Class>
<Implementation-Version>${project.version}</Implementation-Version>
</manifestEntries>
</transformer>
<transformer implementation="com.github.edwgiz.mavenShadePlugin.log4j2CacheTransformer.PluginsCacheFileTransformer">
</transformer>
</transformers>
</configuration>
<dependencies>
<dependency>
<groupId>com.github.edwgiz</groupId>
<artifactId>maven-shade-plugin.log4j2-cachefile-transformer</artifactId>
<version>${log4j.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright (c) 2014 Markus Kreusch
This file is licensed under the terms of the MIT license.
See the LICENSE.txt file for more info.
Contributors:
Markus Kreusch - switched to log4j 2
-->
<Configuration status="WARN">
<Appenders>
<Console name="StdOut" target="SYSTEM_OUT">
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="ACCEPT" />
</Console>
<Console name="StdErr" target="SYSTEM_ERR">
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY" />
</Console>
</Appenders>
<Loggers>
<Logger name="org.cryptomator" level="DEBUG" />
<!-- defaults: -->
<Root level="INFO">
<AppenderRef ref="StdOut" />
<AppenderRef ref="StdErr" />
</Root>
</Loggers>
</Configuration>

View File

@@ -51,17 +51,15 @@
<artifactId>easybind</artifactId>
</dependency>
<!-- JSON -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<!-- Guava -->
<!-- Google -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<!-- apache commons -->
<dependency>
@@ -91,12 +89,6 @@
<artifactId>dagger-compiler</artifactId>
<scope>provided</scope>
</dependency>
<!-- Tests -->
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>commons-test</artifactId>
</dependency>
<!-- Zxcvbn -->
<dependency>
@@ -104,5 +96,19 @@
<artifactId>zxcvbn</artifactId>
<version>1.2.2</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-jul</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -1,154 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014, 2016 cryptomator.org
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Tillmann Gaida - initial implementation
* Sebastian Stenzel - refactoring
******************************************************************************/
package org.cryptomator.ui;
import java.io.File;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Consumer;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.util.ApplicationVersion;
import org.cryptomator.ui.util.SingleInstanceManager;
import org.cryptomator.ui.util.SingleInstanceManager.RemoteInstance;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javafx.application.Application;
public class Cryptomator {
public static final CompletableFuture<Consumer<File>> OPEN_FILE_HANDLER = new CompletableFuture<>();
private static final Logger LOG = LoggerFactory.getLogger(Cryptomator.class);
private static final ConcurrentMap<Runnable, Boolean> SHUTDOWN_TASKS = new ConcurrentHashMap<>();
public static void main(String[] args) {
LOG.info("Starting Cryptomator {} on {} {} ({})", ApplicationVersion.orElse("SNAPSHOT"), SystemUtils.OS_NAME, SystemUtils.OS_VERSION, SystemUtils.OS_ARCH);
if (SystemUtils.IS_OS_MAC_OSX) {
addOsxFileOpenHandler();
}
new CleanShutdownPerformer().registerShutdownHook();
final Optional<RemoteInstance> runningInstance = SingleInstanceManager.getRemoteInstance(MainApplication.APPLICATION_KEY);
if (runningInstance.isPresent()) {
sendArgsToRunningInstance(args, runningInstance);
} else {
Application.launch(MainApplication.class, args);
}
}
private static void addOsxFileOpenHandler() {
/*
* On OSX we're in an awkward position. We need to register a handler in the main thread of this application. However, we can't
* even pass objects to the application, so we're forced to use a static CompletableFuture for the handler, which actually opens
* the file in the application.
*
* Code taken from https://github.com/axet/desktop/blob/master/src/main/java/com/github/axet/desktop/os/mac/AppleHandlers.java
*/
try {
final Class<?> applicationClass = Class.forName("com.apple.eawt.Application");
final Class<?> openFilesHandlerClass = Class.forName("com.apple.eawt.OpenFilesHandler");
final Method getApplication = applicationClass.getMethod("getApplication");
final Object application = getApplication.invoke(null);
final Method setOpenFileHandler = applicationClass.getMethod("setOpenFileHandler", openFilesHandlerClass);
final ClassLoader openFilesHandlerClassLoader = openFilesHandlerClass.getClassLoader();
final OpenFilesHandlerClassHandler openFilesHandlerHandler = new OpenFilesHandlerClassHandler();
final Object openFilesHandlerObject = Proxy.newProxyInstance(openFilesHandlerClassLoader, new Class<?>[] {openFilesHandlerClass}, openFilesHandlerHandler);
setOpenFileHandler.invoke(application, openFilesHandlerObject);
} catch (ReflectiveOperationException | RuntimeException e) {
// Since we're trying to call OS-specific code, we'll just have
// to hope for the best.
LOG.error("exception adding OSX file open handler", e);
}
}
private static void sendArgsToRunningInstance(String[] args, final Optional<RemoteInstance> remoteInstance) {
try (RemoteInstance instance = remoteInstance.get()) {
LOG.info("An instance of Cryptomator is already running at {}.", instance.getRemotePort());
for (int i = 0; i < args.length; i++) {
remoteInstance.get().sendMessage(args[i], 100);
}
} catch (Exception e) {
LOG.error("Error forwarding arguments to remote instance", e);
}
}
public static void addShutdownTask(Runnable r) {
SHUTDOWN_TASKS.put(r, Boolean.TRUE);
}
public static void removeShutdownTask(Runnable r) {
SHUTDOWN_TASKS.remove(r);
}
private static class CleanShutdownPerformer extends Thread {
@Override
public void run() {
LOG.debug("Shutting down");
SHUTDOWN_TASKS.keySet().forEach(r -> {
try {
r.run();
} catch (RuntimeException e) {
LOG.error("exception while shutting down", e);
}
});
SHUTDOWN_TASKS.clear();
}
public void registerShutdownHook() {
Runtime.getRuntime().addShutdownHook(this);
}
}
private static void handleOpenFileRequest(File file) {
try {
OPEN_FILE_HANDLER.get().accept(file);
} catch (Exception e) {
LOG.error("exception handling file open event for file " + file.getAbsolutePath(), e);
throw new RuntimeException(e);
}
}
/**
* Handler class taken from https://github.com/axet/desktop/blob/master/src/main/java/com/github/axet/desktop/os/mac/AppleHandlers.java
*/
private static class OpenFilesHandlerClassHandler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("openFiles")) {
final Class<?> openFilesEventClass = Class.forName("com.apple.eawt.AppEvent$OpenFilesEvent");
final Method getFiles = openFilesEventClass.getMethod("getFiles");
Object e = args[0];
try {
@SuppressWarnings("unchecked")
final List<File> ff = (List<File>) getFiles.invoke(e);
for (File f : ff) {
handleOpenFileRequest(f);
}
} catch (RuntimeException ee) {
throw ee;
} catch (Exception ee) {
throw new RuntimeException(ee);
}
}
return null;
}
}
}

View File

@@ -1,38 +0,0 @@
/*******************************************************************************
* Copyright (c) 2016 Sebastian Stenzel and others.
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.ui;
import java.util.concurrent.ExecutorService;
import javax.inject.Singleton;
import org.cryptomator.ui.controllers.MainController;
import org.cryptomator.ui.model.VaultComponent;
import org.cryptomator.ui.model.VaultModule;
import org.cryptomator.ui.util.DeferredCloser;
import dagger.Component;
@Singleton
@Component(modules = CryptomatorModule.class)
public interface CryptomatorComponent {
ExecutorService executorService();
DeferredCloser deferredCloser();
MainController mainController();
ExitUtil exitUtil();
DebugMode debugMode();
VaultComponent newVaultComponent(VaultModule vaultModule);
}

View File

@@ -33,11 +33,11 @@ import javax.script.ScriptException;
import javax.swing.SwingUtilities;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.jni.JniException;
import org.cryptomator.jni.MacApplicationUiState;
import org.cryptomator.jni.MacFunctions;
import org.cryptomator.ui.settings.Localization;
import org.cryptomator.ui.settings.Settings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -45,7 +45,7 @@ import javafx.application.Platform;
import javafx.stage.Stage;
@Singleton
class ExitUtil {
public class ExitUtil {
private static final Logger LOG = LoggerFactory.getLogger(ExitUtil.class);
@@ -62,7 +62,15 @@ class ExitUtil {
this.macFunctions = macFunctions;
}
public void initExitHandler(Runnable exitCommand) {
public void initExitHandler() {
initExitHandler(ExitUtil::platformExitOnMainThread);
}
private static void platformExitOnMainThread() {
Platform.runLater(Platform::exit);
}
private void initExitHandler(Runnable exitCommand) {
if (SystemUtils.IS_OS_LINUX) {
initMinimizeExitHandler(exitCommand);
} else {

View File

@@ -1,187 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014, 2016 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.ui;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.concurrent.ExecutionException;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.cryptolib.common.SecureRandomModule;
import org.cryptomator.ui.controllers.MainController;
import org.cryptomator.ui.util.ActiveWindowStyleSupport;
import org.cryptomator.ui.util.DeferredCloser;
import org.cryptomator.ui.util.SingleInstanceManager;
import org.cryptomator.ui.util.SingleInstanceManager.LocalInstance;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.scene.image.Image;
import javafx.scene.text.Font;
import javafx.stage.Stage;
public class MainApplication extends Application {
public static final String APPLICATION_KEY = "CryptomatorGUI";
private static final Logger LOG = LoggerFactory.getLogger(MainApplication.class);
private DeferredCloser closer;
@Override
public void start(Stage primaryStage) throws IOException {
LOG.info("JavaFX application started");
CryptomatorComponent comp = createCryptomatorComponent(primaryStage);
MainController mainCtrl = comp.mainController();
closer = comp.deferredCloser();
comp.debugMode().initialize();
setupFXMLClassLoader();
setupStylesheets();
initializeStage(primaryStage, mainCtrl);
showWindow(primaryStage);
registerExitHandler(comp);
openFilesRequestedDuringStartup(primaryStage, mainCtrl);
registerApplicationToProcessOpenFileRequests(primaryStage, comp, mainCtrl);
}
@Override
public void stop() {
try {
closer.close();
} catch (ExecutionException e) {
LOG.error("Error closing ressources", e);
}
}
private CryptomatorComponent createCryptomatorComponent(Stage primaryStage) {
try {
return DaggerCryptomatorComponent.builder() //
.cryptomatorModule(new CryptomatorModule(this, primaryStage)) //
.secureRandomModule(new SecureRandomModule(SecureRandom.getInstanceStrong())) //
.build();
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("Every implementation of the Java platform is required to support at least one strong SecureRandom implementation.", e);
}
}
private void setupFXMLClassLoader() {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
FXMLLoader.setDefaultClassLoader(contextClassLoader);
Platform.runLater(() -> {
/*
* This fixes a bug on OSX where the magic file open handler leads to no context class loader being set in the AppKit (event)
* thread if the application is not started opening a file.
*/
if (Thread.currentThread().getContextClassLoader() == null) {
Thread.currentThread().setContextClassLoader(contextClassLoader);
}
});
}
private void setupStylesheets() {
loadFont("/css/ionicons.ttf");
loadFont("/css/fontawesome-webfont.ttf");
chooseNativeStylesheet();
}
private void loadFont(String resourcePath) {
try (InputStream in = getClass().getResourceAsStream(resourcePath)) {
Font.loadFont(in, 12.0);
} catch (IOException e) {
LOG.warn("Error loading font from path: " + resourcePath, e);
}
}
private void initializeStage(Stage primaryStage, MainController mainCtrl) {
mainCtrl.initStage(primaryStage);
primaryStage.titleProperty().bind(mainCtrl.windowTitle());
primaryStage.setResizable(false);
if (SystemUtils.IS_OS_WINDOWS) {
primaryStage.getIcons().add(new Image(MainApplication.class.getResourceAsStream("/window_icon.png")));
}
}
private void showWindow(Stage primaryStage) {
primaryStage.show();
ActiveWindowStyleSupport.startObservingFocus(primaryStage);
}
private void registerExitHandler(CryptomatorComponent comp) {
comp.exitUtil().initExitHandler(this::quit);
}
private void openFilesRequestedDuringStartup(Stage primaryStage, final MainController mainCtrl) {
for (String arg : getParameters().getUnnamed()) {
handleCommandLineArg(arg, primaryStage, mainCtrl);
}
if (SystemUtils.IS_OS_MAC_OSX) {
Cryptomator.OPEN_FILE_HANDLER.complete(file -> handleCommandLineArg(file.getAbsolutePath(), primaryStage, mainCtrl));
}
}
private void registerApplicationToProcessOpenFileRequests(Stage primaryStage, final CryptomatorComponent comp, final MainController mainCtrl) throws IOException {
LocalInstance cryptomatorGuiInstance = closer.closeLater(SingleInstanceManager.startLocalInstance(APPLICATION_KEY, comp.executorService()), LocalInstance::close).get().get();
cryptomatorGuiInstance.registerListener(arg -> handleCommandLineArg(arg, primaryStage, mainCtrl));
}
private void chooseNativeStylesheet() {
if (SystemUtils.IS_OS_MAC_OSX) {
setUserAgentStylesheet(getClass().getResource("/css/mac_theme.css").toString());
} else if (SystemUtils.IS_OS_LINUX) {
setUserAgentStylesheet(getClass().getResource("/css/linux_theme.css").toString());
} else if (SystemUtils.IS_OS_WINDOWS) {
setUserAgentStylesheet(getClass().getResource("/css/win_theme.css").toString());
}
}
private void handleCommandLineArg(String arg, Stage primaryStage, MainController mainCtrl) {
// find correct location:
final Path path = FileSystems.getDefault().getPath(arg);
final Path vaultPath;
if (Files.isDirectory(path)) {
vaultPath = path;
} else if (Files.isRegularFile(path)) {
vaultPath = path.getParent();
} else {
LOG.warn("Invalid vault path %s", arg);
return;
}
// add vault to ctrl:
Platform.runLater(() -> {
mainCtrl.addVault(vaultPath, true);
primaryStage.setIconified(false);
primaryStage.show();
primaryStage.toFront();
primaryStage.requestFocus();
});
}
private void quit() {
Platform.runLater(() -> {
stop();
Platform.exit();
System.exit(0);
});
}
}

View File

@@ -11,17 +11,18 @@ package org.cryptomator.ui;
import java.net.InetSocketAddress;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
import javax.inject.Named;
import javax.inject.Singleton;
import org.cryptomator.common.CommonsModule;
import org.cryptomator.cryptolib.CryptoLibModule;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.SettingsProvider;
import org.cryptomator.frontend.webdav.WebDavServer;
import org.cryptomator.jni.JniModule;
import org.cryptomator.keychain.KeychainModule;
import org.cryptomator.ui.settings.Settings;
import org.cryptomator.ui.settings.SettingsProvider;
import org.cryptomator.ui.model.VaultComponent;
import org.cryptomator.ui.util.DeferredCloser;
import org.fxmisc.easybind.EasyBind;
import org.slf4j.Logger;
@@ -29,40 +30,18 @@ import org.slf4j.LoggerFactory;
import dagger.Module;
import dagger.Provides;
import javafx.application.Application;
import javafx.beans.binding.Binding;
import javafx.stage.Stage;
@Module(includes = {CommonsModule.class, KeychainModule.class, JniModule.class, CryptoLibModule.class})
class CryptomatorModule {
@Module(includes = {CommonsModule.class, KeychainModule.class, JniModule.class}, subcomponents = {VaultComponent.class})
public class UiModule {
private static final Logger LOG = LoggerFactory.getLogger(CryptomatorModule.class);
private final Application application;
private final Stage mainWindow;
public CryptomatorModule(Application application, Stage mainWindow) {
this.application = application;
this.mainWindow = mainWindow;
}
private static final Logger LOG = LoggerFactory.getLogger(UiModule.class);
@Provides
@Singleton
Application provideApplication() {
return application;
}
@Provides
@Singleton
@Named("mainWindow")
Stage provideMainWindow() {
return mainWindow;
}
@Provides
@Singleton
DeferredCloser provideDeferredCloser() {
DeferredCloser provideDeferredCloser(@Named("shutdownTaskScheduler") Consumer<Runnable> shutdownTaskScheduler) {
DeferredCloser closer = new DeferredCloser();
Cryptomator.addShutdownTask(() -> {
shutdownTaskScheduler.accept(() -> {
try {
closer.close();
} catch (Exception e) {

View File

@@ -11,6 +11,7 @@ package org.cryptomator.ui.controllers;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -18,6 +19,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import javax.inject.Inject;
import javax.inject.Named;
@@ -25,6 +28,9 @@ import javax.inject.Provider;
import javax.inject.Singleton;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.ui.ExitUtil;
import org.cryptomator.ui.controls.DirectoryListCell;
import org.cryptomator.ui.model.UpgradeStrategies;
import org.cryptomator.ui.model.UpgradeStrategy;
@@ -32,15 +38,15 @@ import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.model.VaultFactory;
import org.cryptomator.ui.model.VaultList;
import org.cryptomator.ui.settings.Localization;
import org.cryptomator.ui.settings.Settings;
import org.cryptomator.ui.settings.VaultSettings;
import org.cryptomator.ui.util.DialogBuilderUtil;
import org.fxmisc.easybind.EasyBind;
import org.fxmisc.easybind.Subscription;
import org.fxmisc.easybind.monadic.MonadicBinding;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import dagger.Lazy;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.binding.Binding;
import javafx.beans.binding.Bindings;
@@ -60,8 +66,10 @@ import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ToggleButton;
import javafx.scene.image.Image;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.text.Font;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
@@ -69,8 +77,13 @@ import javafx.stage.Stage;
public class MainController extends LocalizedFXMLViewController {
private static final Logger LOG = LoggerFactory.getLogger(MainController.class);
private static final String ACTIVE_WINDOW_STYLE_CLASS = "active-window";
private static final String INACTIVE_WINDOW_STYLE_CLASS = "inactive-window";
private final Stage mainWindow;
private final ExitUtil exitUtil;
private final ExecutorService executorService;
private final BlockingQueue<Path> fileOpenRequests;
private final Settings settings;
private final VaultFactory vaultFactoy;
private final Lazy<WelcomeController> welcomeController;
@@ -91,13 +104,18 @@ public class MainController extends LocalizedFXMLViewController {
private final BooleanBinding isShowingSettings;
private final Map<Vault, UnlockedController> unlockedVaults = new HashMap<>();
private Subscription subs = Subscription.EMPTY;
@Inject
public MainController(@Named("mainWindow") Stage mainWindow, Localization localization, Settings settings, VaultFactory vaultFactoy, Lazy<WelcomeController> welcomeController,
Lazy<InitializeController> initializeController, Lazy<NotFoundController> notFoundController, Lazy<UpgradeController> upgradeController, Lazy<UnlockController> unlockController,
Provider<UnlockedController> unlockedControllerProvider, Lazy<ChangePasswordController> changePasswordController, Lazy<SettingsController> settingsController, UpgradeStrategies upgradeStrategies,
VaultList vaults) {
public MainController(@Named("mainWindow") Stage mainWindow, ExecutorService executorService, @Named("fileOpenRequests") BlockingQueue<Path> fileOpenRequests, ExitUtil exitUtil, Localization localization,
Settings settings, VaultFactory vaultFactoy, Lazy<WelcomeController> welcomeController, Lazy<InitializeController> initializeController, Lazy<NotFoundController> notFoundController,
Lazy<UpgradeController> upgradeController, Lazy<UnlockController> unlockController, Provider<UnlockedController> unlockedControllerProvider, Lazy<ChangePasswordController> changePasswordController,
Lazy<SettingsController> settingsController, UpgradeStrategies upgradeStrategies, VaultList vaults) {
super(localization);
this.mainWindow = mainWindow;
this.executorService = executorService;
this.fileOpenRequests = fileOpenRequests;
this.exitUtil = exitUtil;
this.settings = settings;
this.vaultFactoy = vaultFactoy;
this.welcomeController = welcomeController;
@@ -155,10 +173,58 @@ public class MainController extends LocalizedFXMLViewController {
emptyListInstructions.visibleProperty().bind(Bindings.isEmpty(vaults));
changePasswordMenuItem.visibleProperty().bind(isSelectedVaultValid.and(Bindings.isNull(upgradeStrategyForSelectedVault)));
EasyBind.subscribe(selectedVault, this::selectedVaultDidChange);
EasyBind.subscribe(activeController, this::activeControllerDidChange);
EasyBind.subscribe(isShowingSettings, settingsButton::setSelected);
EasyBind.subscribe(addVaultContextMenu.showingProperty(), addVaultButton::setSelected);
subs = subs.and(EasyBind.subscribe(selectedVault, this::selectedVaultDidChange));
subs = subs.and(EasyBind.subscribe(activeController, this::activeControllerDidChange));
subs = subs.and(EasyBind.subscribe(isShowingSettings, settingsButton::setSelected));
subs = subs.and(EasyBind.subscribe(addVaultContextMenu.showingProperty(), addVaultButton::setSelected));
}
@Override
public void initStage(Stage stage) {
super.initStage(stage);
stage.titleProperty().bind(windowTitle());
stage.setResizable(false);
loadFont("/css/ionicons.ttf");
loadFont("/css/fontawesome-webfont.ttf");
if (SystemUtils.IS_OS_MAC_OSX) {
subs = subs.and(EasyBind.includeWhen(mainWindow.getScene().getRoot().getStyleClass(), ACTIVE_WINDOW_STYLE_CLASS, mainWindow.focusedProperty()));
subs = subs.and(EasyBind.includeWhen(mainWindow.getScene().getRoot().getStyleClass(), INACTIVE_WINDOW_STYLE_CLASS, mainWindow.focusedProperty().not()));
Application.setUserAgentStylesheet(getClass().getResource("/css/mac_theme.css").toString());
} else if (SystemUtils.IS_OS_LINUX) {
Application.setUserAgentStylesheet(getClass().getResource("/css/linux_theme.css").toString());
} else if (SystemUtils.IS_OS_WINDOWS) {
stage.getIcons().add(new Image(getClass().getResourceAsStream("/window_icon.png")));
Application.setUserAgentStylesheet(getClass().getResource("/css/win_theme.css").toString());
}
exitUtil.initExitHandler();
listenToFileOpenRequests(stage);
}
private void loadFont(String resourcePath) {
try (InputStream in = getClass().getResourceAsStream(resourcePath)) {
Font.loadFont(in, 12.0);
} catch (IOException e) {
LOG.warn("Error loading font from path: " + resourcePath, e);
}
}
private void listenToFileOpenRequests(Stage stage) {
executorService.submit(() -> {
while (!Thread.interrupted()) {
try {
final Path path = fileOpenRequests.take();
Platform.runLater(() -> {
addVault(path, true);
stage.setIconified(false);
stage.show();
stage.toFront();
stage.requestFocus();
});
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
}
@Override
@@ -218,30 +284,31 @@ public class MainController extends LocalizedFXMLViewController {
/**
* adds the given directory or selects it if it is already in the list of directories.
*
* @param path non-null, writable, existing directory
* @param path to a vault directory or masterkey file
*/
public void addVault(final Path path, boolean select) {
// TODO: `|| !Files.isWritable(path)` is broken on windows. Fix in Java 8u72, see https://bugs.openjdk.java.net/browse/JDK-8034057
if (path == null) {
return;
}
final Path vaultPath;
if (path != null && Files.isDirectory(path)) {
vaultPath = path;
} else if (path != null && Files.isRegularFile(path)) {
vaultPath = path.getParent();
} else {
LOG.warn("Ignoring attempt to add vault with invalid path: {}", path);
return;
}
final VaultSettings vaultSettings = VaultSettings.withRandomId(settings);
vaultSettings.path().set(vaultPath);
final Vault vault = vaultFactoy.get(vaultSettings);
final Vault vault = vaults.stream().filter(v -> v.getPath().equals(vaultPath)).findAny().orElseGet(() -> {
VaultSettings vaultSettings = VaultSettings.withRandomId(settings);
vaultSettings.path().set(vaultPath);
return vaultFactoy.get(vaultSettings);
});
if (!vaults.contains(vault)) {
vaults.add(vault);
}
vaultList.getSelectionModel().select(vault);
if (select) {
vaultList.getSelectionModel().select(vault);
}
}
@FXML

View File

@@ -16,8 +16,8 @@ import javax.inject.Singleton;
import org.apache.commons.lang3.CharUtils;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.ui.settings.Localization;
import org.cryptomator.ui.settings.Settings;
import javafx.beans.binding.Bindings;
import javafx.event.ActionEvent;

View File

@@ -15,6 +15,7 @@ import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Comparator;
import java.util.Map;
import java.util.Optional;
import javax.inject.Inject;
import javax.inject.Named;
@@ -27,9 +28,8 @@ import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.ui.settings.Localization;
import org.cryptomator.ui.settings.Settings;
import org.cryptomator.ui.util.ApplicationVersion;
import org.cryptomator.ui.util.AsyncTaskService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -53,14 +53,17 @@ public class WelcomeController extends LocalizedFXMLViewController {
private static final Logger LOG = LoggerFactory.getLogger(WelcomeController.class);
private final Application app;
private final Optional<String> applicationVersion;
private final Settings settings;
private final Comparator<String> semVerComparator;
private final AsyncTaskService asyncTaskService;
@Inject
public WelcomeController(Application app, Localization localization, Settings settings, @Named("SemVer") Comparator<String> semVerComparator, AsyncTaskService asyncTaskService) {
public WelcomeController(Application app, @Named("applicationVersion") Optional<String> applicationVersion, Localization localization, Settings settings, @Named("SemVer") Comparator<String> semVerComparator,
AsyncTaskService asyncTaskService) {
super(localization);
this.app = app;
this.applicationVersion = applicationVersion;
this.settings = settings;
this.semVerComparator = semVerComparator;
this.asyncTaskService = asyncTaskService;
@@ -112,7 +115,7 @@ public class WelcomeController extends LocalizedFXMLViewController {
HttpClientBuilder httpClientBuilder = HttpClients.custom() //
.disableCookieManagement() //
.setDefaultRequestConfig(requestConfig) //
.setUserAgent("Cryptomator VersionChecker/" + ApplicationVersion.orElse("SNAPSHOT"));
.setUserAgent("Cryptomator VersionChecker/" + applicationVersion.orElse("SNAPSHOT"));
LOG.debug("Checking for updates...");
try (CloseableHttpClient client = httpClientBuilder.build()) {
HttpGet request = new HttpGet("https://cryptomator.org/downloads/latestVersion.json");
@@ -148,7 +151,7 @@ public class WelcomeController extends LocalizedFXMLViewController {
// no version check possible on unsupported OS
return;
}
final String currentVersion = ApplicationVersion.orElse(null);
final String currentVersion = applicationVersion.orElse(null);
LOG.info("Current version: {}, lastest version: {}", currentVersion, latestVersion);
if (currentVersion != null && semVerComparator.compare(currentVersion, latestVersion) < 0) {
final String msg = String.format(localization.getString("welcome.newVersionMessage"), latestVersion, currentVersion);

View File

@@ -1,117 +0,0 @@
/*******************************************************************************
* Copyright (c) 2016 Sebastian Stenzel and others.
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.ui.logging;
import java.io.IOException;
import java.io.Serializable;
import java.net.URISyntaxException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.regex.Pattern;
import org.apache.commons.lang3.SystemUtils;
import org.apache.logging.log4j.core.Filter;
import org.apache.logging.log4j.core.Layout;
import org.apache.logging.log4j.core.appender.AbstractAppender;
import org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender;
import org.apache.logging.log4j.core.appender.FileManager;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginElement;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
import org.apache.logging.log4j.core.layout.PatternLayout;
import org.apache.logging.log4j.core.util.Booleans;
import org.apache.logging.log4j.util.Strings;
/**
* A preconfigured FileAppender only relying on a configurable system property, e.g. <code>-Dcryptomator.logPath=/var/log/cryptomator.log</code>.<br/>
* Other than the normal {@link org.apache.logging.log4j.core.appender.FileAppender} paths can be resolved relative to the users home directory.
*/
@Plugin(name = "ConfigurableFile", category = "Core", elementType = "appender", printObject = true)
public class ConfigurableFileAppender extends AbstractOutputStreamAppender<FileManager> {
private static final long serialVersionUID = -6548221568069606389L;
private static final int DEFAULT_BUFFER_SIZE = 8192;
private static final Pattern DRIVE_LETTER_WITH_PRECEEDING_SLASH = Pattern.compile("^/[A-Z]:", Pattern.CASE_INSENSITIVE);
protected ConfigurableFileAppender(String name, Layout<? extends Serializable> layout, Filter filter, FileManager manager) {
super(name, layout, filter, true, true, manager);
LOGGER.info("Logging to " + manager.getFileName());
}
@PluginFactory
public static AbstractAppender createAppender(@PluginAttribute("name") final String name, @PluginAttribute("pathPropertyName") final String pathPropertyName, @PluginAttribute("append") final String append,
@PluginElement("Layout") Layout<? extends Serializable> layout) {
if (name == null) {
LOGGER.error("No name provided for ConfigurableFileAppender");
return null;
}
if (pathPropertyName == null) {
LOGGER.error("No pathPropertyName provided for ConfigurableFileAppender with name " + name);
return null;
}
final String fileName = System.getProperty(pathPropertyName);
if (Strings.isEmpty(fileName)) {
LOGGER.warn("No log file location provided in system property \"" + pathPropertyName + "\"");
return null;
}
final Path filePath = parsePath(fileName);
if (filePath == null) {
LOGGER.warn("Invalid path \"" + fileName + "\"");
return null;
}
if (!Files.exists(filePath.getParent())) {
try {
Files.createDirectories(filePath.getParent());
} catch (IOException e) {
LOGGER.error("Could not create parent directories for log file located at " + filePath.toString(), e);
return null;
}
}
final boolean shouldAppend = Booleans.parseBoolean(append, true);
if (layout == null) {
layout = PatternLayout.createDefaultLayout();
}
final FileManager manager = FileManager.getFileManager(filePath.toString(), shouldAppend, false, true, null, layout, DEFAULT_BUFFER_SIZE);
return new ConfigurableFileAppender(name, layout, null, manager);
}
private static Path parsePath(String path) {
if (path.startsWith("~/")) {
// home-dir-relative Path:
final Path userHome = FileSystems.getDefault().getPath(SystemUtils.USER_HOME);
return userHome.resolve(path.substring(2));
} else if (path.startsWith("/")) {
// absolute Path:
return FileSystems.getDefault().getPath(path);
} else {
// relative Path:
try {
String jarFileLocation = ConfigurableFileAppender.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath();
if (SystemUtils.IS_OS_WINDOWS && DRIVE_LETTER_WITH_PRECEEDING_SLASH.matcher(jarFileLocation).find()) {
// on windows we need to remove a preceeding slash from "/C:/foo/bar":
jarFileLocation = jarFileLocation.substring(1);
}
final Path workingDir = FileSystems.getDefault().getPath(jarFileLocation).getParent();
return workingDir.resolve(path);
} catch (URISyntaxException e) {
LOGGER.error("Unable to resolve working directory ", e);
return null;
}
}
}
}

View File

@@ -10,6 +10,8 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.cryptolib.api.CryptorProvider;
@@ -38,6 +40,14 @@ public abstract class UpgradeStrategy {
this.vaultVersionAfterUpgrade = vaultVersionAfterUpgrade;
}
static SecureRandom strongSecureRandom() {
try {
return SecureRandom.getInstanceStrong();
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("A strong algorithm must exist in every Java platform.", e);
}
}
/**
* @return Localized title string to display to the user when an upgrade is needed.
*/

View File

@@ -13,10 +13,8 @@ import javax.inject.Inject;
import javax.inject.Singleton;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.cryptolib.api.CryptoLibVersion;
import org.cryptomator.cryptolib.api.CryptoLibVersion.Version;
import org.cryptomator.cryptolib.Cryptors;
import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.cryptolib.api.CryptorProvider;
import org.cryptomator.ui.settings.Localization;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -29,8 +27,8 @@ class UpgradeVersion3DropBundleExtension extends UpgradeStrategy {
private static final Logger LOG = LoggerFactory.getLogger(UpgradeVersion3DropBundleExtension.class);
@Inject
public UpgradeVersion3DropBundleExtension(@CryptoLibVersion(Version.ONE) CryptorProvider version1CryptorProvider, Localization localization) {
super(version1CryptorProvider, localization, 3, 3);
public UpgradeVersion3DropBundleExtension(Localization localization) {
super(Cryptors.version1(UpgradeStrategy.strongSecureRandom()), localization, 3, 3);
}
@Override

View File

@@ -23,10 +23,8 @@ import javax.inject.Singleton;
import org.apache.commons.codec.binary.Base32;
import org.apache.commons.codec.binary.BaseNCodec;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.cryptolib.api.CryptoLibVersion;
import org.cryptomator.cryptolib.api.CryptoLibVersion.Version;
import org.cryptomator.cryptolib.Cryptors;
import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.cryptolib.api.CryptorProvider;
import org.cryptomator.cryptolib.common.MessageDigestSupplier;
import org.cryptomator.ui.settings.Localization;
import org.slf4j.Logger;
@@ -50,8 +48,8 @@ class UpgradeVersion3to4 extends UpgradeStrategy {
private final BaseNCodec base32 = new Base32();
@Inject
public UpgradeVersion3to4(@CryptoLibVersion(Version.ONE) CryptorProvider version1CryptorProvider, Localization localization) {
super(version1CryptorProvider, localization, 3, 4);
public UpgradeVersion3to4(Localization localization) {
super(Cryptors.version1(UpgradeStrategy.strongSecureRandom()), localization, 3, 4);
}
@Override

View File

@@ -20,10 +20,7 @@ import javax.inject.Inject;
import javax.inject.Singleton;
import org.cryptomator.cryptolib.Cryptors;
import org.cryptomator.cryptolib.api.CryptoLibVersion;
import org.cryptomator.cryptolib.api.CryptoLibVersion.Version;
import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.cryptolib.api.CryptorProvider;
import org.cryptomator.cryptolib.api.FileHeader;
import org.cryptomator.ui.settings.Localization;
import org.slf4j.Logger;
@@ -40,8 +37,8 @@ class UpgradeVersion4to5 extends UpgradeStrategy {
private static final Pattern BASE32_PATTERN = Pattern.compile("^([A-Z2-7]{8})*[A-Z2-7=]{8}");
@Inject
public UpgradeVersion4to5(@CryptoLibVersion(Version.ONE) CryptorProvider version1CryptorProvider, Localization localization) {
super(version1CryptorProvider, localization, 4, 5);
public UpgradeVersion4to5(Localization localization) {
super(Cryptors.version1(UpgradeStrategy.strongSecureRandom()), localization, 4, 5);
}
@Override

View File

@@ -26,6 +26,8 @@ import javax.inject.Inject;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.LazyInitializer;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.cryptofs.CryptoFileSystem;
import org.cryptomator.cryptofs.CryptoFileSystemProperties;
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
@@ -37,8 +39,6 @@ import org.cryptomator.frontend.webdav.mount.Mounter.Mount;
import org.cryptomator.frontend.webdav.mount.Mounter.MountParam;
import org.cryptomator.frontend.webdav.servlet.WebDavServletController;
import org.cryptomator.ui.model.VaultModule.PerVault;
import org.cryptomator.ui.settings.Settings;
import org.cryptomator.ui.settings.VaultSettings;
import org.cryptomator.ui.util.DeferredCloser;
import org.fxmisc.easybind.EasyBind;
import org.slf4j.Logger;

View File

@@ -15,4 +15,11 @@ public interface VaultComponent {
Vault vault();
@Subcomponent.Builder
interface Builder {
Builder vaultModule(VaultModule module);
VaultComponent build();
}
}

View File

@@ -14,18 +14,18 @@ import java.util.concurrent.ConcurrentMap;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.cryptomator.ui.CryptomatorComponent;
import org.cryptomator.ui.settings.VaultSettings;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.ui.model.VaultComponent.Builder;
@Singleton
public class VaultFactory {
private final CryptomatorComponent cryptomatorComponent;
private final Builder vaultComponentBuilder;
private final ConcurrentMap<VaultSettings, Vault> vaults = new ConcurrentHashMap<>();
@Inject
public VaultFactory(CryptomatorComponent cryptomatorComponent) {
this.cryptomatorComponent = cryptomatorComponent;
public VaultFactory(VaultComponent.Builder vaultComponentBuilder) {
this.vaultComponentBuilder = vaultComponentBuilder;
}
public Vault get(VaultSettings vaultSettings) {
@@ -34,7 +34,7 @@ public class VaultFactory {
private Vault create(VaultSettings vaultSettings) {
VaultModule module = new VaultModule(vaultSettings);
VaultComponent comp = cryptomatorComponent.newVaultComponent(module);
VaultComponent comp = vaultComponentBuilder.vaultModule(module).build();
return comp.vault();
}

View File

@@ -11,8 +11,8 @@ import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.cryptomator.ui.settings.Settings;
import org.cryptomator.ui.settings.VaultSettings;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.VaultSettings;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;

View File

@@ -12,7 +12,7 @@ import java.util.Objects;
import javax.inject.Scope;
import org.cryptomator.ui.settings.VaultSettings;
import org.cryptomator.common.settings.VaultSettings;
import dagger.Module;
import dagger.Provides;

View File

@@ -12,6 +12,10 @@ import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.stage.Window;
/**
* @deprecated use https://github.com/TomasMikula/EasyBind#conditional-collection-membership
*/
@Deprecated
public class ActiveWindowStyleSupport implements ChangeListener<Boolean> {
public static final String ACTIVE_WINDOW_STYLE_CLASS = "active-window";

View File

@@ -1,374 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014, 2016 cryptomator.org
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Tillmann Gaida - initial implementation
******************************************************************************/
package org.cryptomator.ui.util;
import java.io.Closeable;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.ClosedSelectorException;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.prefs.Preferences;
import org.apache.commons.io.IOUtils;
import org.cryptomator.ui.Cryptomator;
import org.cryptomator.ui.util.ListenerRegistry.ListenerRegistration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Classes and methods to manage running this application in a mode, which only
* shows one instance.
*
* @author Tillmann Gaida
*/
public class SingleInstanceManager {
private static final Logger LOG = LoggerFactory.getLogger(SingleInstanceManager.class);
/**
* Connection to a running instance
*/
public static class RemoteInstance implements Closeable {
final SocketChannel channel;
RemoteInstance(SocketChannel channel) {
super();
this.channel = channel;
}
/**
* Sends a message to the running instance.
*
* @param string
* May not be longer than 2^16 - 1 bytes.
* @param timeout
* timeout in milliseconds. this should be larger than the
* precision of {@link System#currentTimeMillis()}.
* @return true if the message was sent within the given timeout.
* @throws IOException
*/
public boolean sendMessage(String string, long timeout) throws IOException {
Objects.requireNonNull(string);
byte[] message = string.getBytes(StandardCharsets.UTF_8);
if (message.length >= 256 * 256) {
throw new IOException("Message too long.");
}
ByteBuffer buf = ByteBuffer.allocate(message.length + 2);
buf.put((byte) (message.length / 256));
buf.put((byte) (message.length % 256));
buf.put(message);
buf.flip();
TimeoutTask.attempt(t -> {
if (channel.write(buf) < 0) {
return true;
}
return !buf.hasRemaining();
}, timeout, 10);
return !buf.hasRemaining();
}
@Override
public void close() throws IOException {
channel.close();
}
public int getRemotePort() throws IOException {
return ((InetSocketAddress) channel.getRemoteAddress()).getPort();
}
}
public static interface MessageListener {
void handleMessage(String message);
}
/**
* Represents a socket making this the main instance of the application.
*/
public static class LocalInstance implements Closeable {
private class ChannelState {
ByteBuffer write = ByteBuffer.wrap(applicationKey.getBytes(StandardCharsets.UTF_8));
ByteBuffer readLength = ByteBuffer.allocate(2);
ByteBuffer readMessage = null;
}
final ListenerRegistry<MessageListener, String> registry = new ListenerRegistry<>(MessageListener::handleMessage);
final String applicationKey;
final ServerSocketChannel channel;
final Selector selector;
final int port;
public LocalInstance(String applicationKey, ServerSocketChannel channel, Selector selector, int port) {
Objects.requireNonNull(applicationKey);
this.applicationKey = applicationKey;
this.channel = channel;
this.selector = selector;
this.port = port;
}
/**
* Register a listener for
*
* @param listener
* @return
*/
public ListenerRegistration registerListener(MessageListener listener) {
Objects.requireNonNull(listener);
return registry.registerListener(listener);
}
void handleSelection(SelectionKey key) throws IOException {
if (key.isAcceptable()) {
final SocketChannel accepted = channel.accept();
SelectionKey keyOfAcceptedConnection = null;
try {
if (accepted != null) {
LOG.debug("accepted incoming connection");
accepted.configureBlocking(false);
keyOfAcceptedConnection = accepted.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
}
} finally {
if (keyOfAcceptedConnection == null) {
accepted.close();
}
}
}
if (key.attachment() == null) {
key.attach(new ChannelState());
}
ChannelState state = (ChannelState) key.attachment();
if (key.isWritable() && state.write != null) {
((WritableByteChannel) key.channel()).write(state.write);
if (!state.write.hasRemaining()) {
state.write = null;
}
LOG.trace("wrote welcome. switching to read only.");
key.interestOps(SelectionKey.OP_READ);
}
if (key.isReadable()) {
ByteBuffer buffer = state.readLength != null ? state.readLength : state.readMessage;
if (((ReadableByteChannel) key.channel()).read(buffer) < 0) {
key.cancel();
}
if (!buffer.hasRemaining()) {
buffer.flip();
if (state.readLength != null) {
int length = (buffer.get() + 256) % 256;
length = length * 256 + ((buffer.get() + 256) % 256);
state.readLength = null;
state.readMessage = ByteBuffer.allocate(length);
} else {
byte[] bytes = new byte[buffer.limit()];
buffer.get(bytes);
state.readMessage = null;
state.readLength = ByteBuffer.allocate(2);
registry.broadcast(new String(bytes, "UTF-8"));
}
}
}
}
@Override
public void close() {
IOUtils.closeQuietly(selector);
IOUtils.closeQuietly(channel);
if (getSavedPort(applicationKey).orElse(-1).equals(port)) {
Preferences.userNodeForPackage(Cryptomator.class).remove(applicationKey);
}
}
void selectionLoop() {
try {
final Set<SelectionKey> keysToRemove = new HashSet<>();
while (selector.select() > 0) {
final Set<SelectionKey> keys = selector.selectedKeys();
for (SelectionKey key : keys) {
if (Thread.interrupted()) {
return;
}
try {
handleSelection(key);
} catch (IOException | IllegalStateException e) {
LOG.error("exception in selector", e);
} finally {
keysToRemove.add(key);
}
}
keys.removeAll(keysToRemove);
}
} catch (ClosedSelectorException e) {
return;
} catch (Exception e) {
LOG.error("error while selecting", e);
}
}
}
/**
* Checks if there is a valid port at
* {@link Preferences#userNodeForPackage(Class)} for {@link Cryptomator} under the
* given applicationKey, tries to connect to the port at the loopback
* address and checks if the port identifies with the applicationKey.
*
* @param applicationKey
* key used to load the port and check the identity of the
* connection.
* @return
*/
public static Optional<RemoteInstance> getRemoteInstance(String applicationKey) {
Optional<Integer> port = getSavedPort(applicationKey);
if (!port.isPresent()) {
return Optional.empty();
}
SocketChannel channel = null;
boolean close = true;
try {
channel = SocketChannel.open();
channel.configureBlocking(false);
LOG.debug("connecting to instance {}", port.get());
channel.connect(new InetSocketAddress(InetAddress.getLoopbackAddress(), port.get()));
SocketChannel fChannel = channel;
if (!TimeoutTask.attempt(t -> fChannel.finishConnect(), 1000, 10)) {
return Optional.empty();
}
LOG.debug("connected to instance {}", port.get());
final byte[] bytes = applicationKey.getBytes(StandardCharsets.UTF_8);
ByteBuffer buf = ByteBuffer.allocate(bytes.length);
tryFill(channel, buf, 1000);
if (buf.hasRemaining()) {
return Optional.empty();
}
buf.flip();
for (int i = 0; i < bytes.length; i++) {
if (buf.get() != bytes[i]) {
return Optional.empty();
}
}
close = false;
return Optional.of(new RemoteInstance(channel));
} catch (Exception e) {
return Optional.empty();
} finally {
if (close) {
IOUtils.closeQuietly(channel);
}
}
}
static Optional<Integer> getSavedPort(String applicationKey) {
int port = Preferences.userNodeForPackage(Cryptomator.class).getInt(applicationKey, -1);
if (port == -1) {
LOG.info("no running instance found");
return Optional.empty();
}
return Optional.of(port);
}
/**
* Creates a server socket on a free port and saves the port in
* {@link Preferences#userNodeForPackage(Class)} for {@link Cryptomator} under the
* given applicationKey.
*
* @param applicationKey
* key used to save the port and identify upon connection.
* @param exec
* the task which is submitted is interruptable.
* @return
* @throws IOException
*/
public static LocalInstance startLocalInstance(String applicationKey, ExecutorService exec) throws IOException {
final ServerSocketChannel channel = ServerSocketChannel.open();
boolean success = false;
try {
channel.configureBlocking(false);
channel.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0));
final int port = ((InetSocketAddress) channel.getLocalAddress()).getPort();
Preferences.userNodeForPackage(Cryptomator.class).putInt(applicationKey, port);
LOG.debug("InstanceManager bound to port {}", port);
Selector selector = Selector.open();
channel.register(selector, SelectionKey.OP_ACCEPT);
LocalInstance instance = new LocalInstance(applicationKey, channel, selector, port);
exec.submit(instance::selectionLoop);
success = true;
return instance;
} finally {
if (!success) {
channel.close();
}
}
}
/**
* tries to fill the given buffer for the given time
*
* @param channel
* @param buf
* @param timeout
* @throws ClosedChannelException
* @throws IOException
*/
public static <T extends SelectableChannel & ReadableByteChannel> void tryFill(T channel, final ByteBuffer buf, int timeout) throws IOException {
if (channel.isBlocking()) {
throw new IllegalStateException("Channel is in blocking mode.");
}
try (Selector selector = Selector.open()) {
channel.register(selector, SelectionKey.OP_READ);
TimeoutTask.attempt(remainingTime -> {
if (!buf.hasRemaining()) {
return true;
}
if (selector.select(remainingTime) > 0) {
if (channel.read(buf) < 0) {
return true;
}
}
return !buf.hasRemaining();
}, timeout, 1);
}
}
}

View File

@@ -1,20 +0,0 @@
/*******************************************************************************
* Copyright (c) 2016 Sebastian Stenzel and others.
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.ui;
import org.junit.Test;
public class MainApplicationTest {
@Test
public void testInjection() throws Exception {
new MainApplication();
}
}

View File

@@ -1,200 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014, 2016 cryptomator.org
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Tillmann Gaida - initial implementation
******************************************************************************/
package org.cryptomator.ui.util;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Collections;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.TimeUnit;
import org.cryptomator.ui.util.SingleInstanceManager.LocalInstance;
import org.cryptomator.ui.util.SingleInstanceManager.MessageListener;
import org.cryptomator.ui.util.SingleInstanceManager.RemoteInstance;
import org.junit.Test;
public class SingleInstanceManagerTest {
@Test(timeout = 10000)
public void testTryFillTimeout() throws Exception {
try (final ServerSocket socket = new ServerSocket(0)) {
// we need to asynchronously accept the connection
final ForkJoinTask<?> forked = ForkJoinTask.adapt(() -> {
try {
socket.setSoTimeout(1000);
socket.accept();
} catch (Exception e) {
throw new RuntimeException(e);
}
}).fork();
try (SocketChannel channel = SocketChannel.open()) {
channel.configureBlocking(false);
channel.connect(new InetSocketAddress(InetAddress.getLoopbackAddress(), socket.getLocalPort()));
TimeoutTask.attempt(t -> channel.finishConnect(), 1000, 1);
final ByteBuffer buffer = ByteBuffer.allocate(1);
SingleInstanceManager.tryFill(channel, buffer, 1000);
assertTrue(buffer.hasRemaining());
}
forked.join();
}
}
@Test(timeout = 10000)
public void testTryFill() throws Exception {
try (final ServerSocket socket = new ServerSocket(0)) {
// we need to asynchronously accept the connection
final ForkJoinTask<?> forked = ForkJoinTask.adapt(() -> {
try {
socket.setSoTimeout(1000);
socket.accept().getOutputStream().write(1);
} catch (Exception e) {
throw new RuntimeException(e);
}
}).fork();
try (SocketChannel channel = SocketChannel.open()) {
channel.configureBlocking(false);
channel.connect(new InetSocketAddress(InetAddress.getLoopbackAddress(), socket.getLocalPort()));
TimeoutTask.attempt(t -> channel.finishConnect(), 1000, 1);
final ByteBuffer buffer = ByteBuffer.allocate(1);
SingleInstanceManager.tryFill(channel, buffer, 1000);
assertFalse(buffer.hasRemaining());
}
forked.join();
}
}
String appKey = "APPKEY";
@Test
public void testOneMessage() throws Exception {
ExecutorService exec = Executors.newCachedThreadPool();
try {
final LocalInstance server = SingleInstanceManager.startLocalInstance(appKey, exec);
final Optional<RemoteInstance> r = SingleInstanceManager.getRemoteInstance(appKey);
CountDownLatch latch = new CountDownLatch(1);
final MessageListener listener = spy(new MessageListener() {
@Override
public void handleMessage(String message) {
latch.countDown();
}
});
server.registerListener(listener);
assertTrue(r.isPresent());
String message = "Is this thing on?";
assertTrue(r.get().sendMessage(message, 1000));
System.out.println("wrote message");
latch.await(10, TimeUnit.SECONDS);
verify(listener).handleMessage(message);
} finally {
exec.shutdownNow();
}
}
@Test(timeout = 60000)
public void testALotOfMessages() throws Exception {
final int connectors = 256;
final int messagesPerConnector = 256;
ExecutorService exec = Executors.newSingleThreadExecutor();
ExecutorService exec2 = Executors.newFixedThreadPool(16);
try (final LocalInstance server = SingleInstanceManager.startLocalInstance(appKey, exec)) {
Set<String> sentMessages = new ConcurrentSkipListSet<>();
Set<String> receivedMessages = new HashSet<>();
CountDownLatch sendLatch = new CountDownLatch(connectors);
CountDownLatch receiveLatch = new CountDownLatch(connectors * messagesPerConnector);
server.registerListener(message -> {
receivedMessages.add(message);
receiveLatch.countDown();
});
Set<RemoteInstance> instances = Collections.synchronizedSet(new HashSet<>());
for (int i = 0; i < connectors; i++) {
exec2.submit(() -> {
try {
final Optional<RemoteInstance> r = SingleInstanceManager.getRemoteInstance(appKey);
assertTrue(r.isPresent());
instances.add(r.get());
for (int j = 0; j < messagesPerConnector; j++) {
exec2.submit(() -> {
try {
for (;;) {
final String message = UUID.randomUUID().toString();
if (!sentMessages.add(message)) {
continue;
}
r.get().sendMessage(message, 1000);
break;
}
} catch (Exception e) {
e.printStackTrace();
}
});
}
sendLatch.countDown();
} catch (Throwable e) {
e.printStackTrace();
}
});
}
assertTrue(sendLatch.await(1, TimeUnit.MINUTES));
exec2.shutdown();
assertTrue(exec2.awaitTermination(1, TimeUnit.MINUTES));
assertTrue(receiveLatch.await(1, TimeUnit.MINUTES));
assertEquals(sentMessages, receivedMessages);
for (RemoteInstance remoteInstance : instances) {
try {
remoteInstance.close();
} catch (Exception e) {
e.printStackTrace();
}
}
} finally {
exec.shutdownNow();
exec2.shutdownNow();
}
}
}