mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-17 18:21:26 +00:00
Refactored Cryptomator UI. Extracted Launcher to its own Maven module.
This commit is contained in:
1
main/commons-test/.gitignore
vendored
1
main/commons-test/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
/target/
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
57
main/launcher/pom.xml
Normal 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>
|
||||
@@ -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) {
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.cryptomator.launcher;
|
||||
|
||||
public interface InterProcessCommunicationProtocol {
|
||||
void handleLaunchArgs(String[] args);
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
@@ -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"});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
33
main/launcher/src/test/resources/log4j2.xml
Normal file
33
main/launcher/src/test/resources/log4j2.xml
Normal 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>
|
||||
52
main/pom.xml
52
main/pom.xml
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
33
main/uber-jar/src/main/resources/log4j2.xml
Normal file
33
main/uber-jar/src/main/resources/log4j2.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -15,4 +15,11 @@ public interface VaultComponent {
|
||||
|
||||
Vault vault();
|
||||
|
||||
@Subcomponent.Builder
|
||||
interface Builder {
|
||||
Builder vaultModule(VaultModule module);
|
||||
|
||||
VaultComponent build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user