Browse Source

Merge pull request #1767 from cryptomator/feature/early-loading

Eagerly load vault config
Armin Schrenk 3 years ago
parent
commit
cfe61a51b6

+ 9 - 21
src/main/java/org/cryptomator/common/vaults/Vault.java

@@ -17,9 +17,6 @@ import org.cryptomator.cryptofs.CryptoFileSystem;
 import org.cryptomator.cryptofs.CryptoFileSystemProperties;
 import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags;
 import org.cryptomator.cryptofs.CryptoFileSystemProvider;
-import org.cryptomator.cryptofs.VaultConfig;
-import org.cryptomator.cryptofs.VaultConfig.UnverifiedVaultConfig;
-import org.cryptomator.cryptofs.VaultConfigLoadException;
 import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
 import org.cryptomator.cryptolib.api.CryptoException;
 import org.cryptomator.cryptolib.api.MasterkeyLoader;
@@ -38,8 +35,6 @@ import javafx.beans.property.BooleanProperty;
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.SimpleBooleanProperty;
 import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.EnumSet;
@@ -62,6 +57,7 @@ public class Vault {
 	private final AtomicReference<CryptoFileSystem> cryptoFileSystem;
 	private final VaultState state;
 	private final ObjectProperty<Exception> lastKnownException;
+	private final VaultConfigCache configCache;
 	private final VaultStats stats;
 	private final StringBinding displayName;
 	private final StringBinding displayablePath;
@@ -78,8 +74,9 @@ public class Vault {
 	private volatile Volume volume;
 
 	@Inject
-	Vault(VaultSettings vaultSettings, Provider<Volume> volumeProvider, @DefaultMountFlags StringBinding defaultMountFlags, AtomicReference<CryptoFileSystem> cryptoFileSystem, VaultState state, @Named("lastKnownException") ObjectProperty<Exception> lastKnownException, VaultStats stats) {
+	Vault(VaultSettings vaultSettings, VaultConfigCache configCache, Provider<Volume> volumeProvider, @DefaultMountFlags StringBinding defaultMountFlags, AtomicReference<CryptoFileSystem> cryptoFileSystem, VaultState state, @Named("lastKnownException") ObjectProperty<Exception> lastKnownException, VaultStats stats) {
 		this.vaultSettings = vaultSettings;
+		this.configCache = configCache;
 		this.volumeProvider = volumeProvider;
 		this.defaultMountFlags = defaultMountFlags;
 		this.cryptoFileSystem = cryptoFileSystem;
@@ -107,10 +104,10 @@ public class Vault {
 		Set<FileSystemFlags> flags = EnumSet.noneOf(FileSystemFlags.class);
 		if (vaultSettings.usesReadOnlyMode().get()) {
 			flags.add(FileSystemFlags.READONLY);
-		} else if(vaultSettings.maxCleartextFilenameLength().get() == -1) {
+		} else if (vaultSettings.maxCleartextFilenameLength().get() == -1) {
 			LOG.debug("Determining cleartext filename length limitations...");
 			var checker = new FileSystemCapabilityChecker();
-			int shorteningThreshold = getUnverifiedVaultConfig().allegedShorteningThreshold();
+			int shorteningThreshold = configCache.get().allegedShorteningThreshold();
 			int ciphertextLimit = checker.determineSupportedCiphertextFileNameLength(getPath());
 			if (ciphertextLimit < shorteningThreshold) {
 				int cleartextLimit = checker.determineSupportedCleartextFileNameLength(getPath());
@@ -328,19 +325,6 @@ public class Vault {
 		return stats;
 	}
 
-	/**
-	 * Attempts to read the vault config file and parse it without verifying its integrity.
-	 *
-	 * @return an unverified vault config
-	 * @throws VaultConfigLoadException if the read file cannot be properly parsed
-	 * @throws IOException if reading the file fails
-	 *
-	 */
-	public UnverifiedVaultConfig getUnverifiedVaultConfig() throws IOException {
-		Path configPath = getPath().resolve(org.cryptomator.common.Constants.VAULTCONFIG_FILENAME);
-		String token = Files.readString(configPath, StandardCharsets.US_ASCII);
-		return VaultConfig.decode(token);
-	}
 
 	public Observable[] observables() {
 		return new Observable[]{state};
@@ -375,6 +359,10 @@ public class Vault {
 		}
 	}
 
+	public VaultConfigCache getVaultConfigCache() {
+		return configCache;
+	}
+
 	public void setCustomMountFlags(String mountFlags) {
 		vaultSettings.mountFlags().set(mountFlags);
 	}

+ 3 - 0
src/main/java/org/cryptomator/common/vaults/VaultComponent.java

@@ -25,6 +25,9 @@ public interface VaultComponent {
 		@BindsInstance
 		Builder vaultSettings(VaultSettings vaultSettings);
 
+		@BindsInstance
+		Builder vaultConfigCache(VaultConfigCache configCache);
+
 		@BindsInstance
 		Builder initialVaultState(VaultState.Value vaultState);
 

+ 65 - 0
src/main/java/org/cryptomator/common/vaults/VaultConfigCache.java

@@ -0,0 +1,65 @@
+package org.cryptomator.common.vaults;
+
+import org.cryptomator.common.Constants;
+import org.cryptomator.common.settings.VaultSettings;
+import org.cryptomator.cryptofs.VaultConfig;
+import org.cryptomator.cryptofs.VaultConfigLoadException;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Wrapper for lazy loading and on-demand reloading of the vault configuration.
+ */
+public class VaultConfigCache {
+
+	private final VaultSettings settings;
+	private final AtomicReference<VaultConfig.UnverifiedVaultConfig> config;
+
+	VaultConfigCache(VaultSettings settings) {
+		this.settings = settings;
+		this.config = new AtomicReference<>(null);
+	}
+
+	void reloadConfig() throws IOException {
+		try {
+			config.set(readConfigFromStorage(this.settings.path().get()));
+		} catch (IOException e) {
+			config.set(null);
+			throw e;
+		}
+	}
+
+	public VaultConfig.UnverifiedVaultConfig get() throws IOException {
+		if (config.get() == null) {
+			reloadConfig();
+		}
+		return config.get();
+	}
+
+	public VaultConfig.UnverifiedVaultConfig getUnchecked() {
+		try {
+			return get();
+		} catch (IOException e) {
+			throw new UncheckedIOException(e);
+		}
+	}
+
+
+	/**
+	 * Attempts to read the vault config file and parse it without verifying its integrity.
+	 *
+	 * @throws VaultConfigLoadException if the read file cannot be properly parsed
+	 * @throws IOException if reading the file fails
+	 */
+	static VaultConfig.UnverifiedVaultConfig readConfigFromStorage(Path vaultPath) throws IOException {
+		Path configPath = vaultPath.resolve(Constants.VAULTCONFIG_FILENAME);
+		String token = Files.readString(configPath, StandardCharsets.US_ASCII);
+		return VaultConfig.decode(token);
+	}
+
+}

+ 12 - 2
src/main/java/org/cryptomator/common/vaults/VaultListManager.java

@@ -18,7 +18,6 @@ import org.slf4j.LoggerFactory;
 
 import javax.inject.Inject;
 import javax.inject.Singleton;
-import javafx.collections.FXCollections;
 import javafx.collections.ObservableList;
 import java.io.IOException;
 import java.nio.file.Files;
@@ -31,6 +30,7 @@ import java.util.ResourceBundle;
 import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
 import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME;
 import static org.cryptomator.common.vaults.VaultState.Value.ERROR;
+import static org.cryptomator.common.vaults.VaultState.Value.LOCKED;
 
 @Singleton
 public class VaultListManager {
@@ -96,6 +96,11 @@ public class VaultListManager {
 		VaultComponent.Builder compBuilder = vaultComponentBuilder.vaultSettings(vaultSettings);
 		try {
 			VaultState.Value vaultState = determineVaultState(vaultSettings.path().get());
+			VaultConfigCache wrapper = new VaultConfigCache(vaultSettings);
+			compBuilder.vaultConfigCache(wrapper); //first set the wrapper in the builder, THEN try to load config
+			if (vaultState == LOCKED) { //for legacy reasons: pre v8 vault do not have a config, but they are in the NEEDS_MIGRATION state
+				wrapper.reloadConfig();
+			}
 			compBuilder.initialVaultState(vaultState);
 		} catch (IOException e) {
 			LOG.warn("Failed to determine vault state for " + vaultSettings.path().get(), e);
@@ -112,6 +117,9 @@ public class VaultListManager {
 			case LOCKED, NEEDS_MIGRATION, MISSING -> {
 				try {
 					var determinedState = determineVaultState(vault.getPath());
+					if (determinedState == LOCKED) {
+						vault.getVaultConfigCache().reloadConfig();
+					}
 					state.set(determinedState);
 					yield determinedState;
 				} catch (IOException e) {
@@ -132,7 +140,9 @@ public class VaultListManager {
 		return switch (CryptoFileSystemProvider.checkDirStructureForVault(pathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME)) {
 			case VAULT -> VaultState.Value.LOCKED;
 			case UNRELATED -> VaultState.Value.MISSING;
-			case MAYBE_LEGACY -> Migrators.get().needsMigration(pathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME) ? VaultState.Value.NEEDS_MIGRATION : VaultState.Value.MISSING;
+			case MAYBE_LEGACY -> Migrators.get().needsMigration(pathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME) ? //
+					VaultState.Value.NEEDS_MIGRATION //
+					: VaultState.Value.MISSING;
 		};
 	}
 

+ 0 - 1
src/main/java/org/cryptomator/ui/common/FxmlFile.java

@@ -12,7 +12,6 @@ public enum FxmlFile {
 	ERROR("/fxml/error.fxml"), //
 	FORGET_PASSWORD("/fxml/forget_password.fxml"), //
 	HEALTH_START("/fxml/health_start.fxml"), //
-	HEALTH_START_FAIL("/fxml/health_start_fail.fxml"), //
 	HEALTH_CHECK_LIST("/fxml/health_check_list.fxml"), //
 	LOCK_FORCED("/fxml/lock_forced.fxml"), //
 	LOCK_FAILED("/fxml/lock_failed.fxml"), //

+ 1 - 13
src/main/java/org/cryptomator/ui/health/HealthCheckComponent.java

@@ -16,26 +16,15 @@ import javafx.stage.Stage;
 @Subcomponent(modules = {HealthCheckModule.class})
 public interface HealthCheckComponent {
 
-	LoadUnverifiedConfigResult loadConfig();
-
 	@HealthCheckWindow
 	Stage window();
 
 	@FxmlScene(FxmlFile.HEALTH_START)
 	Lazy<Scene> startScene();
 
-	@FxmlScene(FxmlFile.HEALTH_START_FAIL)
-	Lazy<Scene> failScene();
-
 	default Stage showHealthCheckWindow() {
 		Stage stage = window();
-		// TODO reevaluate config loading, as soon as we have the new generic error screen
-		var unverifiedConf = loadConfig();
-		if (unverifiedConf.config() != null) {
-			stage.setScene(startScene().get());
-		} else {
-			stage.setScene(failScene().get());
-		}
+		stage.setScene(startScene().get());
 		stage.show();
 		return stage;
 	}
@@ -52,5 +41,4 @@ public interface HealthCheckComponent {
 		HealthCheckComponent build();
 	}
 
-	record LoadUnverifiedConfigResult(VaultConfig.UnverifiedVaultConfig config, Throwable error) {}
 }

+ 0 - 25
src/main/java/org/cryptomator/ui/health/HealthCheckModule.java

@@ -27,7 +27,6 @@ import javafx.scene.Scene;
 import javafx.stage.Modality;
 import javafx.stage.Stage;
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -37,18 +36,6 @@ import java.util.concurrent.atomic.AtomicReference;
 @Module(subcomponents = {KeyLoadingComponent.class})
 abstract class HealthCheckModule {
 
-	// TODO reevaluate config loading, as soon as we have the new generic error screen
-	@Provides
-	@HealthCheckScoped
-	static HealthCheckComponent.LoadUnverifiedConfigResult provideLoadConfigResult(@HealthCheckWindow Vault vault) {
-		try {
-			return new HealthCheckComponent.LoadUnverifiedConfigResult(vault.getUnverifiedVaultConfig(), null);
-		} catch (IOException e) {
-			return new HealthCheckComponent.LoadUnverifiedConfigResult(null, e);
-		}
-	}
-
-
 	@Provides
 	@HealthCheckScoped
 	static AtomicReference<Masterkey> provideMasterkeyRef() {
@@ -129,13 +116,6 @@ abstract class HealthCheckModule {
 		return fxmlLoaders.createScene(FxmlFile.HEALTH_START);
 	}
 
-	@Provides
-	@FxmlScene(FxmlFile.HEALTH_START_FAIL)
-	@HealthCheckScoped
-	static Scene provideHealthStartFailScene(@HealthCheckWindow FxmlLoaderFactory fxmlLoaders) {
-		return fxmlLoaders.createScene(FxmlFile.HEALTH_START_FAIL);
-	}
-
 	@Provides
 	@FxmlScene(FxmlFile.HEALTH_CHECK_LIST)
 	@HealthCheckScoped
@@ -148,11 +128,6 @@ abstract class HealthCheckModule {
 	@FxControllerKey(StartController.class)
 	abstract FxController bindStartController(StartController controller);
 
-	@Binds
-	@IntoMap
-	@FxControllerKey(StartFailController.class)
-	abstract FxController bindStartFailController(StartFailController controller);
-
 	@Binds
 	@IntoMap
 	@FxControllerKey(CheckListController.class)

+ 6 - 9
src/main/java/org/cryptomator/ui/health/StartController.java

@@ -1,7 +1,8 @@
 package org.cryptomator.ui.health;
 
-import com.google.common.base.Preconditions;
 import dagger.Lazy;
+import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.common.vaults.VaultConfigCache;
 import org.cryptomator.cryptofs.VaultConfig;
 import org.cryptomator.cryptofs.VaultConfigLoadException;
 import org.cryptomator.cryptofs.VaultKeyInvalidException;
@@ -18,8 +19,6 @@ import org.slf4j.LoggerFactory;
 import javax.inject.Inject;
 import javax.inject.Named;
 import javafx.application.Platform;
-import javafx.beans.property.ObjectProperty;
-import javafx.beans.property.SimpleObjectProperty;
 import javafx.fxml.FXML;
 import javafx.scene.Scene;
 import javafx.stage.Stage;
@@ -35,7 +34,7 @@ public class StartController implements FxController {
 
 	private final Stage window;
 	private final Stage unlockWindow;
-	private final ObjectProperty<VaultConfig.UnverifiedVaultConfig> unverifiedVaultConfig;
+	private final VaultConfigCache vaultConfig;
 	private final KeyLoadingStrategy keyLoadingStrategy;
 	private final ExecutorService executor;
 	private final AtomicReference<Masterkey> masterkeyRef;
@@ -44,11 +43,10 @@ public class StartController implements FxController {
 	private final Lazy<ErrorComponent.Builder> errorComponent;
 
 	@Inject
-	public StartController(@HealthCheckWindow Stage window, HealthCheckComponent.LoadUnverifiedConfigResult configLoadResult, @HealthCheckWindow KeyLoadingStrategy keyLoadingStrategy, ExecutorService executor, AtomicReference<Masterkey> masterkeyRef, AtomicReference<VaultConfig> vaultConfigRef, @FxmlScene(FxmlFile.HEALTH_CHECK_LIST) Lazy<Scene> checkScene, Lazy<ErrorComponent.Builder> errorComponent, @Named("unlockWindow") Stage unlockWindow) {
-		Preconditions.checkNotNull(configLoadResult.config());
+	public StartController(@HealthCheckWindow Stage window, @HealthCheckWindow Vault vault, @HealthCheckWindow KeyLoadingStrategy keyLoadingStrategy, ExecutorService executor, AtomicReference<Masterkey> masterkeyRef, AtomicReference<VaultConfig> vaultConfigRef, @FxmlScene(FxmlFile.HEALTH_CHECK_LIST) Lazy<Scene> checkScene, Lazy<ErrorComponent.Builder> errorComponent, @Named("unlockWindow") Stage unlockWindow) {
 		this.window = window;
 		this.unlockWindow = unlockWindow;
-		this.unverifiedVaultConfig = new SimpleObjectProperty<>(configLoadResult.config());
+		this.vaultConfig = vault.getVaultConfigCache();
 		this.keyLoadingStrategy = keyLoadingStrategy;
 		this.executor = executor;
 		this.masterkeyRef = masterkeyRef;
@@ -71,7 +69,6 @@ public class StartController implements FxController {
 
 	private void loadKey() {
 		assert !Platform.isFxApplicationThread();
-		assert unverifiedVaultConfig.get() != null;
 		try {
 			keyLoadingStrategy.use(this::verifyVaultConfig);
 		} catch (VaultConfigLoadException | UnlockCancelledException e) {
@@ -80,7 +77,7 @@ public class StartController implements FxController {
 	}
 
 	private void verifyVaultConfig(KeyLoadingStrategy keyLoadingStrategy) throws VaultConfigLoadException {
-		var unverifiedCfg = unverifiedVaultConfig.get();
+		var unverifiedCfg = vaultConfig.getUnchecked();
 		try (var masterkey = keyLoadingStrategy.loadKey(unverifiedCfg.getKeyId())) {
 			var verifiedCfg = unverifiedCfg.verify(masterkey.getEncoded(), unverifiedCfg.allegedVaultVersion());
 			vaultConfigRef.set(verifiedCfg);

+ 0 - 79
src/main/java/org/cryptomator/ui/health/StartFailController.java

@@ -1,79 +0,0 @@
-package org.cryptomator.ui.health;
-
-import com.google.common.base.Preconditions;
-import org.cryptomator.cryptofs.VaultConfigLoadException;
-import org.cryptomator.ui.common.FxController;
-import org.cryptomator.ui.controls.FontAwesome5Icon;
-
-import javax.inject.Inject;
-import javafx.beans.property.ObjectProperty;
-import javafx.beans.property.SimpleObjectProperty;
-import javafx.beans.value.ObservableValue;
-import javafx.fxml.FXML;
-import javafx.scene.control.TitledPane;
-import javafx.stage.Stage;
-import java.io.ByteArrayOutputStream;
-import java.io.PrintStream;
-import java.nio.charset.StandardCharsets;
-
-// TODO reevaluate config loading, as soon as we have the new generic error screen
-@HealthCheckScoped
-public class StartFailController implements FxController {
-
-	private final Stage window;
-	private final ObjectProperty<Throwable> loadError;
-	private final ObjectProperty<FontAwesome5Icon> moreInfoIcon;
-
-	/* FXML */
-	public TitledPane moreInfoPane;
-
-	@Inject
-	public StartFailController(@HealthCheckWindow Stage window, HealthCheckComponent.LoadUnverifiedConfigResult configLoadResult) {
-		Preconditions.checkNotNull(configLoadResult.error());
-		this.window = window;
-		this.loadError = new SimpleObjectProperty<>(configLoadResult.error());
-		this.moreInfoIcon = new SimpleObjectProperty<>(FontAwesome5Icon.CARET_RIGHT);
-	}
-
-	public void initialize() {
-		moreInfoPane.expandedProperty().addListener(this::setMoreInfoIcon);
-	}
-
-	private void setMoreInfoIcon(ObservableValue<? extends Boolean> observable, boolean wasExpanded, boolean willExpand) {
-		moreInfoIcon.set(willExpand ? FontAwesome5Icon.CARET_DOWN : FontAwesome5Icon.CARET_RIGHT);
-	}
-
-	@FXML
-	public void close() {
-		window.close();
-	}
-
-	/* Getter & Setter */
-
-	public ObjectProperty<FontAwesome5Icon> moreInfoIconProperty() {
-		return moreInfoIcon;
-	}
-
-	public FontAwesome5Icon getMoreInfoIcon() {
-		return moreInfoIcon.getValue();
-	}
-
-	public String getStackTrace() {
-		ByteArrayOutputStream baos = new ByteArrayOutputStream();
-		loadError.get().printStackTrace(new PrintStream(baos));
-		return baos.toString(StandardCharsets.UTF_8);
-	}
-
-	public String getLocalizedErrorMessage() {
-		return loadError.get().getLocalizedMessage();
-	}
-
-	public boolean isParseException() {
-		return loadError.get() instanceof VaultConfigLoadException;
-	}
-
-	public boolean isIoException() {
-		return !isParseException();
-	}
-
-}

+ 1 - 4
src/main/java/org/cryptomator/ui/keyloading/KeyLoadingModule.java

@@ -3,7 +3,6 @@ package org.cryptomator.ui.keyloading;
 import dagger.Module;
 import dagger.Provides;
 import org.cryptomator.common.vaults.Vault;
-import org.cryptomator.cryptofs.VaultConfig.UnverifiedVaultConfig;
 import org.cryptomator.ui.common.DefaultSceneFactory;
 import org.cryptomator.ui.common.FxController;
 import org.cryptomator.ui.common.FxmlLoaderFactory;
@@ -11,9 +10,7 @@ import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingModule;
 
 import javax.inject.Provider;
 import java.io.IOException;
-import java.net.URI;
 import java.util.Map;
-import java.util.Optional;
 import java.util.ResourceBundle;
 
 @Module(includes = {MasterkeyFileLoadingModule.class})
@@ -31,7 +28,7 @@ abstract class KeyLoadingModule {
 	@KeyLoadingScoped
 	static KeyLoadingStrategy provideKeyLoaderProvider(@KeyLoading Vault vault, Map<String, Provider<KeyLoadingStrategy>> strategies) {
 		try {
-			String scheme = vault.getUnverifiedVaultConfig().getKeyId().getScheme();
+			String scheme = vault.getVaultConfigCache().get().getKeyId().getScheme();
 			var fallback = KeyLoadingStrategy.failed(new IllegalArgumentException("Unsupported key id " + scheme));
 			return strategies.getOrDefault(scheme, () -> fallback).get();
 		} catch (IOException e) {

+ 0 - 44
src/main/resources/fxml/health_start_fail.fxml

@@ -1,44 +0,0 @@
-<?import javafx.scene.layout.VBox?>
-<?import javafx.scene.control.Label?>
-<?import javafx.scene.control.ButtonBar?>
-<?import javafx.scene.control.Button?>
-<?import javafx.scene.control.TextArea?>
-<?import javafx.geometry.Insets?>
-<?import javafx.scene.control.TitledPane?>
-<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
-<?import javafx.scene.layout.Region?>
-<?import javafx.scene.layout.HBox?>
-<?import javafx.scene.text.TextFlow?>
-<?import javafx.scene.text.Text?>
-<VBox xmlns="http://javafx.com/javafx"
-	  xmlns:fx="http://javafx.com/fxml"
-	  fx:controller="org.cryptomator.ui.health.StartFailController"
-	  prefWidth="600"
-	  prefHeight="400"
-	  spacing="12">
-	<padding>
-		<Insets topRightBottomLeft="12"/>
-	</padding>
-	<Label text="%health.fail.header" styleClass="label-large" />
-	<TextFlow fx:id="ioErrorLabel" visible="${controller.ioException}" managed="${controller.ioException}">
-		<Text text="%health.fail.ioError" />
-		<Text text="${controller.localizedErrorMessage}"/>
-	</TextFlow>
-	<Label fx:id="parseErrorLabel" text="%health.fail.parseError" visible="${controller.parseException}" managed="${controller.parseException}"/>
-	<TitledPane fx:id="moreInfoPane" text="%health.fail.moreInfo" expanded="false">
-		<graphic>
-			<HBox alignment="CENTER" minWidth="8">
-				<FontAwesome5IconView glyph="${controller.moreInfoIcon}"/>
-			</HBox>
-		</graphic>
-		<content>
-			<TextArea VBox.vgrow="ALWAYS" text="${controller.stackTrace}" prefRowCount="20" editable="false" />
-		</content>
-	</TitledPane>
-	<Region VBox.vgrow="ALWAYS"/>
-	<ButtonBar buttonMinWidth="120" buttonOrder="+CX">
-		<buttons>
-			<Button text="%generic.button.close" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#close"/>
-		</buttons>
-	</ButtonBar>
-</VBox>