diff --git a/main/launcher/src/main/java/org/cryptomator/launcher/Cryptomator.java b/main/launcher/src/main/java/org/cryptomator/launcher/Cryptomator.java index 95ef8ed3d..7fd431bcb 100644 --- a/main/launcher/src/main/java/org/cryptomator/launcher/Cryptomator.java +++ b/main/launcher/src/main/java/org/cryptomator/launcher/Cryptomator.java @@ -5,74 +5,107 @@ *******************************************************************************/ package org.cryptomator.launcher; -import javafx.application.Application; -import javafx.stage.Stage; +import javafx.application.Platform; import org.apache.commons.lang3.SystemUtils; -import org.cryptomator.ui.controllers.MainController; +import org.cryptomator.logging.DebugMode; +import org.cryptomator.logging.LoggerConfiguration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; import java.io.IOException; +import java.util.Optional; +import java.util.concurrent.CountDownLatch; +@Singleton public class Cryptomator { - private static final Logger LOG; - private static final CryptomatorComponent CRYPTOMATOR_COMPONENT; + // DaggerCryptomatorComponent gets generated by Dagger. + // Run Maven and include target/generated-sources/annotations in your IDE. + private static final CryptomatorComponent CRYPTOMATOR_COMPONENT = DaggerCryptomatorComponent.create(); + private static final Logger LOG = LoggerFactory.getLogger(Cryptomator.class); - static { - // DaggerCryptomatorComponent gets generated by Dagger. - // Run Maven and include target/generated-sources/annotations in your IDE. - CRYPTOMATOR_COMPONENT = DaggerCryptomatorComponent.create(); - CRYPTOMATOR_COMPONENT.initLogging().run(); - LOG = LoggerFactory.getLogger(Cryptomator.class); + private final LoggerConfiguration logConfig; + private final DebugMode debugMode; + private final IpcFactory ipcFactory; + private final Optional applicationVersion; + private final CountDownLatch shutdownLatch; + + @Inject + Cryptomator(LoggerConfiguration logConfig, DebugMode debugMode, IpcFactory ipcFactory, @Named("applicationVersion") Optional applicationVersion, @Named("shutdownLatch") CountDownLatch shutdownLatch) { + this.logConfig = logConfig; + this.debugMode = debugMode; + this.ipcFactory = ipcFactory; + this.applicationVersion = applicationVersion; + this.shutdownLatch = shutdownLatch; } public static void main(String[] args) { - LOG.info("Starting Cryptomator {} on {} {} ({})", CRYPTOMATOR_COMPONENT.applicationVersion().orElse("SNAPSHOT"), SystemUtils.OS_NAME, SystemUtils.OS_VERSION, SystemUtils.OS_ARCH); - - try (IpcFactory.IpcEndpoint endpoint = CRYPTOMATOR_COMPONENT.ipcFactory().create()) { - endpoint.getRemote().handleLaunchArgs(args); // if we are the server, getRemote() returns self. - if (endpoint.isConnectedToRemote()) { - LOG.info("Found running application instance. Shutting down."); - } else { - CRYPTOMATOR_COMPONENT.debugMode().initialize(); - CleanShutdownPerformer.registerShutdownHook(); - Application.launch(MainApp.class, args); - } - } catch (IOException e) { - LOG.error("Failed to initiate inter-process communication.", e); - System.exit(2); - } catch (Throwable e) { - LOG.error("Error during startup", e); - System.exit(1); - } - System.exit(0); // end remaining non-daemon threads. + int exitCode = CRYPTOMATOR_COMPONENT.application().run(args); + System.exit(exitCode); // end remaining non-daemon threads. } - // We need a separate FX Application class, until we can use the module system. See https://stackoverflow.com/q/54756176/4014509 - public static class MainApp extends Application { + /** + * Main entry point of the application launcher. + * @param args The arguments passed to this program via {@link #main(String[])}. + * @return Nonzero exit code in case of an error. + */ + private int run(String[] args) { + logConfig.init(); + LOG.info("Starting Cryptomator {} on {} {} ({})", applicationVersion.orElse("SNAPSHOT"), SystemUtils.OS_NAME, SystemUtils.OS_VERSION, SystemUtils.OS_ARCH); - @Override - public void start(Stage primaryStage) { - LOG.info("JavaFX application started."); - primaryStage.setMinWidth(652.0); - primaryStage.setMinHeight(440.0); - - FxApplicationComponent fxApplicationComponent = CRYPTOMATOR_COMPONENT.fxApplicationComponent() // - .fxApplication(this) // - .mainWindow(primaryStage) // - .build(); - - MainController mainCtrl = fxApplicationComponent.fxmlLoader().load("/fxml/main.fxml"); - mainCtrl.initStage(primaryStage); - primaryStage.show(); + if (sendArgsToRunningInstance(args)) { + LOG.info("Found running application instance. Shutting down..."); + return 0; } - @Override - public void stop() { - LOG.info("JavaFX application stopped."); + try { + runGuiApplication(); + LOG.info("Shutting down..."); + return 0; + } catch (Throwable e) { + LOG.error("Terminating due to error", e); + return 1; } + } + /** + * Attempts to create an IPC connection to a running Cryptomator instance and sends it the given args. + * If no external process could be reached, the args will be handled by the loopback IPC endpoint. + * + * @param args Arguments to send to the instance (if possible) + * @return true if a different process could be reached, false otherwise. + */ + private boolean sendArgsToRunningInstance(String[] args) { + try (IpcFactory.IpcEndpoint endpoint = ipcFactory.create()) { + endpoint.getRemote().handleLaunchArgs(args); // if we are the server, getRemote() returns self. + return endpoint.isConnectedToRemote(); + } catch (IOException e) { + LOG.error("Failed to initiate inter-process communication.", e); + return false; + } + } + + /** + * Launches the JavaFX application and waits until shutdown is requested. + * + * @implNote This method blocks until {@link #shutdownLatch} reached zero. + */ + private void runGuiApplication() { + try { + debugMode.initialize(); + CleanShutdownPerformer.registerShutdownHook(); + Platform.startup(() -> { + assert Platform.isFxApplicationThread(); + FxApplication app = CRYPTOMATOR_COMPONENT.fxApplicationComponent().application(); + app.start(); + }); + shutdownLatch.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } } } diff --git a/main/launcher/src/main/java/org/cryptomator/launcher/CryptomatorComponent.java b/main/launcher/src/main/java/org/cryptomator/launcher/CryptomatorComponent.java index 068823744..480ff4c74 100644 --- a/main/launcher/src/main/java/org/cryptomator/launcher/CryptomatorComponent.java +++ b/main/launcher/src/main/java/org/cryptomator/launcher/CryptomatorComponent.java @@ -14,16 +14,8 @@ import java.util.Optional; @Component(modules = {CryptomatorModule.class, CommonsModule.class, LoggerModule.class}) public interface CryptomatorComponent { - @Named("initLogging") - Runnable initLogging(); + Cryptomator application(); - DebugMode debugMode(); - - IpcFactory ipcFactory(); - - @Named("applicationVersion") - Optional applicationVersion(); - - FxApplicationComponent.Builder fxApplicationComponent(); + FxApplicationComponent fxApplicationComponent(); } diff --git a/main/launcher/src/main/java/org/cryptomator/launcher/CryptomatorModule.java b/main/launcher/src/main/java/org/cryptomator/launcher/CryptomatorModule.java index 8b93a2f08..2e7091acb 100644 --- a/main/launcher/src/main/java/org/cryptomator/launcher/CryptomatorModule.java +++ b/main/launcher/src/main/java/org/cryptomator/launcher/CryptomatorModule.java @@ -11,27 +11,35 @@ import javax.inject.Singleton; import java.util.Optional; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; @Module class CryptomatorModule { @Provides @Singleton - Settings provideSettings(SettingsProvider settingsProvider) { + @Named("shutdownLatch") + static CountDownLatch provideShutdownLatch() { + return new CountDownLatch(1); + } + + @Provides + @Singleton + static Settings provideSettings(SettingsProvider settingsProvider) { return settingsProvider.get(); } @Provides @Singleton @Named("launchEventQueue") - BlockingQueue provideFileOpenRequests() { + static BlockingQueue provideFileOpenRequests() { return new ArrayBlockingQueue<>(10); } @Provides @Singleton @Named("applicationVersion") - Optional provideApplicationVersion() { + static Optional provideApplicationVersion() { return Optional.ofNullable(Cryptomator.class.getPackage().getImplementationVersion()); } diff --git a/main/launcher/src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java b/main/launcher/src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java index 535e2767c..c422b8b51 100644 --- a/main/launcher/src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java +++ b/main/launcher/src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java @@ -8,6 +8,7 @@ package org.cryptomator.launcher; import java.awt.Desktop; import java.awt.desktop.OpenFilesEvent; +import java.awt.desktop.QuitStrategy; import java.io.File; import java.nio.file.FileSystem; import java.nio.file.FileSystems; diff --git a/main/launcher/src/main/java/org/cryptomator/launcher/FxApplication.java b/main/launcher/src/main/java/org/cryptomator/launcher/FxApplication.java new file mode 100644 index 000000000..e1b8b3a25 --- /dev/null +++ b/main/launcher/src/main/java/org/cryptomator/launcher/FxApplication.java @@ -0,0 +1,40 @@ +package org.cryptomator.launcher; + +import javafx.application.Application; +import javafx.stage.Stage; +import org.cryptomator.common.FxApplicationScoped; +import org.cryptomator.ui.controllers.MainController; +import org.cryptomator.ui.controllers.ViewControllerLoader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.inject.Named; + +@FxApplicationScoped +public class FxApplication extends Application { + + private static final Logger LOG = LoggerFactory.getLogger(FxApplication.class); + + private final Stage primaryStage; + private final ViewControllerLoader fxmlLoader; + + @Inject + FxApplication(@Named("mainWindow") Stage primaryStage, ViewControllerLoader fxmlLoader) { + this.primaryStage = primaryStage; + this.fxmlLoader = fxmlLoader; + } + + public void start() { + LOG.info("Starting GUI..."); + start(primaryStage); + } + + @Override + public void start(Stage primaryStage) { + MainController mainCtrl = fxmlLoader.load("/fxml/main.fxml"); + mainCtrl.initStage(primaryStage); + primaryStage.show(); + } + +} diff --git a/main/launcher/src/main/java/org/cryptomator/launcher/FxApplicationComponent.java b/main/launcher/src/main/java/org/cryptomator/launcher/FxApplicationComponent.java index 701e3e546..ce37f7fa0 100644 --- a/main/launcher/src/main/java/org/cryptomator/launcher/FxApplicationComponent.java +++ b/main/launcher/src/main/java/org/cryptomator/launcher/FxApplicationComponent.java @@ -5,33 +5,13 @@ *******************************************************************************/ package org.cryptomator.launcher; -import dagger.BindsInstance; import dagger.Subcomponent; -import javafx.application.Application; -import javafx.stage.Stage; import org.cryptomator.common.FxApplicationScoped; -import org.cryptomator.logging.DebugMode; -import org.cryptomator.ui.controllers.ViewControllerLoader; - -import javax.inject.Named; @FxApplicationScoped @Subcomponent(modules = FxApplicationModule.class) interface FxApplicationComponent { - ViewControllerLoader fxmlLoader(); - - @Subcomponent.Builder - interface Builder { - - @BindsInstance - Builder fxApplication(Application application); - - @BindsInstance - Builder mainWindow(@Named("mainWindow") Stage mainWindow); - - FxApplicationComponent build(); - - } + FxApplication application(); } diff --git a/main/launcher/src/main/java/org/cryptomator/launcher/FxApplicationModule.java b/main/launcher/src/main/java/org/cryptomator/launcher/FxApplicationModule.java index 1d9988d06..95e2b25b1 100644 --- a/main/launcher/src/main/java/org/cryptomator/launcher/FxApplicationModule.java +++ b/main/launcher/src/main/java/org/cryptomator/launcher/FxApplicationModule.java @@ -5,8 +5,12 @@ *******************************************************************************/ package org.cryptomator.launcher; +import dagger.Binds; import dagger.Module; import dagger.Provides; +import javafx.application.Application; +import javafx.application.Platform; +import javafx.stage.Stage; import org.cryptomator.common.FxApplicationScoped; import org.cryptomator.ui.UiModule; @@ -14,13 +18,28 @@ import javax.inject.Named; import java.util.function.Consumer; @Module(includes = {UiModule.class}) -class FxApplicationModule { +abstract class FxApplicationModule { @Provides @FxApplicationScoped @Named("shutdownTaskScheduler") - Consumer provideShutdownTaskScheduler() { + static Consumer provideShutdownTaskScheduler() { return CleanShutdownPerformer::scheduleShutdownTask; } + @Provides + @FxApplicationScoped + @Named("mainWindow") + static Stage providePrimaryStage() { + Stage stage = new Stage(); + stage.setMinWidth(652.0); + stage.setMinHeight(440.0); + return stage; + } + + @Binds + @FxApplicationScoped + abstract Application bindApplication(FxApplication application); + + } diff --git a/main/launcher/src/main/java/org/cryptomator/logging/LoggerConfiguration.java b/main/launcher/src/main/java/org/cryptomator/logging/LoggerConfiguration.java new file mode 100644 index 000000000..d1916abee --- /dev/null +++ b/main/launcher/src/main/java/org/cryptomator/logging/LoggerConfiguration.java @@ -0,0 +1,71 @@ +package org.cryptomator.logging; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.Appender; +import ch.qos.logback.core.hook.DelayingShutdownHook; +import ch.qos.logback.core.util.Duration; +import org.cryptomator.common.Environment; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; +import java.util.Map; + +@Singleton +public class LoggerConfiguration { + + private static final double SHUTDOWN_DELAY_MS = 100; + + private final LoggerContext context; + private final Environment environment; + private final Appender stdout; + private final Appender upgrade; + private final Appender file; + + @Inject + LoggerConfiguration(LoggerContext context, // + Environment environment, // + @Named("stdoutAppender") Appender stdout, // + @Named("upgradeAppender") Appender upgrade, // + @Named("fileAppender") Appender file) { + this.context = context; + this.environment = environment; + this.stdout = stdout; + this.upgrade = upgrade; + this.file = file; + } + + public void init() { + if (environment.useCustomLogbackConfig()) { + Logger root = context.getLogger(Logger.ROOT_LOGGER_NAME); + root.info("Using external logback configuration file."); + } else { + context.reset(); + + // configure loggers: + for (Map.Entry loglevel : LoggerModule.DEFAULT_LOG_LEVELS.entrySet()) { + Logger logger = context.getLogger(loglevel.getKey()); + logger.setLevel(loglevel.getValue()); + logger.setAdditive(false); + logger.addAppender(stdout); + logger.addAppender(file); + } + + // configure upgrade logger: + Logger upgrades = context.getLogger("org.cryptomator.ui.model.upgrade"); + upgrades.setLevel(Level.DEBUG); + upgrades.addAppender(stdout); + upgrades.addAppender(upgrade); + upgrades.setAdditive(false); + + // add shutdown hook + DelayingShutdownHook shutdownHook = new DelayingShutdownHook(); + shutdownHook.setContext(context); + shutdownHook.setDelay(Duration.buildByMilliseconds(SHUTDOWN_DELAY_MS)); + } + } + +} diff --git a/main/launcher/src/main/java/org/cryptomator/logging/LoggerModule.java b/main/launcher/src/main/java/org/cryptomator/logging/LoggerModule.java index 1c0ef1bcc..dab3a3476 100644 --- a/main/launcher/src/main/java/org/cryptomator/logging/LoggerModule.java +++ b/main/launcher/src/main/java/org/cryptomator/logging/LoggerModule.java @@ -32,7 +32,6 @@ public class LoggerModule { private static final String LOGFILE_ROLLING_PATTERN = "cryptomator%i.log"; private static final int LOGFILE_ROLLING_MIN = 1; private static final int LOGFILE_ROLLING_MAX = 9; - private static final double SHUTDOWN_DELAY_MS = 100; private static final String LOG_PATTERN = "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"; static final Map DEFAULT_LOG_LEVELS = Map.of( // Logger.ROOT_LOGGER_NAME, Level.INFO, // @@ -45,7 +44,7 @@ public class LoggerModule { @Provides @Singleton - LoggerContext provideLoggerContext() { + static LoggerContext provideLoggerContext() { ILoggerFactory loggerFactory = LoggerFactory.getILoggerFactory(); if (loggerFactory instanceof LoggerContext) { return (LoggerContext) loggerFactory; @@ -56,7 +55,7 @@ public class LoggerModule { @Provides @Singleton - PatternLayoutEncoder provideLayoutEncoder(LoggerContext context) { + static PatternLayoutEncoder provideLayoutEncoder(LoggerContext context) { PatternLayoutEncoder ple = new PatternLayoutEncoder(); ple.setPattern(LOG_PATTERN); ple.setContext(context); @@ -67,7 +66,7 @@ public class LoggerModule { @Provides @Singleton @Named("stdoutAppender") - Appender provideStdoutAppender(LoggerContext context, PatternLayoutEncoder encoder) { + static Appender provideStdoutAppender(LoggerContext context, PatternLayoutEncoder encoder) { ConsoleAppender appender = new ConsoleAppender<>(); appender.setContext(context); appender.setEncoder(encoder); @@ -78,7 +77,7 @@ public class LoggerModule { @Provides @Singleton @Named("fileAppender") - Appender provideFileAppender(LoggerContext context, PatternLayoutEncoder encoder, Environment environment) { + static Appender provideFileAppender(LoggerContext context, PatternLayoutEncoder encoder, Environment environment) { if (environment.getLogDir().isPresent()) { Path logDir = environment.getLogDir().get(); RollingFileAppender appender = new RollingFileAppender<>(); @@ -109,7 +108,7 @@ public class LoggerModule { @Provides @Singleton @Named("upgradeAppender") - Appender provideUpgradeAppender(LoggerContext context, PatternLayoutEncoder encoder, Environment environment) { + static Appender provideUpgradeAppender(LoggerContext context, PatternLayoutEncoder encoder, Environment environment) { if (environment.getLogDir().isPresent()) { FileAppender appender = new FileAppender<>(); appender.setFile(environment.getLogDir().get().resolve(UPGRADE_FILENAME).toString()); @@ -124,46 +123,5 @@ public class LoggerModule { } } - @Provides - @Singleton - @Named("initLogging") - Runnable provideLogbackInitializer(LoggerContext context, // - Environment environment, // - @Named("stdoutAppender") Appender stdout, // - @Named("upgradeAppender") Appender upgrade, // - @Named("fileAppender") Appender file) { - if (environment.useCustomLogbackConfig()) { - return () -> { - Logger root = context.getLogger(Logger.ROOT_LOGGER_NAME); - root.info("Using external logback configuration file."); - }; - } else { - return () -> { - context.reset(); - - // configure loggers: - for (Map.Entry loglevel : DEFAULT_LOG_LEVELS.entrySet()) { - Logger logger = context.getLogger(loglevel.getKey()); - logger.setLevel(loglevel.getValue()); - logger.setAdditive(false); - logger.addAppender(stdout); - logger.addAppender(file); - } - - // configure upgrade logger: - Logger upgrades = context.getLogger("org.cryptomator.ui.model.upgrade"); - upgrades.setLevel(Level.DEBUG); - upgrades.addAppender(stdout); - upgrades.addAppender(upgrade); - upgrades.setAdditive(false); - - // add shutdown hook - DelayingShutdownHook shutdownHook = new DelayingShutdownHook(); - shutdownHook.setContext(context); - shutdownHook.setDelay(Duration.buildByMilliseconds(SHUTDOWN_DELAY_MS)); - }; - } - } - } 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 e94a02f53..ba28726bf 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 @@ -78,6 +78,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.stream.Stream; @@ -99,6 +100,7 @@ public class MainController implements ViewController { private final ViewControllerLoader viewControllerLoader; private final ObjectProperty activeController = new SimpleObjectProperty<>(); private final ObservableList vaults; + private final CountDownLatch shutdownLatch; private final BooleanBinding areAllVaultsLocked; private final ObjectProperty selectedVault = new SimpleObjectProperty<>(); private final ObjectExpression selectedVaultState = ObjectExpression.objectExpression(EasyBind.select(selectedVault).selectObject(Vault::stateProperty)); @@ -112,7 +114,7 @@ public class MainController implements ViewController { @Inject public MainController(@Named("mainWindow") Stage mainWindow, ExecutorService executorService, @Named("launchEventQueue") BlockingQueue launchEventQueue, ExitUtil exitUtil, Localization localization, - VaultFactory vaultFactoy, ViewControllerLoader viewControllerLoader, UpgradeStrategies upgradeStrategies, VaultList vaults, AutoUnlocker autoUnlocker) { + VaultFactory vaultFactoy, ViewControllerLoader viewControllerLoader, UpgradeStrategies upgradeStrategies, VaultList vaults, AutoUnlocker autoUnlocker, @Named("shutdownLatch") CountDownLatch shutdownLatch) { this.mainWindow = mainWindow; this.executorService = executorService; this.launchEventQueue = launchEventQueue; @@ -121,6 +123,7 @@ public class MainController implements ViewController { this.vaultFactoy = vaultFactoy; this.viewControllerLoader = viewControllerLoader; this.vaults = vaults; + this.shutdownLatch = shutdownLatch; // derived bindings: this.isShowingSettings = Bindings.equal(SettingsController.class, EasyBind.monadic(activeController).map(ViewController::getClass)); @@ -231,13 +234,13 @@ public class MainController implements ViewController { if (tryAgainButtonType.equals(btnType)) { gracefulShutdown(); } else if (forceShutdownButtonType.equals(btnType)) { - Platform.runLater(Platform::exit); + shutdownLatch.countDown(); } else { return; } }); } else { - Platform.runLater(Platform::exit); + shutdownLatch.countDown(); } } diff --git a/main/ui/src/test/java/org/cryptomator/ui/controls/SecPasswordFieldTest.java b/main/ui/src/test/java/org/cryptomator/ui/controls/SecPasswordFieldTest.java index 34fac228f..545df27be 100644 --- a/main/ui/src/test/java/org/cryptomator/ui/controls/SecPasswordFieldTest.java +++ b/main/ui/src/test/java/org/cryptomator/ui/controls/SecPasswordFieldTest.java @@ -1,5 +1,6 @@ package org.cryptomator.ui.controls; +import javafx.application.Platform; import javafx.embed.swing.JFXPanel; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assumptions; @@ -22,10 +23,7 @@ class SecPasswordFieldTest { static void initJavaFx() throws InterruptedException { Assumptions.assumeFalse(GraphicsEnvironment.isHeadless()); final CountDownLatch latch = new CountDownLatch(1); - SwingUtilities.invokeLater(() -> { - new JFXPanel(); // initializes JavaFX environment - latch.countDown(); - }); + Platform.startup(latch::countDown); if (!latch.await(5L, TimeUnit.SECONDS)) { throw new ExceptionInInitializerError();