mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-09-19 22:44:32 +00:00
Refactored Cryptomator UI. Extracted Launcher to its own Maven module.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+5
@@ -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"});
|
||||
}
|
||||
|
||||
}
|
||||
+82
@@ -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>
|
||||
Reference in New Issue
Block a user