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

This commit is contained in:
Sebastian Stenzel
2017-04-18 13:40:59 +02:00
parent ada1195a26
commit 93b2a4e07a
55 changed files with 1234 additions and 1324 deletions
@@ -0,0 +1,20 @@
/*******************************************************************************
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.launcher;
import java.util.Optional;
public class ApplicationVersion {
public static String orElse(String other) {
return get().orElse(other);
}
public static Optional<String> get() {
return Optional.ofNullable(Cryptomator.class.getPackage().getImplementationVersion());
}
}
@@ -0,0 +1,35 @@
package org.cryptomator.launcher;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class CleanShutdownPerformer extends Thread {
private static final Logger LOG = LoggerFactory.getLogger(CleanShutdownPerformer.class);
static final ConcurrentMap<Runnable, Boolean> SHUTDOWN_TASKS = new ConcurrentHashMap<>();
@Override
public void run() {
LOG.debug("Running graceful shutdown tasks...");
SHUTDOWN_TASKS.keySet().forEach(r -> {
try {
r.run();
} catch (RuntimeException e) {
LOG.error("Exception while shutting down.", e);
}
});
SHUTDOWN_TASKS.clear();
LOG.info("Goodbye.");
}
static void scheduleShutdownTask(Runnable task) {
SHUTDOWN_TASKS.put(task, Boolean.TRUE);
}
static void registerShutdownHook() {
Runtime.getRuntime().addShutdownHook(new CleanShutdownPerformer());
}
}
@@ -0,0 +1,54 @@
package org.cryptomator.launcher;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import org.apache.commons.lang3.SystemUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javafx.application.Application;
public class Cryptomator {
private static final Logger LOG = LoggerFactory.getLogger(Cryptomator.class);
static final BlockingQueue<Path> FILE_OPEN_REQUESTS = new ArrayBlockingQueue<>(10);
public static void main(String[] args) throws IOException {
LOG.info("Starting Cryptomator {} on {} {} ({})", ApplicationVersion.orElse("SNAPSHOT"), SystemUtils.OS_NAME, SystemUtils.OS_VERSION, SystemUtils.OS_ARCH);
FileOpenRequestHandler fileOpenRequestHandler = new FileOpenRequestHandler(FILE_OPEN_REQUESTS);
try (InterProcessCommunicator communicator = InterProcessCommunicator.start(new IpcProtocolImpl(fileOpenRequestHandler))) {
if (communicator.isServer()) {
fileOpenRequestHandler.handleLaunchArgs(args);
CleanShutdownPerformer.registerShutdownHook();
Application.launch(MainApplication.class, args);
} else {
communicator.handleLaunchArgs(args);
LOG.info("Found running application instance. Shutting down.");
}
}
System.exit(0); // end remaining non-daemon threads.
}
private static class IpcProtocolImpl implements InterProcessCommunicationProtocol {
private final FileOpenRequestHandler fileOpenRequestHandler;
// TODO: inject?
public IpcProtocolImpl(FileOpenRequestHandler fileOpenRequestHandler) {
this.fileOpenRequestHandler = fileOpenRequestHandler;
}
@Override
public void handleLaunchArgs(String[] args) {
LOG.info("Received launch args: {}", Arrays.stream(args).reduce((a, b) -> a + ", " + b).orElse(""));
fileOpenRequestHandler.handleLaunchArgs(args);
}
}
}
@@ -0,0 +1,102 @@
/*******************************************************************************
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
* All rights reserved.
*
* This class is licensed under the LGPL 3.0 (https://www.gnu.org/licenses/lgpl-3.0.de.html).
*******************************************************************************/
package org.cryptomator.launcher;
import java.io.File;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import org.apache.commons.lang3.SystemUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class FileOpenRequestHandler {
private static final Logger LOG = LoggerFactory.getLogger(FileOpenRequestHandler.class);
private final BlockingQueue<Path> fileOpenRequests;
public FileOpenRequestHandler(BlockingQueue<Path> fileOpenRequests) {
this.fileOpenRequests = fileOpenRequests;
if (SystemUtils.IS_OS_MAC_OSX) {
addOsxFileOpenHandler();
}
}
public void handleLaunchArgs(String[] args) {
handleLaunchArgs(FileSystems.getDefault(), args);
}
// visible for testing
void handleLaunchArgs(FileSystem fs, String[] args) {
for (String arg : args) {
try {
Path path = fs.getPath(arg);
tryToEnqueueFileOpenRequest(path);
} catch (InvalidPathException e) {
LOG.trace("{} not a valid path", arg);
}
}
}
private void tryToEnqueueFileOpenRequest(Path path) {
if (!fileOpenRequests.offer(path)) {
LOG.warn("{} could not be enqueued for opening.", path);
}
}
/**
* Event subscription code inspired by https://gitlab.com/axet/desktop/blob/master/java/src/main/java/com/github/axet/desktop/os/mac/AppleHandlers.java
*/
private void addOsxFileOpenHandler() {
try {
final Class<?> applicationClass = Class.forName("com.apple.eawt.Application");
final Class<?> openFilesHandlerClass = Class.forName("com.apple.eawt.OpenFilesHandler");
final Method getApplication = applicationClass.getMethod("getApplication");
final Object application = getApplication.invoke(null);
final Method setOpenFileHandler = applicationClass.getMethod("setOpenFileHandler", openFilesHandlerClass);
final ClassLoader openFilesHandlerClassLoader = openFilesHandlerClass.getClassLoader();
final OpenFilesEventInvocationHandler openFilesHandler = new OpenFilesEventInvocationHandler();
final Object openFilesHandlerObject = Proxy.newProxyInstance(openFilesHandlerClassLoader, new Class<?>[] {openFilesHandlerClass}, openFilesHandler);
setOpenFileHandler.invoke(application, openFilesHandlerObject);
} catch (ReflectiveOperationException | RuntimeException e) {
// Since we're trying to call OS-specific code, we'll just have to hope for the best.
LOG.error("Exception adding OS X file open handler", e);
}
}
/**
* Handler class inspired by https://gitlab.com/axet/desktop/blob/master/java/src/main/java/com/github/axet/desktop/os/mac/AppleHandlers.java
*/
private class OpenFilesEventInvocationHandler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("openFiles")) {
final Class<?> openFilesEventClass = Class.forName("com.apple.eawt.AppEvent$OpenFilesEvent");
final Method getFiles = openFilesEventClass.getMethod("getFiles");
Object e = args[0];
try {
@SuppressWarnings("unchecked")
final List<File> ff = (List<File>) getFiles.invoke(e);
ff.stream().map(File::toPath).forEach(fileOpenRequests::add);
} catch (RuntimeException ee) {
throw ee;
} catch (Exception ee) {
throw new RuntimeException(ee);
}
}
return null;
}
}
}
@@ -0,0 +1,5 @@
package org.cryptomator.launcher;
public interface InterProcessCommunicationProtocol {
void handleLaunchArgs(String[] args);
}
@@ -0,0 +1,232 @@
package org.cryptomator.launcher;
import java.io.Closeable;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.rmi.ConnectException;
import java.rmi.NoSuchObjectException;
import java.rmi.NotBoundException;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.RMIClientSocketFactory;
import java.rmi.server.RMIServerSocketFactory;
import java.rmi.server.RMISocketFactory;
import java.rmi.server.UnicastRemoteObject;
import org.apache.commons.lang3.SystemUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* First running application on a machine opens a server socket. Further processes will connect as clients.
*/
abstract class InterProcessCommunicator implements InterProcessCommunicationProtocol, Closeable {
private static final Logger LOG = LoggerFactory.getLogger(InterProcessCommunicator.class);
private static final String RMI_NAME = "Cryptomator";
public abstract boolean isServer();
/**
* @param endpoint The server-side communication endpoint.
* @return Either a client or a server communicator.
* @throws IOException In case of communication errors.
*/
public static InterProcessCommunicator start(InterProcessCommunicationProtocol endpoint) throws IOException {
return start(getIpcPortPath(), endpoint);
}
// visible for testing
static InterProcessCommunicator start(Path portFilePath, InterProcessCommunicationProtocol endpoint) throws IOException {
// try to connect to existing server:
int port = readPort(portFilePath);
LOG.debug("Connecting to running process on TCP port {}...", port);
try {
ClientCommunicator client = new ClientCommunicator(port);
LOG.trace("Connected to running process.");
return client;
} catch (ConnectException | NotBoundException e) {
LOG.debug("Did not find running process.");
// continue
}
// spawn a new server:
LOG.trace("Spawning new server...");
ServerCommunicator server = new ServerCommunicator(endpoint);
writePort(portFilePath, server.getPort());
LOG.debug("Server listening on port {}.", server.getPort());
return server;
}
private static Path getIpcPortPath() {
final String settingsPathProperty = System.getProperty("cryptomator.ipcPortPath");
if (settingsPathProperty == null) {
LOG.warn("System property cryptomator.ipcPortPath not set.");
return Paths.get("ipcPort.tmp");
} else {
return Paths.get(replaceHomeDir(settingsPathProperty));
}
}
private static String replaceHomeDir(String path) {
if (path.startsWith("~/")) {
return SystemUtils.USER_HOME + path.substring(1);
} else {
return path;
}
}
public static class ClientCommunicator extends InterProcessCommunicator {
private final IpcProtocolRemote remote;
private ClientCommunicator(int port) throws ConnectException, NotBoundException, RemoteException {
if (port == 0) {
throw new ConnectException("Can not connect to port 0.");
}
Registry registry = LocateRegistry.getRegistry(port);
this.remote = (IpcProtocolRemote) registry.lookup(RMI_NAME);
}
@Override
public void handleLaunchArgs(String[] args) {
try {
remote.handleLaunchArgs(args);
} catch (RemoteException e) {
throw new RuntimeException(e);
}
}
@Override
public boolean isServer() {
return false;
}
@Override
public void close() throws IOException {
// no-op
}
}
public static class ServerCommunicator extends InterProcessCommunicator {
private final ServerSocket socket;
private final Registry registry;
private final IpcProtocolRemoteImpl remote;
private ServerCommunicator(InterProcessCommunicationProtocol delegate) throws IOException {
this.socket = new ServerSocket(0, Byte.MAX_VALUE, InetAddress.getLocalHost());
RMIClientSocketFactory csf = RMISocketFactory.getDefaultSocketFactory();
SingletonServerSocketFactory ssf = new SingletonServerSocketFactory(socket);
this.registry = LocateRegistry.createRegistry(0, csf, ssf);
this.remote = new IpcProtocolRemoteImpl(delegate);
UnicastRemoteObject.exportObject(remote, 0);
registry.rebind(RMI_NAME, remote);
}
@Override
public void handleLaunchArgs(String[] args) {
throw new UnsupportedOperationException("Server doesn't invoke methods.");
}
@Override
public boolean isServer() {
return true;
}
private int getPort() {
return socket.getLocalPort();
}
@Override
public void close() throws IOException {
try {
registry.unbind(RMI_NAME);
UnicastRemoteObject.unexportObject(remote, true);
socket.close();
LOG.debug("Server shut down.");
} catch (NotBoundException | NoSuchObjectException e) {
// ignore
}
}
}
private static interface IpcProtocolRemote extends Remote {
void handleLaunchArgs(String[] args) throws RemoteException;
}
private static class IpcProtocolRemoteImpl implements IpcProtocolRemote {
private final InterProcessCommunicationProtocol delegate;
protected IpcProtocolRemoteImpl(InterProcessCommunicationProtocol delegate) throws RemoteException {
this.delegate = delegate;
}
@Override
public void handleLaunchArgs(String[] args) {
delegate.handleLaunchArgs(args);
}
}
/**
* Always returns the same pre-constructed server socket.
*/
private static class SingletonServerSocketFactory implements RMIServerSocketFactory {
private final ServerSocket socket;
public SingletonServerSocketFactory(ServerSocket socket) {
this.socket = socket;
}
@Override
public synchronized ServerSocket createServerSocket(int port) throws IOException {
if (port != 0) {
throw new IllegalArgumentException("This factory doesn't support specific ports.");
}
return this.socket;
}
}
private static int readPort(Path path) throws IOException {
if (Files.notExists(path)) {
return 0;
}
ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES);
try (ReadableByteChannel ch = Files.newByteChannel(path, StandardOpenOption.READ)) {
if (ch.read(buf) == Integer.BYTES) {
buf.flip();
return buf.getInt();
} else {
return 0;
}
}
}
private static void writePort(Path path, int port) throws IOException {
ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES);
buf.putInt(port);
buf.flip();
try (WritableByteChannel ch = Files.newByteChannel(path, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
if (ch.write(buf) != Integer.BYTES) {
throw new IOException("Did not write expected number of bytes.");
}
}
}
}
@@ -0,0 +1,18 @@
package org.cryptomator.launcher;
import javax.inject.Singleton;
import org.cryptomator.logging.DebugMode;
import org.cryptomator.ui.controllers.MainController;
import dagger.Component;
@Singleton
@Component(modules = LauncherModule.class)
interface LauncherComponent {
MainController mainController();
DebugMode debugMode();
}
@@ -0,0 +1,63 @@
package org.cryptomator.launcher;
import java.nio.file.Path;
import java.util.Optional;
import java.util.concurrent.BlockingQueue;
import java.util.function.Consumer;
import javax.inject.Named;
import javax.inject.Singleton;
import org.cryptomator.ui.UiModule;
import dagger.Module;
import dagger.Provides;
import javafx.application.Application;
import javafx.stage.Stage;
@Module(includes = {UiModule.class})
class LauncherModule {
private final Application application;
private final Stage mainWindow;
public LauncherModule(Application application, Stage mainWindow) {
this.application = application;
this.mainWindow = mainWindow;
}
@Provides
@Singleton
Application provideApplication() {
return application;
}
@Provides
@Singleton
@Named("applicationVersion")
Optional<String> provideApplicationVersion() {
return ApplicationVersion.get();
}
@Provides
@Singleton
@Named("mainWindow")
Stage provideMainWindow() {
return mainWindow;
}
@Provides
@Singleton
@Named("fileOpenRequests")
BlockingQueue<Path> provideFileOpenRequests() {
return Cryptomator.FILE_OPEN_REQUESTS;
}
@Provides
@Singleton
@Named("shutdownTaskScheduler")
Consumer<Runnable> provideShutdownTaskScheduler() {
return CleanShutdownPerformer::scheduleShutdownTask;
}
}
@@ -0,0 +1,58 @@
package org.cryptomator.launcher;
import org.cryptomator.ui.controllers.MainController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.stage.Stage;
public class MainApplication extends Application {
private static final Logger LOG = LoggerFactory.getLogger(MainApplication.class);
private Stage primaryStage;
@Override
public void start(Stage primaryStage) throws Exception {
LOG.info("JavaFX application started.");
this.primaryStage = primaryStage;
setupFXMLClassLoader();
LauncherModule launcherModule = new LauncherModule(this, primaryStage);
LauncherComponent launcherComponent = DaggerLauncherComponent.builder() //
.launcherModule(launcherModule) //
.build();
launcherComponent.debugMode().initialize();
MainController mainCtrl = launcherComponent.mainController();
mainCtrl.initStage(primaryStage);
primaryStage.show();
}
@Override
public void stop() throws Exception {
assert primaryStage != null;
primaryStage.hide();
LOG.info("JavaFX application stopped.");
}
// fix discussed in https://github.com/cryptomator/cryptomator/pull/29
private void setupFXMLClassLoader() {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
FXMLLoader.setDefaultClassLoader(contextClassLoader);
Platform.runLater(() -> {
/*
* This fixes a bug on OSX where the magic file open handler leads to no context class loader being set in the AppKit (event)
* thread if the application is not started opening a file.
*/
if (Thread.currentThread().getContextClassLoader() == null) {
Thread.currentThread().setContextClassLoader(contextClassLoader);
}
});
}
}
@@ -0,0 +1,132 @@
/*******************************************************************************
* Copyright (c) 2016 Sebastian Stenzel and others.
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.logging;
import java.io.IOException;
import java.io.Serializable;
import java.net.URISyntaxException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.regex.Pattern;
import org.apache.commons.lang3.SystemUtils;
import org.apache.logging.log4j.core.Appender;
import org.apache.logging.log4j.core.Core;
import org.apache.logging.log4j.core.Filter;
import org.apache.logging.log4j.core.Layout;
import org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender;
import org.apache.logging.log4j.core.appender.FileManager;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
import org.apache.logging.log4j.core.config.plugins.validation.constraints.Required;
import org.apache.logging.log4j.util.Strings;
/**
* A preconfigured FileAppender only relying on a configurable system property, e.g. <code>-Dcryptomator.logPath=/var/log/cryptomator.log</code>.<br/>
* Other than the normal {@link org.apache.logging.log4j.core.appender.FileAppender} paths can be resolved relative to the users home directory.
*/
@Plugin(name = ConfigurableFileAppender.PLUGIN_NAME, category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE, printObject = true)
public class ConfigurableFileAppender extends AbstractOutputStreamAppender<FileManager> {
static final String PLUGIN_NAME = "ConfigurableFile";
private static final Pattern DRIVE_LETTER_WITH_PRECEEDING_SLASH = Pattern.compile("^/[A-Z]:", Pattern.CASE_INSENSITIVE);
private ConfigurableFileAppender(String name, Layout<? extends Serializable> layout, Filter filter, boolean ignoreExceptions, boolean immediateFlush, FileManager manager) {
super(name, layout, filter, ignoreExceptions, immediateFlush, manager);
LOGGER.info("Logging to " + manager.getFileName());
}
@PluginBuilderFactory
public static <B extends Builder<B>> B newBuilder() {
return new Builder<B>().asBuilder();
}
/**
* Builds ConfigurableFileAppender instances.
*
* @param <B>
* The type to build
*/
public static class Builder<B extends Builder<B>> extends AbstractOutputStreamAppender.Builder<B> //
implements org.apache.logging.log4j.core.util.Builder<ConfigurableFileAppender> {
@Required(message = "No system property name containing the log file path provided.")
@PluginBuilderAttribute("pathPropertyName")
private String pathPropertyName;
@PluginBuilderAttribute
private boolean append = true;
@Override
public ConfigurableFileAppender build() {
final String pathProperty = System.getProperty(pathPropertyName);
if (Strings.isEmpty(pathProperty)) {
LOGGER.warn("No log file location provided in system property \"" + pathPropertyName + "\"");
return null;
}
final Path filePath = parsePath(pathProperty);
if (filePath == null) {
LOGGER.warn("Invalid path \"" + pathProperty + "\"");
return null;
}
if (!Files.exists(filePath.getParent())) {
try {
Files.createDirectories(filePath.getParent());
} catch (IOException e) {
LOGGER.error("Could not create parent directories for log file located at " + filePath.toString(), e);
return null;
}
}
FileManager manager = FileManager.getFileManager(filePath.toString(), append, false, isBufferedIo(), true, null, getOrCreateLayout(), getBufferSize(), getConfiguration());
return new ConfigurableFileAppender(getName(), getLayout(), getFilter(), isIgnoreExceptions(), isImmediateFlush(), manager);
}
public B withPathPropertyName(String pathPropertyName) {
this.pathPropertyName = pathPropertyName;
return asBuilder();
}
public B withAppend(boolean append) {
this.append = append;
return asBuilder();
}
}
private static Path parsePath(String path) {
if (path.startsWith("~/")) {
// home-dir-relative Path:
final Path userHome = FileSystems.getDefault().getPath(SystemUtils.USER_HOME);
return userHome.resolve(path.substring(2));
} else if (path.startsWith("/")) {
// absolute Path:
return FileSystems.getDefault().getPath(path);
} else {
// relative Path:
try {
String jarFileLocation = ConfigurableFileAppender.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath();
if (SystemUtils.IS_OS_WINDOWS && DRIVE_LETTER_WITH_PRECEEDING_SLASH.matcher(jarFileLocation).find()) {
// on windows we need to remove a preceeding slash from "/C:/foo/bar":
jarFileLocation = jarFileLocation.substring(1);
}
final Path workingDir = FileSystems.getDefault().getPath(jarFileLocation).getParent();
return workingDir.resolve(path);
} catch (URISyntaxException e) {
LOGGER.error("Unable to resolve working directory ", e);
return null;
}
}
}
}
@@ -0,0 +1,80 @@
/*******************************************************************************
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.logging;
import static java.util.Arrays.asList;
import static org.apache.logging.log4j.LogManager.ROOT_LOGGER_NAME;
import java.util.Collection;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.apache.logging.log4j.Level;
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.common.settings.Settings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
public class DebugMode {
private static final Logger LOG = LoggerFactory.getLogger(DebugMode.class);
private static final Collection<LoggerUpgrade> LOGGER_UPGRADES = asList( //
loggerUpgrade(ROOT_LOGGER_NAME, Level.INFO), //
loggerUpgrade("org.cryptomator", Level.TRACE), //
loggerUpgrade("org.eclipse.jetty.server.Server", Level.DEBUG) //
);
private final Settings settings;
@Inject
public DebugMode(Settings settings) {
this.settings = settings;
}
public void initialize() {
if (settings.debugMode().get()) {
enable();
LOG.debug("Debug mode initialized");
}
}
private void enable() {
LoggerContext context = (LoggerContext) LogManager.getContext(false);
Configuration config = context.getConfiguration();
LOGGER_UPGRADES.forEach(loggerUpgrade -> loggerUpgrade.execute(config));
context.updateLoggers();
}
private static LoggerUpgrade loggerUpgrade(String loggerName, Level minLevel) {
return new LoggerUpgrade(loggerName, minLevel);
}
private static class LoggerUpgrade {
private final Level level;
private final String loggerName;
public LoggerUpgrade(String loggerName, Level minLevel) {
this.loggerName = loggerName;
this.level = minLevel;
}
public void execute(Configuration config) {
LoggerConfig loggerConfig = config.getLoggerConfig(loggerName);
if (loggerConfig.getLevel().isMoreSpecificThan(level)) {
loggerConfig.setLevel(level);
}
}
}
}
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright (c) 2014 Markus Kreusch
This file is licensed under the terms of the MIT license.
See the LICENSE.txt file for more info.
Contributors:
Markus Kreusch - switched to log4j 2
-->
<Configuration status="WARN" packages="org.cryptomator.logging">
<Appenders>
<Console name="StdOut" target="SYSTEM_OUT">
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="ACCEPT" />
</Console>
<Console name="StdErr" target="SYSTEM_ERR">
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY" />
</Console>
<ConfigurableFile name="DefaultLog" pathPropertyName="cryptomator.logPath" append="false">
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
</ConfigurableFile>
<ConfigurableFile name="UpgradeLog" pathPropertyName="cryptomator.upgradeLogPath" append="true">
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
</ConfigurableFile>
</Appenders>
<Loggers>
<!--
= NOTE =
All loggers below are set to info.
This is required to allow changing the levels of the loggers programmatically.
-->
<Logger name="org.cryptomator" level="INFO" />
<Logger name="org.eclipse.jetty.server.Server" level="INFO" />
<Logger name="org.cryptomator.ui.model" level="INFO">
<AppenderRef ref="UpgradeLog" />
</Logger>
<!-- defaults: -->
<Root level="INFO">
<AppenderRef ref="StdOut" />
<AppenderRef ref="StdErr" />
<AppenderRef ref="DefaultLog" />
</Root>
</Loggers>
</Configuration>
@@ -0,0 +1,72 @@
package org.cryptomator.launcher;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.spi.FileSystemProvider;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.Mockito;
public class FileOpenRequestHandlerTest {
@Test
public void testOpenArgsWithCorrectPaths() throws IOException {
Path p1 = Mockito.mock(Path.class);
Path p2 = Mockito.mock(Path.class);
FileSystem fs = Mockito.mock(FileSystem.class);
FileSystemProvider provider = Mockito.mock(FileSystemProvider.class);
BasicFileAttributes attrs = Mockito.mock(BasicFileAttributes.class);
Mockito.when(p1.getFileSystem()).thenReturn(fs);
Mockito.when(p2.getFileSystem()).thenReturn(fs);
Mockito.when(fs.provider()).thenReturn(provider);
Mockito.when(fs.getPath(Mockito.anyString())).thenReturn(p1, p2);
Mockito.when(provider.readAttributes(Mockito.any(), Mockito.eq(BasicFileAttributes.class))).thenReturn(attrs);
Mockito.when(attrs.isRegularFile()).thenReturn(true);
BlockingQueue<Path> queue = new ArrayBlockingQueue<>(10);
FileOpenRequestHandler handler = new FileOpenRequestHandler(queue);
handler.handleLaunchArgs(fs, new String[] {"foo", "bar"});
Assert.assertEquals(p1, queue.poll());
Assert.assertEquals(p2, queue.poll());
}
@Test
public void testOpenArgsWithIncorrectPaths() throws IOException {
FileSystem fs = Mockito.mock(FileSystem.class);
Mockito.when(fs.getPath(Mockito.anyString())).thenThrow(new InvalidPathException("foo", "foo is not a path"));
@SuppressWarnings("unchecked")
BlockingQueue<Path> queue = Mockito.mock(BlockingQueue.class);
FileOpenRequestHandler handler = new FileOpenRequestHandler(queue);
handler.handleLaunchArgs(fs, new String[] {"foo"});
Mockito.verifyNoMoreInteractions(queue);
}
@Test
public void testOpenArgsWithFullQueue() throws IOException {
Path p = Mockito.mock(Path.class);
FileSystem fs = Mockito.mock(FileSystem.class);
FileSystemProvider provider = Mockito.mock(FileSystemProvider.class);
BasicFileAttributes attrs = Mockito.mock(BasicFileAttributes.class);
Mockito.when(p.getFileSystem()).thenReturn(fs);
Mockito.when(fs.provider()).thenReturn(provider);
Mockito.when(fs.getPath(Mockito.anyString())).thenReturn(p);
Mockito.when(provider.readAttributes(Mockito.eq(p), Mockito.eq(BasicFileAttributes.class))).thenReturn(attrs);
Mockito.when(attrs.isRegularFile()).thenReturn(true);
@SuppressWarnings("unchecked")
BlockingQueue<Path> queue = Mockito.mock(BlockingQueue.class);
Mockito.when(queue.offer(Mockito.any())).thenReturn(false);
FileOpenRequestHandler handler = new FileOpenRequestHandler(queue);
handler.handleLaunchArgs(fs, new String[] {"foo"});
}
}
@@ -0,0 +1,82 @@
package org.cryptomator.launcher;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.FileSystem;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.spi.FileSystemProvider;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
public class InterProcessCommunicatorTest {
Path portFilePath = Mockito.mock(Path.class);
FileSystem fs = Mockito.mock(FileSystem.class);
FileSystemProvider provider = Mockito.mock(FileSystemProvider.class);
SeekableByteChannel portFileChannel = Mockito.mock(SeekableByteChannel.class);
AtomicInteger port = new AtomicInteger(-1);
@Before
public void setup() throws IOException {
Mockito.when(portFilePath.getFileSystem()).thenReturn(fs);
Mockito.when(fs.provider()).thenReturn(provider);
Mockito.when(provider.newByteChannel(Mockito.eq(portFilePath), Mockito.any(), Mockito.any())).thenReturn(portFileChannel);
Mockito.when(portFileChannel.read(Mockito.any())).then(invocation -> {
ByteBuffer buf = invocation.getArgument(0);
buf.putInt(port.get());
return Integer.BYTES;
});
Mockito.when(portFileChannel.write(Mockito.any())).then(invocation -> {
ByteBuffer buf = invocation.getArgument(0);
port.set(buf.getInt());
return Integer.BYTES;
});
}
@Test(expected = UnsupportedOperationException.class)
public void testStartWithDummyPort1() throws IOException {
port.set(0);
InterProcessCommunicationProtocol protocol = Mockito.mock(InterProcessCommunicationProtocol.class);
try (InterProcessCommunicator result = InterProcessCommunicator.start(portFilePath, protocol)) {
Assert.assertTrue(result.isServer());
Mockito.verifyZeroInteractions(protocol);
result.handleLaunchArgs(new String[] {"foo"});
}
}
@Test
public void testStartWithDummyPort2() throws IOException {
Mockito.doThrow(new NoSuchFileException("port file")).when(provider).checkAccess(portFilePath);
InterProcessCommunicationProtocol protocol = Mockito.mock(InterProcessCommunicationProtocol.class);
try (InterProcessCommunicator result = InterProcessCommunicator.start(portFilePath, protocol)) {
Assert.assertTrue(result.isServer());
Mockito.verifyZeroInteractions(protocol);
}
}
@Test
public void testInterProcessCommunication() throws IOException, InterruptedException {
port.set(-1);
InterProcessCommunicationProtocol protocol = Mockito.mock(InterProcessCommunicationProtocol.class);
try (InterProcessCommunicator result1 = InterProcessCommunicator.start(portFilePath, protocol)) {
Assert.assertTrue(result1.isServer());
Mockito.verifyZeroInteractions(protocol);
try (InterProcessCommunicator result2 = InterProcessCommunicator.start(portFilePath, null)) {
Assert.assertFalse(result2.isServer());
Assert.assertNotSame(result1, result2);
result2.handleLaunchArgs(new String[] {"foo"});
Mockito.verify(protocol).handleLaunchArgs(new String[] {"foo"});
}
}
}
}
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright (c) 2014 Markus Kreusch
This file is licensed under the terms of the MIT license.
See the LICENSE.txt file for more info.
Contributors:
Markus Kreusch - switched to log4j 2
-->
<Configuration status="WARN" packages="org.cryptomator.logging">
<Appenders>
<Console name="StdOut" target="SYSTEM_OUT">
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="ACCEPT" />
</Console>
<Console name="StdErr" target="SYSTEM_ERR">
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY" />
</Console>
</Appenders>
<Loggers>
<Logger name="org.cryptomator" level="DEBUG" />
<!-- defaults: -->
<Root level="INFO">
<AppenderRef ref="StdOut" />
<AppenderRef ref="StdErr" />
</Root>
</Loggers>
</Configuration>