From 93b2a4e07af716a9c5ba31d88a357eb2bdf1a8ef Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 18 Apr 2017 13:40:59 +0200 Subject: [PATCH] Refactored Cryptomator UI. Extracted Launcher to its own Maven module. --- main/commons-test/.gitignore | 1 - main/commons-test/pom.xml | 43 -- main/commons/pom.xml | 31 +- .../common}/settings/Settings.java | 2 +- .../common}/settings/SettingsJsonAdapter.java | 2 +- .../common}/settings/SettingsProvider.java | 2 +- .../common}/settings/VaultSettings.java | 2 +- .../settings/VaultSettingsJsonAdapter.java | 2 +- .../settings/SettingsJsonAdapterTest.java | 2 +- .../VaultSettingsJsonAdapterTest.java | 2 +- .../common}/settings/VaultSettingsTest.java | 2 +- main/jacoco-report/pom.xml | 13 +- main/keychain/pom.xml | 7 +- main/launcher/pom.xml | 57 +++ .../launcher}/ApplicationVersion.java | 4 +- .../launcher/CleanShutdownPerformer.java | 35 ++ .../org/cryptomator/launcher/Cryptomator.java | 54 +++ .../launcher/FileOpenRequestHandler.java | 102 +++++ .../InterProcessCommunicationProtocol.java | 5 + .../launcher/InterProcessCommunicator.java | 232 +++++++++++ .../launcher/LauncherComponent.java | 18 + .../cryptomator/launcher/LauncherModule.java | 63 +++ .../cryptomator/launcher/MainApplication.java | 58 +++ .../logging/ConfigurableFileAppender.java | 132 +++++++ .../org/cryptomator/logging}/DebugMode.java | 4 +- .../src/main/resources/log4j2.xml | 2 +- .../launcher/FileOpenRequestHandlerTest.java | 72 ++++ .../InterProcessCommunicatorTest.java | 82 ++++ main/launcher/src/test/resources/log4j2.xml | 33 ++ main/pom.xml | 52 ++- main/uber-jar/pom.xml | 46 +-- main/uber-jar/src/main/resources/log4j2.xml | 33 ++ main/ui/pom.xml | 32 +- .../java/org/cryptomator/ui/Cryptomator.java | 154 -------- .../cryptomator/ui/CryptomatorComponent.java | 38 -- .../java/org/cryptomator/ui/ExitUtil.java | 14 +- .../org/cryptomator/ui/MainApplication.java | 187 --------- .../{CryptomatorModule.java => UiModule.java} | 39 +- .../ui/controllers/MainController.java | 107 ++++- .../ui/controllers/SettingsController.java | 2 +- .../ui/controllers/WelcomeController.java | 13 +- .../ui/logging/ConfigurableFileAppender.java | 117 ------ .../cryptomator/ui/model/UpgradeStrategy.java | 10 + .../UpgradeVersion3DropBundleExtension.java | 8 +- .../ui/model/UpgradeVersion3to4.java | 8 +- .../ui/model/UpgradeVersion4to5.java | 7 +- .../java/org/cryptomator/ui/model/Vault.java | 4 +- .../cryptomator/ui/model/VaultComponent.java | 7 + .../cryptomator/ui/model/VaultFactory.java | 12 +- .../org/cryptomator/ui/model/VaultList.java | 4 +- .../org/cryptomator/ui/model/VaultModule.java | 2 +- .../ui/util/ActiveWindowStyleSupport.java | 4 + .../ui/util/SingleInstanceManager.java | 374 ------------------ .../cryptomator/ui/MainApplicationTest.java | 20 - .../ui/util/SingleInstanceManagerTest.java | 200 ---------- 55 files changed, 1234 insertions(+), 1324 deletions(-) delete mode 100644 main/commons-test/.gitignore delete mode 100644 main/commons-test/pom.xml rename main/{ui/src/main/java/org/cryptomator/ui => commons/src/main/java/org/cryptomator/common}/settings/Settings.java (98%) rename main/{ui/src/main/java/org/cryptomator/ui => commons/src/main/java/org/cryptomator/common}/settings/SettingsJsonAdapter.java (98%) rename main/{ui/src/main/java/org/cryptomator/ui => commons/src/main/java/org/cryptomator/common}/settings/SettingsProvider.java (99%) rename main/{ui/src/main/java/org/cryptomator/ui => commons/src/main/java/org/cryptomator/common}/settings/VaultSettings.java (99%) rename main/{ui/src/main/java/org/cryptomator/ui => commons/src/main/java/org/cryptomator/common}/settings/VaultSettingsJsonAdapter.java (98%) rename main/{ui/src/test/java/org/cryptomator/ui => commons/src/test/java/org/cryptomator/common}/settings/SettingsJsonAdapterTest.java (97%) rename main/{ui/src/test/java/org/cryptomator/ui => commons/src/test/java/org/cryptomator/common}/settings/VaultSettingsJsonAdapterTest.java (97%) rename main/{ui/src/test/java/org/cryptomator/ui => commons/src/test/java/org/cryptomator/common}/settings/VaultSettingsTest.java (95%) create mode 100644 main/launcher/pom.xml rename main/{ui/src/main/java/org/cryptomator/ui/util => launcher/src/main/java/org/cryptomator/launcher}/ApplicationVersion.java (89%) create mode 100644 main/launcher/src/main/java/org/cryptomator/launcher/CleanShutdownPerformer.java create mode 100644 main/launcher/src/main/java/org/cryptomator/launcher/Cryptomator.java create mode 100644 main/launcher/src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java create mode 100644 main/launcher/src/main/java/org/cryptomator/launcher/InterProcessCommunicationProtocol.java create mode 100644 main/launcher/src/main/java/org/cryptomator/launcher/InterProcessCommunicator.java create mode 100644 main/launcher/src/main/java/org/cryptomator/launcher/LauncherComponent.java create mode 100644 main/launcher/src/main/java/org/cryptomator/launcher/LauncherModule.java create mode 100644 main/launcher/src/main/java/org/cryptomator/launcher/MainApplication.java create mode 100644 main/launcher/src/main/java/org/cryptomator/logging/ConfigurableFileAppender.java rename main/{ui/src/main/java/org/cryptomator/ui => launcher/src/main/java/org/cryptomator/logging}/DebugMode.java (96%) rename main/{ui => launcher}/src/main/resources/log4j2.xml (95%) create mode 100644 main/launcher/src/test/java/org/cryptomator/launcher/FileOpenRequestHandlerTest.java create mode 100644 main/launcher/src/test/java/org/cryptomator/launcher/InterProcessCommunicatorTest.java create mode 100644 main/launcher/src/test/resources/log4j2.xml create mode 100644 main/uber-jar/src/main/resources/log4j2.xml delete mode 100644 main/ui/src/main/java/org/cryptomator/ui/Cryptomator.java delete mode 100644 main/ui/src/main/java/org/cryptomator/ui/CryptomatorComponent.java delete mode 100644 main/ui/src/main/java/org/cryptomator/ui/MainApplication.java rename main/ui/src/main/java/org/cryptomator/ui/{CryptomatorModule.java => UiModule.java} (72%) delete mode 100644 main/ui/src/main/java/org/cryptomator/ui/logging/ConfigurableFileAppender.java delete mode 100644 main/ui/src/main/java/org/cryptomator/ui/util/SingleInstanceManager.java delete mode 100644 main/ui/src/test/java/org/cryptomator/ui/MainApplicationTest.java delete mode 100644 main/ui/src/test/java/org/cryptomator/ui/util/SingleInstanceManagerTest.java diff --git a/main/commons-test/.gitignore b/main/commons-test/.gitignore deleted file mode 100644 index b83d22266..000000000 --- a/main/commons-test/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target/ diff --git a/main/commons-test/pom.xml b/main/commons-test/pom.xml deleted file mode 100644 index 9f0055118..000000000 --- a/main/commons-test/pom.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - 4.0.0 - - org.cryptomator - main - 1.3.0-SNAPSHOT - - commons-test - Cryptomator common test dependencies - Shared utilities for tests - - - - org.cryptomator - commons - - - - junit - junit - - - org.mockito - mockito-core - - - de.bechte.junit - junit-hierarchicalcontextrunner - - - org.hamcrest - hamcrest-all - - - - diff --git a/main/commons/pom.xml b/main/commons/pom.xml index b3c5595ca..2ab70cf6e 100644 --- a/main/commons/pom.xml +++ b/main/commons/pom.xml @@ -13,7 +13,7 @@ 1.3.0-SNAPSHOT commons - Cryptomator common + Cryptomator Commons Shared utilities @@ -26,6 +26,14 @@ org.apache.commons commons-lang3 + + com.google.code.gson + gson + + + org.fxmisc.easybind + easybind + @@ -38,25 +46,10 @@ provided - + - junit - junit - test - - - org.mockito - mockito-core - test - - - de.bechte.junit - junit-hierarchicalcontextrunner - test - - - org.hamcrest - hamcrest-all + org.slf4j + slf4j-simple test diff --git a/main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java b/main/commons/src/main/java/org/cryptomator/common/settings/Settings.java similarity index 98% rename from main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java rename to main/commons/src/main/java/org/cryptomator/common/settings/Settings.java index 5a1edc66b..67fe4cb53 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java +++ b/main/commons/src/main/java/org/cryptomator/common/settings/Settings.java @@ -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; diff --git a/main/ui/src/main/java/org/cryptomator/ui/settings/SettingsJsonAdapter.java b/main/commons/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java similarity index 98% rename from main/ui/src/main/java/org/cryptomator/ui/settings/SettingsJsonAdapter.java rename to main/commons/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java index 456f94c7e..ae3e301eb 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/settings/SettingsJsonAdapter.java +++ b/main/commons/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java @@ -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; diff --git a/main/ui/src/main/java/org/cryptomator/ui/settings/SettingsProvider.java b/main/commons/src/main/java/org/cryptomator/common/settings/SettingsProvider.java similarity index 99% rename from main/ui/src/main/java/org/cryptomator/ui/settings/SettingsProvider.java rename to main/commons/src/main/java/org/cryptomator/common/settings/SettingsProvider.java index d074f7f68..b413a7e23 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/settings/SettingsProvider.java +++ b/main/commons/src/main/java/org/cryptomator/common/settings/SettingsProvider.java @@ -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; diff --git a/main/ui/src/main/java/org/cryptomator/ui/settings/VaultSettings.java b/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java similarity index 99% rename from main/ui/src/main/java/org/cryptomator/ui/settings/VaultSettings.java rename to main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java index 66b6419e2..915a4e487 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/settings/VaultSettings.java +++ b/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java @@ -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; diff --git a/main/ui/src/main/java/org/cryptomator/ui/settings/VaultSettingsJsonAdapter.java b/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java similarity index 98% rename from main/ui/src/main/java/org/cryptomator/ui/settings/VaultSettingsJsonAdapter.java rename to main/commons/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java index a7c696ab8..7fd9bc407 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/settings/VaultSettingsJsonAdapter.java +++ b/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java @@ -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; diff --git a/main/ui/src/test/java/org/cryptomator/ui/settings/SettingsJsonAdapterTest.java b/main/commons/src/test/java/org/cryptomator/common/settings/SettingsJsonAdapterTest.java similarity index 97% rename from main/ui/src/test/java/org/cryptomator/ui/settings/SettingsJsonAdapterTest.java rename to main/commons/src/test/java/org/cryptomator/common/settings/SettingsJsonAdapterTest.java index 88aabd0a2..5ce31767e 100644 --- a/main/ui/src/test/java/org/cryptomator/ui/settings/SettingsJsonAdapterTest.java +++ b/main/commons/src/test/java/org/cryptomator/common/settings/SettingsJsonAdapterTest.java @@ -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; diff --git a/main/ui/src/test/java/org/cryptomator/ui/settings/VaultSettingsJsonAdapterTest.java b/main/commons/src/test/java/org/cryptomator/common/settings/VaultSettingsJsonAdapterTest.java similarity index 97% rename from main/ui/src/test/java/org/cryptomator/ui/settings/VaultSettingsJsonAdapterTest.java rename to main/commons/src/test/java/org/cryptomator/common/settings/VaultSettingsJsonAdapterTest.java index ee38acf8e..0c2604e10 100644 --- a/main/ui/src/test/java/org/cryptomator/ui/settings/VaultSettingsJsonAdapterTest.java +++ b/main/commons/src/test/java/org/cryptomator/common/settings/VaultSettingsJsonAdapterTest.java @@ -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; diff --git a/main/ui/src/test/java/org/cryptomator/ui/settings/VaultSettingsTest.java b/main/commons/src/test/java/org/cryptomator/common/settings/VaultSettingsTest.java similarity index 95% rename from main/ui/src/test/java/org/cryptomator/ui/settings/VaultSettingsTest.java rename to main/commons/src/test/java/org/cryptomator/common/settings/VaultSettingsTest.java index 604e43ed8..b19412847 100644 --- a/main/ui/src/test/java/org/cryptomator/ui/settings/VaultSettingsTest.java +++ b/main/commons/src/test/java/org/cryptomator/common/settings/VaultSettingsTest.java @@ -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; diff --git a/main/jacoco-report/pom.xml b/main/jacoco-report/pom.xml index 17261c267..2079aed52 100644 --- a/main/jacoco-report/pom.xml +++ b/main/jacoco-report/pom.xml @@ -9,18 +9,7 @@ jacoco-report Cryptomator Code Coverage Report - - - - - org.cryptomator - commons - - - org.cryptomator - commons-test - - + pom diff --git a/main/keychain/pom.xml b/main/keychain/pom.xml index 3976e2297..d5429fb69 100644 --- a/main/keychain/pom.xml +++ b/main/keychain/pom.xml @@ -37,10 +37,11 @@ provided - + - org.cryptomator - commons-test + org.slf4j + slf4j-simple + test \ No newline at end of file diff --git a/main/launcher/pom.xml b/main/launcher/pom.xml new file mode 100644 index 000000000..25ff891e7 --- /dev/null +++ b/main/launcher/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + org.cryptomator + main + 1.3.0-SNAPSHOT + + launcher + Cryptomator Launcher + + + + org.cryptomator + commons + + + org.cryptomator + ui + + + + + com.google.guava + guava + + + org.apache.commons + commons-lang3 + + + + + com.google.dagger + dagger + + + com.google.dagger + dagger-compiler + provided + + + + + org.apache.logging.log4j + log4j-core + + + org.apache.logging.log4j + log4j-slf4j-impl + + + org.apache.logging.log4j + log4j-jul + + + \ No newline at end of file diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/ApplicationVersion.java b/main/launcher/src/main/java/org/cryptomator/launcher/ApplicationVersion.java similarity index 89% rename from main/ui/src/main/java/org/cryptomator/ui/util/ApplicationVersion.java rename to main/launcher/src/main/java/org/cryptomator/launcher/ApplicationVersion.java index 8b466b099..6a60c5e86 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/util/ApplicationVersion.java +++ b/main/launcher/src/main/java/org/cryptomator/launcher/ApplicationVersion.java @@ -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) { diff --git a/main/launcher/src/main/java/org/cryptomator/launcher/CleanShutdownPerformer.java b/main/launcher/src/main/java/org/cryptomator/launcher/CleanShutdownPerformer.java new file mode 100644 index 000000000..0e932647e --- /dev/null +++ b/main/launcher/src/main/java/org/cryptomator/launcher/CleanShutdownPerformer.java @@ -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 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()); + } +} \ No newline at end of file diff --git a/main/launcher/src/main/java/org/cryptomator/launcher/Cryptomator.java b/main/launcher/src/main/java/org/cryptomator/launcher/Cryptomator.java new file mode 100644 index 000000000..17b1096e5 --- /dev/null +++ b/main/launcher/src/main/java/org/cryptomator/launcher/Cryptomator.java @@ -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 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); + } + + } + +} diff --git a/main/launcher/src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java b/main/launcher/src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java new file mode 100644 index 000000000..fdaafe483 --- /dev/null +++ b/main/launcher/src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java @@ -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 fileOpenRequests; + + public FileOpenRequestHandler(BlockingQueue 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 ff = (List) 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; + } + } + +} diff --git a/main/launcher/src/main/java/org/cryptomator/launcher/InterProcessCommunicationProtocol.java b/main/launcher/src/main/java/org/cryptomator/launcher/InterProcessCommunicationProtocol.java new file mode 100644 index 000000000..98e2eafb6 --- /dev/null +++ b/main/launcher/src/main/java/org/cryptomator/launcher/InterProcessCommunicationProtocol.java @@ -0,0 +1,5 @@ +package org.cryptomator.launcher; + +public interface InterProcessCommunicationProtocol { + void handleLaunchArgs(String[] args); +} \ No newline at end of file diff --git a/main/launcher/src/main/java/org/cryptomator/launcher/InterProcessCommunicator.java b/main/launcher/src/main/java/org/cryptomator/launcher/InterProcessCommunicator.java new file mode 100644 index 000000000..a021067c0 --- /dev/null +++ b/main/launcher/src/main/java/org/cryptomator/launcher/InterProcessCommunicator.java @@ -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."); + } + } + } + +} diff --git a/main/launcher/src/main/java/org/cryptomator/launcher/LauncherComponent.java b/main/launcher/src/main/java/org/cryptomator/launcher/LauncherComponent.java new file mode 100644 index 000000000..06a50b024 --- /dev/null +++ b/main/launcher/src/main/java/org/cryptomator/launcher/LauncherComponent.java @@ -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(); + +} diff --git a/main/launcher/src/main/java/org/cryptomator/launcher/LauncherModule.java b/main/launcher/src/main/java/org/cryptomator/launcher/LauncherModule.java new file mode 100644 index 000000000..0c1aba68c --- /dev/null +++ b/main/launcher/src/main/java/org/cryptomator/launcher/LauncherModule.java @@ -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 provideApplicationVersion() { + return ApplicationVersion.get(); + } + + @Provides + @Singleton + @Named("mainWindow") + Stage provideMainWindow() { + return mainWindow; + } + + @Provides + @Singleton + @Named("fileOpenRequests") + BlockingQueue provideFileOpenRequests() { + return Cryptomator.FILE_OPEN_REQUESTS; + } + + @Provides + @Singleton + @Named("shutdownTaskScheduler") + Consumer provideShutdownTaskScheduler() { + return CleanShutdownPerformer::scheduleShutdownTask; + } + +} diff --git a/main/launcher/src/main/java/org/cryptomator/launcher/MainApplication.java b/main/launcher/src/main/java/org/cryptomator/launcher/MainApplication.java new file mode 100644 index 000000000..5cdc4e6ae --- /dev/null +++ b/main/launcher/src/main/java/org/cryptomator/launcher/MainApplication.java @@ -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); + } + }); + } + +} diff --git a/main/launcher/src/main/java/org/cryptomator/logging/ConfigurableFileAppender.java b/main/launcher/src/main/java/org/cryptomator/logging/ConfigurableFileAppender.java new file mode 100644 index 000000000..a453b1adf --- /dev/null +++ b/main/launcher/src/main/java/org/cryptomator/logging/ConfigurableFileAppender.java @@ -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. -Dcryptomator.logPath=/var/log/cryptomator.log.
+ * 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 { + + 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 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 newBuilder() { + return new Builder().asBuilder(); + } + + /** + * Builds ConfigurableFileAppender instances. + * + * @param + * The type to build + */ + public static class Builder> extends AbstractOutputStreamAppender.Builder // + implements org.apache.logging.log4j.core.util.Builder { + + @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; + } + } + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/DebugMode.java b/main/launcher/src/main/java/org/cryptomator/logging/DebugMode.java similarity index 96% rename from main/ui/src/main/java/org/cryptomator/ui/DebugMode.java rename to main/launcher/src/main/java/org/cryptomator/logging/DebugMode.java index 55d79327b..baa6c94f5 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/DebugMode.java +++ b/main/launcher/src/main/java/org/cryptomator/logging/DebugMode.java @@ -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; diff --git a/main/ui/src/main/resources/log4j2.xml b/main/launcher/src/main/resources/log4j2.xml similarity index 95% rename from main/ui/src/main/resources/log4j2.xml rename to main/launcher/src/main/resources/log4j2.xml index 368769ee3..85afbab88 100644 --- a/main/ui/src/main/resources/log4j2.xml +++ b/main/launcher/src/main/resources/log4j2.xml @@ -7,7 +7,7 @@ Contributors: Markus Kreusch - switched to log4j 2 --> - + diff --git a/main/launcher/src/test/java/org/cryptomator/launcher/FileOpenRequestHandlerTest.java b/main/launcher/src/test/java/org/cryptomator/launcher/FileOpenRequestHandlerTest.java new file mode 100644 index 000000000..ae43abc95 --- /dev/null +++ b/main/launcher/src/test/java/org/cryptomator/launcher/FileOpenRequestHandlerTest.java @@ -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 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 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 queue = Mockito.mock(BlockingQueue.class); + Mockito.when(queue.offer(Mockito.any())).thenReturn(false); + FileOpenRequestHandler handler = new FileOpenRequestHandler(queue); + handler.handleLaunchArgs(fs, new String[] {"foo"}); + } + +} diff --git a/main/launcher/src/test/java/org/cryptomator/launcher/InterProcessCommunicatorTest.java b/main/launcher/src/test/java/org/cryptomator/launcher/InterProcessCommunicatorTest.java new file mode 100644 index 000000000..c258efb1b --- /dev/null +++ b/main/launcher/src/test/java/org/cryptomator/launcher/InterProcessCommunicatorTest.java @@ -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"}); + } + } + } + +} diff --git a/main/launcher/src/test/resources/log4j2.xml b/main/launcher/src/test/resources/log4j2.xml new file mode 100644 index 000000000..e0ea8ca2b --- /dev/null +++ b/main/launcher/src/test/resources/log4j2.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/main/pom.xml b/main/pom.xml index ce4dd6020..661f652b1 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -31,7 +31,7 @@ 1.2.0 0.4.0 1.0.0 - 2.1 + 2.8.1 1.7.25 4.12 4.12.1 @@ -69,12 +69,6 @@ commons ${project.version} - - org.cryptomator - commons-test - ${project.version} - test - org.cryptomator keychain @@ -85,7 +79,12 @@ ui ${project.version} - + + org.cryptomator + launcher + ${project.version} + + org.cryptomator @@ -114,6 +113,11 @@ slf4j-api ${slf4j.version} + + org.slf4j + slf4j-simple + ${slf4j.version} + org.apache.logging.log4j log4j-core @@ -212,12 +216,6 @@ org.mockito mockito-core ${mockito.version} - - - hamcrest-core - org.hamcrest - - org.hamcrest @@ -228,25 +226,37 @@ + - org.apache.logging.log4j - log4j-core + org.slf4j + slf4j-api - org.apache.logging.log4j - log4j-slf4j-impl + junit + junit + test - org.apache.logging.log4j - log4j-jul + org.hamcrest + hamcrest-all + + + org.mockito + mockito-core + test + + + de.bechte.junit + junit-hierarchicalcontextrunner + test commons - commons-test keychain ui + launcher diff --git a/main/uber-jar/pom.xml b/main/uber-jar/pom.xml index 35b3e2273..a16e82cb2 100644 --- a/main/uber-jar/pom.xml +++ b/main/uber-jar/pom.xml @@ -1,12 +1,5 @@ - + 4.0.0 @@ -15,43 +8,50 @@ 1.3.0-SNAPSHOT uber-jar - pom Single über jar with all dependencies org.cryptomator - ui + launcher - maven-assembly-plugin + maven-shade-plugin 3.0.0 make-assembly package - single + shade - Cryptomator-${project.parent.version} - - jar-with-dependencies - - false - - - org.cryptomator.ui.Cryptomator - ${project.version} - - + Cryptomator-${project.version} + false + + + + org.cryptomator.launcher.Cryptomator + ${project.version} + + + + + + + + com.github.edwgiz + maven-shade-plugin.log4j2-cachefile-transformer + ${log4j.version} + + diff --git a/main/uber-jar/src/main/resources/log4j2.xml b/main/uber-jar/src/main/resources/log4j2.xml new file mode 100644 index 000000000..9a9ee5032 --- /dev/null +++ b/main/uber-jar/src/main/resources/log4j2.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/main/ui/pom.xml b/main/ui/pom.xml index 09245175c..1e13bbf25 100644 --- a/main/ui/pom.xml +++ b/main/ui/pom.xml @@ -51,17 +51,15 @@ easybind - - - com.google.code.gson - gson - - - + com.google.guava guava + + com.google.code.gson + gson + @@ -91,12 +89,6 @@ dagger-compiler provided - - - - org.cryptomator - commons-test - @@ -104,5 +96,19 @@ zxcvbn 1.2.2 + + + + org.apache.logging.log4j + log4j-core + + + org.apache.logging.log4j + log4j-slf4j-impl + + + org.apache.logging.log4j + log4j-jul + diff --git a/main/ui/src/main/java/org/cryptomator/ui/Cryptomator.java b/main/ui/src/main/java/org/cryptomator/ui/Cryptomator.java deleted file mode 100644 index 90786f1f4..000000000 --- a/main/ui/src/main/java/org/cryptomator/ui/Cryptomator.java +++ /dev/null @@ -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> OPEN_FILE_HANDLER = new CompletableFuture<>(); - private static final Logger LOG = LoggerFactory.getLogger(Cryptomator.class); - private static final ConcurrentMap 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 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) { - 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 ff = (List) getFiles.invoke(e); - for (File f : ff) { - handleOpenFileRequest(f); - } - } catch (RuntimeException ee) { - throw ee; - } catch (Exception ee) { - throw new RuntimeException(ee); - } - } - return null; - } - } -} diff --git a/main/ui/src/main/java/org/cryptomator/ui/CryptomatorComponent.java b/main/ui/src/main/java/org/cryptomator/ui/CryptomatorComponent.java deleted file mode 100644 index 830945bd3..000000000 --- a/main/ui/src/main/java/org/cryptomator/ui/CryptomatorComponent.java +++ /dev/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); - -} diff --git a/main/ui/src/main/java/org/cryptomator/ui/ExitUtil.java b/main/ui/src/main/java/org/cryptomator/ui/ExitUtil.java index 9126c8ff9..0aebd795f 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/ExitUtil.java +++ b/main/ui/src/main/java/org/cryptomator/ui/ExitUtil.java @@ -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 { diff --git a/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java b/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java deleted file mode 100644 index 80fd591c6..000000000 --- a/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java +++ /dev/null @@ -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); - }); - } - -} diff --git a/main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java b/main/ui/src/main/java/org/cryptomator/ui/UiModule.java similarity index 72% rename from main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java rename to main/ui/src/main/java/org/cryptomator/ui/UiModule.java index 64f7399dc..9b4671162 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/UiModule.java @@ -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 shutdownTaskScheduler) { DeferredCloser closer = new DeferredCloser(); - Cryptomator.addShutdownTask(() -> { + shutdownTaskScheduler.accept(() -> { try { closer.close(); } catch (Exception e) { diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java index a1eb9bb16..7b0de1f51 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java @@ -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 fileOpenRequests; private final Settings settings; private final VaultFactory vaultFactoy; private final Lazy welcomeController; @@ -91,13 +104,18 @@ public class MainController extends LocalizedFXMLViewController { private final BooleanBinding isShowingSettings; private final Map unlockedVaults = new HashMap<>(); + private Subscription subs = Subscription.EMPTY; + @Inject - public MainController(@Named("mainWindow") Stage mainWindow, Localization localization, Settings settings, VaultFactory vaultFactoy, Lazy welcomeController, - Lazy initializeController, Lazy notFoundController, Lazy upgradeController, Lazy unlockController, - Provider unlockedControllerProvider, Lazy changePasswordController, Lazy settingsController, UpgradeStrategies upgradeStrategies, - VaultList vaults) { + public MainController(@Named("mainWindow") Stage mainWindow, ExecutorService executorService, @Named("fileOpenRequests") BlockingQueue fileOpenRequests, ExitUtil exitUtil, Localization localization, + Settings settings, VaultFactory vaultFactoy, Lazy welcomeController, Lazy initializeController, Lazy notFoundController, + Lazy upgradeController, Lazy unlockController, Provider unlockedControllerProvider, Lazy changePasswordController, + Lazy 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 diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/SettingsController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/SettingsController.java index 6e1bdf263..1cf66d10f 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/SettingsController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/SettingsController.java @@ -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; diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/WelcomeController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/WelcomeController.java index 40fd97fd6..b635fdaf6 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/WelcomeController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/WelcomeController.java @@ -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 applicationVersion; private final Settings settings; private final Comparator semVerComparator; private final AsyncTaskService asyncTaskService; @Inject - public WelcomeController(Application app, Localization localization, Settings settings, @Named("SemVer") Comparator semVerComparator, AsyncTaskService asyncTaskService) { + public WelcomeController(Application app, @Named("applicationVersion") Optional applicationVersion, Localization localization, Settings settings, @Named("SemVer") Comparator 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); diff --git a/main/ui/src/main/java/org/cryptomator/ui/logging/ConfigurableFileAppender.java b/main/ui/src/main/java/org/cryptomator/ui/logging/ConfigurableFileAppender.java deleted file mode 100644 index 3a227bfd6..000000000 --- a/main/ui/src/main/java/org/cryptomator/ui/logging/ConfigurableFileAppender.java +++ /dev/null @@ -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. -Dcryptomator.logPath=/var/log/cryptomator.log.
- * 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 { - - 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 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 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; - } - } - } - -} diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeStrategy.java b/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeStrategy.java index b2c1b2e8f..40105140b 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeStrategy.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeStrategy.java @@ -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. */ diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion3DropBundleExtension.java b/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion3DropBundleExtension.java index e080aeea3..9bbc5f2d2 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion3DropBundleExtension.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion3DropBundleExtension.java @@ -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 diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion3to4.java b/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion3to4.java index e28e2ff8b..005392390 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion3to4.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion3to4.java @@ -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 diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion4to5.java b/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion4to5.java index 53296c3cd..25d89c3e2 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion4to5.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion4to5.java @@ -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 diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java index 03e39fb04..92ca44eff 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java @@ -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; diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/VaultComponent.java b/main/ui/src/main/java/org/cryptomator/ui/model/VaultComponent.java index eebba2a05..527aa4f85 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/VaultComponent.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/VaultComponent.java @@ -15,4 +15,11 @@ public interface VaultComponent { Vault vault(); + @Subcomponent.Builder + interface Builder { + Builder vaultModule(VaultModule module); + + VaultComponent build(); + } + } diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/VaultFactory.java b/main/ui/src/main/java/org/cryptomator/ui/model/VaultFactory.java index 51d3f8df6..7b4b553e2 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/VaultFactory.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/VaultFactory.java @@ -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 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(); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/VaultList.java b/main/ui/src/main/java/org/cryptomator/ui/model/VaultList.java index fdf22d9d9..519fea90f 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/VaultList.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/VaultList.java @@ -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; diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/VaultModule.java b/main/ui/src/main/java/org/cryptomator/ui/model/VaultModule.java index c24168378..c8a622233 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/VaultModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/VaultModule.java @@ -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; diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/ActiveWindowStyleSupport.java b/main/ui/src/main/java/org/cryptomator/ui/util/ActiveWindowStyleSupport.java index 769e951e6..72da6f48c 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/util/ActiveWindowStyleSupport.java +++ b/main/ui/src/main/java/org/cryptomator/ui/util/ActiveWindowStyleSupport.java @@ -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 { public static final String ACTIVE_WINDOW_STYLE_CLASS = "active-window"; diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/SingleInstanceManager.java b/main/ui/src/main/java/org/cryptomator/ui/util/SingleInstanceManager.java deleted file mode 100644 index adcf9bcd6..000000000 --- a/main/ui/src/main/java/org/cryptomator/ui/util/SingleInstanceManager.java +++ /dev/null @@ -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 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 keysToRemove = new HashSet<>(); - while (selector.select() > 0) { - final Set 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 getRemoteInstance(String applicationKey) { - Optional 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 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 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); - } - } -} \ No newline at end of file diff --git a/main/ui/src/test/java/org/cryptomator/ui/MainApplicationTest.java b/main/ui/src/test/java/org/cryptomator/ui/MainApplicationTest.java deleted file mode 100644 index 58749a4bf..000000000 --- a/main/ui/src/test/java/org/cryptomator/ui/MainApplicationTest.java +++ /dev/null @@ -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(); - } - -} diff --git a/main/ui/src/test/java/org/cryptomator/ui/util/SingleInstanceManagerTest.java b/main/ui/src/test/java/org/cryptomator/ui/util/SingleInstanceManagerTest.java deleted file mode 100644 index 7ffe072a4..000000000 --- a/main/ui/src/test/java/org/cryptomator/ui/util/SingleInstanceManagerTest.java +++ /dev/null @@ -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 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 sentMessages = new ConcurrentSkipListSet<>(); - Set receivedMessages = new HashSet<>(); - - CountDownLatch sendLatch = new CountDownLatch(connectors); - CountDownLatch receiveLatch = new CountDownLatch(connectors * messagesPerConnector); - - server.registerListener(message -> { - receivedMessages.add(message); - receiveLatch.countDown(); - }); - - Set instances = Collections.synchronizedSet(new HashSet<>()); - - for (int i = 0; i < connectors; i++) { - exec2.submit(() -> { - try { - final Optional 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(); - } - } -}