FxApplication now part of the Dagger graph

(references #663)
This commit is contained in:
Sebastian Stenzel
2019-02-26 22:41:33 +01:00
parent a3474e05eb
commit dd3c969f0f
11 changed files with 242 additions and 139 deletions

View File

@@ -5,74 +5,107 @@
*******************************************************************************/ *******************************************************************************/
package org.cryptomator.launcher; package org.cryptomator.launcher;
import javafx.application.Application; import javafx.application.Platform;
import javafx.stage.Stage;
import org.apache.commons.lang3.SystemUtils; 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.io.IOException; import java.io.IOException;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
@Singleton
public class Cryptomator { public class Cryptomator {
private static final Logger LOG; // DaggerCryptomatorComponent gets generated by Dagger.
private static final CryptomatorComponent CRYPTOMATOR_COMPONENT; // 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 { private final LoggerConfiguration logConfig;
// DaggerCryptomatorComponent gets generated by Dagger. private final DebugMode debugMode;
// Run Maven and include target/generated-sources/annotations in your IDE. private final IpcFactory ipcFactory;
CRYPTOMATOR_COMPONENT = DaggerCryptomatorComponent.create(); private final Optional<String> applicationVersion;
CRYPTOMATOR_COMPONENT.initLogging().run(); private final CountDownLatch shutdownLatch;
LOG = LoggerFactory.getLogger(Cryptomator.class);
@Inject
Cryptomator(LoggerConfiguration logConfig, DebugMode debugMode, IpcFactory ipcFactory, @Named("applicationVersion") Optional<String> 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) { 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); int exitCode = CRYPTOMATOR_COMPONENT.application().run(args);
System.exit(exitCode); // end remaining non-daemon threads.
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.
} }
// 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 if (sendArgsToRunningInstance(args)) {
public void start(Stage primaryStage) { LOG.info("Found running application instance. Shutting down...");
LOG.info("JavaFX application started."); return 0;
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();
} }
@Override try {
public void stop() { runGuiApplication();
LOG.info("JavaFX application stopped."); 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 <code>true</code> if a different process could be reached, <code>false</code> 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();
}
} }
} }

View File

@@ -14,16 +14,8 @@ import java.util.Optional;
@Component(modules = {CryptomatorModule.class, CommonsModule.class, LoggerModule.class}) @Component(modules = {CryptomatorModule.class, CommonsModule.class, LoggerModule.class})
public interface CryptomatorComponent { public interface CryptomatorComponent {
@Named("initLogging") Cryptomator application();
Runnable initLogging();
DebugMode debugMode(); FxApplicationComponent fxApplicationComponent();
IpcFactory ipcFactory();
@Named("applicationVersion")
Optional<String> applicationVersion();
FxApplicationComponent.Builder fxApplicationComponent();
} }

View File

@@ -11,27 +11,35 @@ import javax.inject.Singleton;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
@Module @Module
class CryptomatorModule { class CryptomatorModule {
@Provides @Provides
@Singleton @Singleton
Settings provideSettings(SettingsProvider settingsProvider) { @Named("shutdownLatch")
static CountDownLatch provideShutdownLatch() {
return new CountDownLatch(1);
}
@Provides
@Singleton
static Settings provideSettings(SettingsProvider settingsProvider) {
return settingsProvider.get(); return settingsProvider.get();
} }
@Provides @Provides
@Singleton @Singleton
@Named("launchEventQueue") @Named("launchEventQueue")
BlockingQueue<AppLaunchEvent> provideFileOpenRequests() { static BlockingQueue<AppLaunchEvent> provideFileOpenRequests() {
return new ArrayBlockingQueue<>(10); return new ArrayBlockingQueue<>(10);
} }
@Provides @Provides
@Singleton @Singleton
@Named("applicationVersion") @Named("applicationVersion")
Optional<String> provideApplicationVersion() { static Optional<String> provideApplicationVersion() {
return Optional.ofNullable(Cryptomator.class.getPackage().getImplementationVersion()); return Optional.ofNullable(Cryptomator.class.getPackage().getImplementationVersion());
} }

View File

@@ -8,6 +8,7 @@ package org.cryptomator.launcher;
import java.awt.Desktop; import java.awt.Desktop;
import java.awt.desktop.OpenFilesEvent; import java.awt.desktop.OpenFilesEvent;
import java.awt.desktop.QuitStrategy;
import java.io.File; import java.io.File;
import java.nio.file.FileSystem; import java.nio.file.FileSystem;
import java.nio.file.FileSystems; import java.nio.file.FileSystems;

View File

@@ -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();
}
}

View File

@@ -5,33 +5,13 @@
*******************************************************************************/ *******************************************************************************/
package org.cryptomator.launcher; package org.cryptomator.launcher;
import dagger.BindsInstance;
import dagger.Subcomponent; import dagger.Subcomponent;
import javafx.application.Application;
import javafx.stage.Stage;
import org.cryptomator.common.FxApplicationScoped; import org.cryptomator.common.FxApplicationScoped;
import org.cryptomator.logging.DebugMode;
import org.cryptomator.ui.controllers.ViewControllerLoader;
import javax.inject.Named;
@FxApplicationScoped @FxApplicationScoped
@Subcomponent(modules = FxApplicationModule.class) @Subcomponent(modules = FxApplicationModule.class)
interface FxApplicationComponent { interface FxApplicationComponent {
ViewControllerLoader fxmlLoader(); FxApplication application();
@Subcomponent.Builder
interface Builder {
@BindsInstance
Builder fxApplication(Application application);
@BindsInstance
Builder mainWindow(@Named("mainWindow") Stage mainWindow);
FxApplicationComponent build();
}
} }

View File

@@ -5,8 +5,12 @@
*******************************************************************************/ *******************************************************************************/
package org.cryptomator.launcher; package org.cryptomator.launcher;
import dagger.Binds;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.Provides;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.stage.Stage;
import org.cryptomator.common.FxApplicationScoped; import org.cryptomator.common.FxApplicationScoped;
import org.cryptomator.ui.UiModule; import org.cryptomator.ui.UiModule;
@@ -14,13 +18,28 @@ import javax.inject.Named;
import java.util.function.Consumer; import java.util.function.Consumer;
@Module(includes = {UiModule.class}) @Module(includes = {UiModule.class})
class FxApplicationModule { abstract class FxApplicationModule {
@Provides @Provides
@FxApplicationScoped @FxApplicationScoped
@Named("shutdownTaskScheduler") @Named("shutdownTaskScheduler")
Consumer<Runnable> provideShutdownTaskScheduler() { static Consumer<Runnable> provideShutdownTaskScheduler() {
return CleanShutdownPerformer::scheduleShutdownTask; 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);
} }

View File

@@ -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<ILoggingEvent> stdout;
private final Appender<ILoggingEvent> upgrade;
private final Appender<ILoggingEvent> file;
@Inject
LoggerConfiguration(LoggerContext context, //
Environment environment, //
@Named("stdoutAppender") Appender<ILoggingEvent> stdout, //
@Named("upgradeAppender") Appender<ILoggingEvent> upgrade, //
@Named("fileAppender") Appender<ILoggingEvent> 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<String, Level> 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));
}
}
}

View File

@@ -32,7 +32,6 @@ public class LoggerModule {
private static final String LOGFILE_ROLLING_PATTERN = "cryptomator%i.log"; private static final String LOGFILE_ROLLING_PATTERN = "cryptomator%i.log";
private static final int LOGFILE_ROLLING_MIN = 1; private static final int LOGFILE_ROLLING_MIN = 1;
private static final int LOGFILE_ROLLING_MAX = 9; 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"; private static final String LOG_PATTERN = "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n";
static final Map<String, Level> DEFAULT_LOG_LEVELS = Map.of( // static final Map<String, Level> DEFAULT_LOG_LEVELS = Map.of( //
Logger.ROOT_LOGGER_NAME, Level.INFO, // Logger.ROOT_LOGGER_NAME, Level.INFO, //
@@ -45,7 +44,7 @@ public class LoggerModule {
@Provides @Provides
@Singleton @Singleton
LoggerContext provideLoggerContext() { static LoggerContext provideLoggerContext() {
ILoggerFactory loggerFactory = LoggerFactory.getILoggerFactory(); ILoggerFactory loggerFactory = LoggerFactory.getILoggerFactory();
if (loggerFactory instanceof LoggerContext) { if (loggerFactory instanceof LoggerContext) {
return (LoggerContext) loggerFactory; return (LoggerContext) loggerFactory;
@@ -56,7 +55,7 @@ public class LoggerModule {
@Provides @Provides
@Singleton @Singleton
PatternLayoutEncoder provideLayoutEncoder(LoggerContext context) { static PatternLayoutEncoder provideLayoutEncoder(LoggerContext context) {
PatternLayoutEncoder ple = new PatternLayoutEncoder(); PatternLayoutEncoder ple = new PatternLayoutEncoder();
ple.setPattern(LOG_PATTERN); ple.setPattern(LOG_PATTERN);
ple.setContext(context); ple.setContext(context);
@@ -67,7 +66,7 @@ public class LoggerModule {
@Provides @Provides
@Singleton @Singleton
@Named("stdoutAppender") @Named("stdoutAppender")
Appender<ILoggingEvent> provideStdoutAppender(LoggerContext context, PatternLayoutEncoder encoder) { static Appender<ILoggingEvent> provideStdoutAppender(LoggerContext context, PatternLayoutEncoder encoder) {
ConsoleAppender<ILoggingEvent> appender = new ConsoleAppender<>(); ConsoleAppender<ILoggingEvent> appender = new ConsoleAppender<>();
appender.setContext(context); appender.setContext(context);
appender.setEncoder(encoder); appender.setEncoder(encoder);
@@ -78,7 +77,7 @@ public class LoggerModule {
@Provides @Provides
@Singleton @Singleton
@Named("fileAppender") @Named("fileAppender")
Appender<ILoggingEvent> provideFileAppender(LoggerContext context, PatternLayoutEncoder encoder, Environment environment) { static Appender<ILoggingEvent> provideFileAppender(LoggerContext context, PatternLayoutEncoder encoder, Environment environment) {
if (environment.getLogDir().isPresent()) { if (environment.getLogDir().isPresent()) {
Path logDir = environment.getLogDir().get(); Path logDir = environment.getLogDir().get();
RollingFileAppender<ILoggingEvent> appender = new RollingFileAppender<>(); RollingFileAppender<ILoggingEvent> appender = new RollingFileAppender<>();
@@ -109,7 +108,7 @@ public class LoggerModule {
@Provides @Provides
@Singleton @Singleton
@Named("upgradeAppender") @Named("upgradeAppender")
Appender<ILoggingEvent> provideUpgradeAppender(LoggerContext context, PatternLayoutEncoder encoder, Environment environment) { static Appender<ILoggingEvent> provideUpgradeAppender(LoggerContext context, PatternLayoutEncoder encoder, Environment environment) {
if (environment.getLogDir().isPresent()) { if (environment.getLogDir().isPresent()) {
FileAppender<ILoggingEvent> appender = new FileAppender<>(); FileAppender<ILoggingEvent> appender = new FileAppender<>();
appender.setFile(environment.getLogDir().get().resolve(UPGRADE_FILENAME).toString()); 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<ILoggingEvent> stdout, //
@Named("upgradeAppender") Appender<ILoggingEvent> upgrade, //
@Named("fileAppender") Appender<ILoggingEvent> 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<String, Level> 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));
};
}
}
} }

View File

@@ -78,6 +78,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.stream.Stream; import java.util.stream.Stream;
@@ -99,6 +100,7 @@ public class MainController implements ViewController {
private final ViewControllerLoader viewControllerLoader; private final ViewControllerLoader viewControllerLoader;
private final ObjectProperty<ViewController> activeController = new SimpleObjectProperty<>(); private final ObjectProperty<ViewController> activeController = new SimpleObjectProperty<>();
private final ObservableList<Vault> vaults; private final ObservableList<Vault> vaults;
private final CountDownLatch shutdownLatch;
private final BooleanBinding areAllVaultsLocked; private final BooleanBinding areAllVaultsLocked;
private final ObjectProperty<Vault> selectedVault = new SimpleObjectProperty<>(); private final ObjectProperty<Vault> selectedVault = new SimpleObjectProperty<>();
private final ObjectExpression<Vault.State> selectedVaultState = ObjectExpression.objectExpression(EasyBind.select(selectedVault).selectObject(Vault::stateProperty)); private final ObjectExpression<Vault.State> selectedVaultState = ObjectExpression.objectExpression(EasyBind.select(selectedVault).selectObject(Vault::stateProperty));
@@ -112,7 +114,7 @@ public class MainController implements ViewController {
@Inject @Inject
public MainController(@Named("mainWindow") Stage mainWindow, ExecutorService executorService, @Named("launchEventQueue") BlockingQueue<AppLaunchEvent> launchEventQueue, ExitUtil exitUtil, Localization localization, public MainController(@Named("mainWindow") Stage mainWindow, ExecutorService executorService, @Named("launchEventQueue") BlockingQueue<AppLaunchEvent> 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.mainWindow = mainWindow;
this.executorService = executorService; this.executorService = executorService;
this.launchEventQueue = launchEventQueue; this.launchEventQueue = launchEventQueue;
@@ -121,6 +123,7 @@ public class MainController implements ViewController {
this.vaultFactoy = vaultFactoy; this.vaultFactoy = vaultFactoy;
this.viewControllerLoader = viewControllerLoader; this.viewControllerLoader = viewControllerLoader;
this.vaults = vaults; this.vaults = vaults;
this.shutdownLatch = shutdownLatch;
// derived bindings: // derived bindings:
this.isShowingSettings = Bindings.equal(SettingsController.class, EasyBind.monadic(activeController).map(ViewController::getClass)); 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)) { if (tryAgainButtonType.equals(btnType)) {
gracefulShutdown(); gracefulShutdown();
} else if (forceShutdownButtonType.equals(btnType)) { } else if (forceShutdownButtonType.equals(btnType)) {
Platform.runLater(Platform::exit); shutdownLatch.countDown();
} else { } else {
return; return;
} }
}); });
} else { } else {
Platform.runLater(Platform::exit); shutdownLatch.countDown();
} }
} }

View File

@@ -1,5 +1,6 @@
package org.cryptomator.ui.controls; package org.cryptomator.ui.controls;
import javafx.application.Platform;
import javafx.embed.swing.JFXPanel; import javafx.embed.swing.JFXPanel;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Assumptions;
@@ -22,10 +23,7 @@ class SecPasswordFieldTest {
static void initJavaFx() throws InterruptedException { static void initJavaFx() throws InterruptedException {
Assumptions.assumeFalse(GraphicsEnvironment.isHeadless()); Assumptions.assumeFalse(GraphicsEnvironment.isHeadless());
final CountDownLatch latch = new CountDownLatch(1); final CountDownLatch latch = new CountDownLatch(1);
SwingUtilities.invokeLater(() -> { Platform.startup(latch::countDown);
new JFXPanel(); // initializes JavaFX environment
latch.countDown();
});
if (!latch.await(5L, TimeUnit.SECONDS)) { if (!latch.await(5L, TimeUnit.SECONDS)) {
throw new ExceptionInInitializerError(); throw new ExceptionInInitializerError();