providers) {
return Bindings.createObjectBinding(() -> {
- var selectedProviderClass = settings.keychainBackend().get().getProviderClass();
+ var selectedProviderClass = settings.keychainProvider().get();
var selectedProvider = providers.stream().filter(provider -> provider.getClass().getName().equals(selectedProviderClass)).findAny();
var fallbackProvider = providers.stream().findAny().orElse(null);
return selectedProvider.orElse(fallbackProvider);
- }, settings.keychainBackend());
+ }, settings.keychainProvider());
}
}
diff --git a/src/main/java/org/cryptomator/common/mountpoint/MountPointChooser.java b/src/main/java/org/cryptomator/common/mountpoint/MountPointChooser.java
index 3a4d82cf3..1e36ba5f6 100644
--- a/src/main/java/org/cryptomator/common/mountpoint/MountPointChooser.java
+++ b/src/main/java/org/cryptomator/common/mountpoint/MountPointChooser.java
@@ -1,6 +1,6 @@
package org.cryptomator.common.mountpoint;
-import com.google.common.base.Preconditions;
+import dagger.multibindings.IntKey;
import org.cryptomator.common.vaults.Volume;
import java.nio.file.Path;
@@ -12,13 +12,13 @@ import java.util.SortedSet;
* preparation of a mountpoint or an exception otherwise.
* All MountPointChoosers (MPCs) need to implement this class and must be added to
* the pool of possible MPCs by the {@link MountPointChooserModule MountPointChooserModule.}
- * The MountPointChooserModule will sort them according to their {@link #getPriority() priority.}
+ * The MountPointChooserModule will sort them according to their {@link IntKey IntKey priority.}
* The priority must be defined by the developer to reflect a useful execution order.
* A specific priority must not be assigned to more than one MPC at a time;
* the result of having two MPCs with equal priority is undefined.
*
- *
MPCs are executed by a {@link Volume} in ascending order of their priority
- * (smaller priorities are tried first) to find and prepare a suitable mountpoint for the volume.
+ *
MPCs are executed by a {@link Volume} in descending order of their priority
+ * (higher priorities are tried first) to find and prepare a suitable mountpoint for the volume.
* The volume has access to a {@link SortedSet} of MPCs in this specific order,
* that is provided by the Module. The Set contains all available Choosers, even if they
* are not {@link #isApplicable(Volume) applicable} for the Vault/Volume. The Volume must
diff --git a/src/main/java/org/cryptomator/common/mountpoint/MountPointChooserModule.java b/src/main/java/org/cryptomator/common/mountpoint/MountPointChooserModule.java
index cfb7b68ec..9c7893e42 100644
--- a/src/main/java/org/cryptomator/common/mountpoint/MountPointChooserModule.java
+++ b/src/main/java/org/cryptomator/common/mountpoint/MountPointChooserModule.java
@@ -9,6 +9,7 @@ import dagger.multibindings.IntoMap;
import org.cryptomator.common.vaults.PerVault;
import javax.inject.Named;
+import java.util.Comparator;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
@@ -24,16 +25,22 @@ public abstract class MountPointChooserModule {
@Binds
@IntoMap
- @IntKey(0)
+ @IntKey(1000)
@PerVault
public abstract MountPointChooser bindCustomMountPointChooser(CustomMountPointChooser chooser);
@Binds
@IntoMap
- @IntKey(100)
+ @IntKey(900)
@PerVault
public abstract MountPointChooser bindCustomDriveLetterChooser(CustomDriveLetterChooser chooser);
+ @Binds
+ @IntoMap
+ @IntKey(800)
+ @PerVault
+ public abstract MountPointChooser bindAvailableDriveLetterChooser(AvailableDriveLetterChooser chooser);
+
@Binds
@IntoMap
@IntKey(101)
@@ -42,13 +49,7 @@ public abstract class MountPointChooserModule {
@Binds
@IntoMap
- @IntKey(200)
- @PerVault
- public abstract MountPointChooser bindAvailableDriveLetterChooser(AvailableDriveLetterChooser chooser);
-
- @Binds
- @IntoMap
- @IntKey(999)
+ @IntKey(100)
@PerVault
public abstract MountPointChooser bindTemporaryMountPointChooser(TemporaryMountPointChooser chooser);
@@ -56,7 +57,8 @@ public abstract class MountPointChooserModule {
@PerVault
@Named("orderedMountPointChoosers")
public static Iterable provideOrderedMountPointChoosers(Map choosers) {
- SortedMap sortedChoosers = new TreeMap<>(choosers);
+ SortedMap sortedChoosers = new TreeMap<>(Comparator.reverseOrder());
+ sortedChoosers.putAll(choosers);
return Iterables.unmodifiableIterable(sortedChoosers.values());
}
}
diff --git a/src/main/java/org/cryptomator/common/settings/KeychainBackend.java b/src/main/java/org/cryptomator/common/settings/KeychainBackend.java
deleted file mode 100644
index 65f869a12..000000000
--- a/src/main/java/org/cryptomator/common/settings/KeychainBackend.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package org.cryptomator.common.settings;
-
-public enum KeychainBackend {
- GNOME("org.cryptomator.linux.keychain.SecretServiceKeychainAccess"),
- KDE("org.cryptomator.linux.keychain.KDEWalletKeychainAccess"),
- MAC_SYSTEM_KEYCHAIN("org.cryptomator.macos.keychain.MacSystemKeychainAccess"),
- WIN_SYSTEM_KEYCHAIN("org.cryptomator.windows.keychain.WindowsProtectedKeychainAccess");
-
- private final String providerClass;
-
- KeychainBackend(String providerClass) {
- this.providerClass = providerClass;
- }
-
- public String getProviderClass() {
- return providerClass;
- }
-
-}
diff --git a/src/main/java/org/cryptomator/common/settings/Settings.java b/src/main/java/org/cryptomator/common/settings/Settings.java
index a419d206a..e4cb9b8f7 100644
--- a/src/main/java/org/cryptomator/common/settings/Settings.java
+++ b/src/main/java/org/cryptomator/common/settings/Settings.java
@@ -30,7 +30,7 @@ public class Settings {
public static final int MIN_PORT = 1024;
public static final int MAX_PORT = 65535;
public static final boolean DEFAULT_ASKED_FOR_UPDATE_CHECK = false;
- public static final boolean DEFAULT_CHECK_FOR_UDPATES = false;
+ public static final boolean DEFAULT_CHECK_FOR_UPDATES = false;
public static final boolean DEFAULT_START_HIDDEN = false;
public static final int DEFAULT_PORT = 42427;
public static final int DEFAULT_NUM_TRAY_NOTIFICATIONS = 3;
@@ -38,14 +38,15 @@ public class Settings {
public static final boolean DEFAULT_DEBUG_MODE = false;
public static final VolumeImpl DEFAULT_PREFERRED_VOLUME_IMPL = SystemUtils.IS_OS_WINDOWS ? VolumeImpl.DOKANY : VolumeImpl.FUSE;
public static final UiTheme DEFAULT_THEME = UiTheme.LIGHT;
- public static final KeychainBackend DEFAULT_KEYCHAIN_BACKEND = SystemUtils.IS_OS_WINDOWS ? KeychainBackend.WIN_SYSTEM_KEYCHAIN : SystemUtils.IS_OS_MAC ? KeychainBackend.MAC_SYSTEM_KEYCHAIN : KeychainBackend.GNOME;
+ @Deprecated // to be changed to "whatever is available" eventually
+ public static final String DEFAULT_KEYCHAIN_PROVIDER = SystemUtils.IS_OS_WINDOWS ? "org.cryptomator.windows.keychain.WindowsProtectedKeychainAccess" : SystemUtils.IS_OS_MAC ? "org.cryptomator.macos.keychain.MacSystemKeychainAccess" : "org.cryptomator.linux.keychain.SecretServiceKeychainAccess";
public static final NodeOrientation DEFAULT_USER_INTERFACE_ORIENTATION = NodeOrientation.LEFT_TO_RIGHT;
public static final String DEFAULT_LICENSE_KEY = "";
public static final boolean DEFAULT_SHOW_MINIMIZE_BUTTON = false;
private final ObservableList directories = FXCollections.observableArrayList(VaultSettings::observables);
private final BooleanProperty askedForUpdateCheck = new SimpleBooleanProperty(DEFAULT_ASKED_FOR_UPDATE_CHECK);
- private final BooleanProperty checkForUpdates = new SimpleBooleanProperty(DEFAULT_CHECK_FOR_UDPATES);
+ private final BooleanProperty checkForUpdates = new SimpleBooleanProperty(DEFAULT_CHECK_FOR_UPDATES);
private final BooleanProperty startHidden = new SimpleBooleanProperty(DEFAULT_START_HIDDEN);
private final IntegerProperty port = new SimpleIntegerProperty(DEFAULT_PORT);
private final IntegerProperty numTrayNotifications = new SimpleIntegerProperty(DEFAULT_NUM_TRAY_NOTIFICATIONS);
@@ -53,7 +54,7 @@ public class Settings {
private final BooleanProperty debugMode = new SimpleBooleanProperty(DEFAULT_DEBUG_MODE);
private final ObjectProperty preferredVolumeImpl = new SimpleObjectProperty<>(DEFAULT_PREFERRED_VOLUME_IMPL);
private final ObjectProperty theme = new SimpleObjectProperty<>(DEFAULT_THEME);
- private final ObjectProperty keychainBackend = new SimpleObjectProperty<>(DEFAULT_KEYCHAIN_BACKEND);
+ private final ObjectProperty keychainProvider = new SimpleObjectProperty<>(DEFAULT_KEYCHAIN_PROVIDER);
private final ObjectProperty userInterfaceOrientation = new SimpleObjectProperty<>(DEFAULT_USER_INTERFACE_ORIENTATION);
private final StringProperty licenseKey = new SimpleStringProperty(DEFAULT_LICENSE_KEY);
private final BooleanProperty showMinimizeButton = new SimpleBooleanProperty(DEFAULT_SHOW_MINIMIZE_BUTTON);
@@ -77,7 +78,7 @@ public class Settings {
debugMode.addListener(this::somethingChanged);
preferredVolumeImpl.addListener(this::somethingChanged);
theme.addListener(this::somethingChanged);
- keychainBackend.addListener(this::somethingChanged);
+ keychainProvider.addListener(this::somethingChanged);
userInterfaceOrientation.addListener(this::somethingChanged);
licenseKey.addListener(this::somethingChanged);
showMinimizeButton.addListener(this::somethingChanged);
@@ -140,7 +141,7 @@ public class Settings {
return theme;
}
- public ObjectProperty keychainBackend() { return keychainBackend; }
+ public ObjectProperty keychainProvider() { return keychainProvider; }
public ObjectProperty userInterfaceOrientation() {
return userInterfaceOrientation;
diff --git a/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java b/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java
index d22e0867f..5bcb5f3d7 100644
--- a/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java
+++ b/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java
@@ -48,7 +48,7 @@ public class SettingsJsonAdapter extends TypeAdapter {
out.name("preferredVolumeImpl").value(value.preferredVolumeImpl().get().name());
out.name("theme").value(value.theme().get().name());
out.name("uiOrientation").value(value.userInterfaceOrientation().get().name());
- out.name("keychainBackend").value(value.keychainBackend().get().name());
+ out.name("keychainProvider").value(value.keychainProvider().get());
out.name("licenseKey").value(value.licenseKey().get());
out.name("showMinimizeButton").value(value.showMinimizeButton().get());
out.name("showTrayIcon").value(value.showTrayIcon().get());
@@ -82,7 +82,7 @@ public class SettingsJsonAdapter extends TypeAdapter {
case "preferredVolumeImpl" -> settings.preferredVolumeImpl().set(parsePreferredVolumeImplName(in.nextString()));
case "theme" -> settings.theme().set(parseUiTheme(in.nextString()));
case "uiOrientation" -> settings.userInterfaceOrientation().set(parseUiOrientation(in.nextString()));
- case "keychainBackend" -> settings.keychainBackend().set(parseKeychainBackend(in.nextString()));
+ case "keychainProvider" -> settings.keychainProvider().set(in.nextString());
case "licenseKey" -> settings.licenseKey().set(in.nextString());
case "showMinimizeButton" -> settings.showMinimizeButton().set(in.nextBoolean());
case "showTrayIcon" -> settings.showTrayIcon().set(in.nextBoolean());
@@ -124,15 +124,6 @@ public class SettingsJsonAdapter extends TypeAdapter {
}
}
- private KeychainBackend parseKeychainBackend(String backendName) {
- try {
- return KeychainBackend.valueOf(backendName.toUpperCase());
- } catch (IllegalArgumentException e) {
- LOG.warn("Invalid keychain backend {}. Defaulting to {}.", backendName, Settings.DEFAULT_KEYCHAIN_BACKEND);
- return Settings.DEFAULT_KEYCHAIN_BACKEND;
- }
- }
-
private NodeOrientation parseUiOrientation(String uiOrientationName) {
try {
return NodeOrientation.valueOf(uiOrientationName.toUpperCase());
diff --git a/src/main/java/org/cryptomator/common/settings/SettingsProvider.java b/src/main/java/org/cryptomator/common/settings/SettingsProvider.java
index 8310d0fa9..3be42f7ac 100644
--- a/src/main/java/org/cryptomator/common/settings/SettingsProvider.java
+++ b/src/main/java/org/cryptomator/common/settings/SettingsProvider.java
@@ -101,7 +101,7 @@ public class SettingsProvider implements Supplier {
if (settings == null) {
return;
}
- final Optional settingsPath = env.getSettingsPath().findFirst(); // alway save to preferred (first) path
+ final Optional settingsPath = env.getSettingsPath().findFirst(); // always save to preferred (first) path
settingsPath.ifPresent(path -> {
Runnable saveCommand = () -> this.save(settings, path);
ScheduledFuture> scheduledTask = scheduler.schedule(saveCommand, SAVE_DELAY_MS, TimeUnit.MILLISECONDS);
diff --git a/src/main/java/org/cryptomator/common/settings/VaultSettings.java b/src/main/java/org/cryptomator/common/settings/VaultSettings.java
index 10a023806..8ae20406c 100644
--- a/src/main/java/org/cryptomator/common/settings/VaultSettings.java
+++ b/src/main/java/org/cryptomator/common/settings/VaultSettings.java
@@ -31,7 +31,7 @@ import java.util.Random;
public class VaultSettings {
public static final boolean DEFAULT_UNLOCK_AFTER_STARTUP = false;
- public static final boolean DEFAULT_REAVEAL_AFTER_MOUNT = true;
+ public static final boolean DEFAULT_REVEAL_AFTER_MOUNT = true;
public static final boolean DEFAULT_USES_INDIVIDUAL_MOUNTPATH = false;
public static final boolean DEFAULT_USES_READONLY_MODE = false;
public static final String DEFAULT_MOUNT_FLAGS = "";
@@ -43,11 +43,11 @@ public class VaultSettings {
private static final Random RNG = new Random();
private final String id;
- private final ObjectProperty path = new SimpleObjectProperty();
+ private final ObjectProperty path = new SimpleObjectProperty<>();
private final StringProperty displayName = new SimpleStringProperty();
private final StringProperty winDriveLetter = new SimpleStringProperty();
private final BooleanProperty unlockAfterStartup = new SimpleBooleanProperty(DEFAULT_UNLOCK_AFTER_STARTUP);
- private final BooleanProperty revealAfterMount = new SimpleBooleanProperty(DEFAULT_REAVEAL_AFTER_MOUNT);
+ private final BooleanProperty revealAfterMount = new SimpleBooleanProperty(DEFAULT_REVEAL_AFTER_MOUNT);
private final BooleanProperty useCustomMountPath = new SimpleBooleanProperty(DEFAULT_USES_INDIVIDUAL_MOUNTPATH);
private final StringProperty customMountPath = new SimpleStringProperty();
private final BooleanProperty usesReadOnlyMode = new SimpleBooleanProperty(DEFAULT_USES_READONLY_MODE);
diff --git a/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java b/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java
index 0cf92a48b..15a081f0d 100644
--- a/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java
+++ b/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java
@@ -44,7 +44,7 @@ class VaultSettingsJsonAdapter {
String customMountPath = null;
String winDriveLetter = null;
boolean unlockAfterStartup = VaultSettings.DEFAULT_UNLOCK_AFTER_STARTUP;
- boolean revealAfterMount = VaultSettings.DEFAULT_REAVEAL_AFTER_MOUNT;
+ boolean revealAfterMount = VaultSettings.DEFAULT_REVEAL_AFTER_MOUNT;
boolean useCustomMountPath = VaultSettings.DEFAULT_USES_INDIVIDUAL_MOUNTPATH;
boolean usesReadOnlyMode = VaultSettings.DEFAULT_USES_READONLY_MODE;
String mountFlags = VaultSettings.DEFAULT_MOUNT_FLAGS;
diff --git a/src/main/java/org/cryptomator/common/vaults/Vault.java b/src/main/java/org/cryptomator/common/vaults/Vault.java
index 02fd03600..74ac7dc40 100644
--- a/src/main/java/org/cryptomator/common/vaults/Vault.java
+++ b/src/main/java/org/cryptomator/common/vaults/Vault.java
@@ -19,6 +19,7 @@ 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;
@@ -327,6 +328,14 @@ 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);
diff --git a/src/main/java/org/cryptomator/common/vaults/VaultComponent.java b/src/main/java/org/cryptomator/common/vaults/VaultComponent.java
index 47be62520..588ff64cd 100644
--- a/src/main/java/org/cryptomator/common/vaults/VaultComponent.java
+++ b/src/main/java/org/cryptomator/common/vaults/VaultComponent.java
@@ -7,10 +7,10 @@ package org.cryptomator.common.vaults;
import dagger.BindsInstance;
import dagger.Subcomponent;
+import org.cryptomator.common.Nullable;
import org.cryptomator.common.mountpoint.MountPointChooserModule;
import org.cryptomator.common.settings.VaultSettings;
-import javax.annotation.Nullable;
import javax.inject.Named;
@PerVault
diff --git a/src/main/java/org/cryptomator/common/vaults/VaultModule.java b/src/main/java/org/cryptomator/common/vaults/VaultModule.java
index 901ee7f42..cc38e6933 100644
--- a/src/main/java/org/cryptomator/common/vaults/VaultModule.java
+++ b/src/main/java/org/cryptomator/common/vaults/VaultModule.java
@@ -8,6 +8,7 @@ package org.cryptomator.common.vaults;
import dagger.Module;
import dagger.Provides;
import org.apache.commons.lang3.SystemUtils;
+import org.cryptomator.common.Nullable;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.common.settings.VolumeImpl;
@@ -15,7 +16,6 @@ import org.cryptomator.cryptofs.CryptoFileSystem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import javax.annotation.Nullable;
import javax.inject.Named;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.StringBinding;
@@ -138,7 +138,7 @@ public class VaultModule {
// see https://github.com/billziss-gh/winfsp/blob/5d0b10d0b643652c00ebb4704dc2bb28e7244973/src/dll/fuse/fuse_main.c#L53-L62 for syntax guide
// see https://github.com/billziss-gh/winfsp/blob/5d0b10d0b643652c00ebb4704dc2bb28e7244973/src/dll/fuse/fuse.c#L295-L319 for options (-o <...>)
- // see https://github.com/billziss-gh/winfsp/wiki/Frequently-Asked-Questions/5ba00e4be4f5e938eaae6ef1500b331de12dee77 (FUSE 4.) on why the given defaults were choosen
+ // see https://github.com/billziss-gh/winfsp/wiki/Frequently-Asked-Questions/5ba00e4be4f5e938eaae6ef1500b331de12dee77 (FUSE 4.) on why the given defaults were chosen
private String getWindowsFuseDefaultMountFlags(StringBinding mountName, ReadOnlyBooleanProperty readOnly) {
assert SystemUtils.IS_OS_WINDOWS;
StringBuilder flags = new StringBuilder();
diff --git a/src/main/java/org/cryptomator/common/vaults/VaultState.java b/src/main/java/org/cryptomator/common/vaults/VaultState.java
index 801ea7653..51365fbd2 100644
--- a/src/main/java/org/cryptomator/common/vaults/VaultState.java
+++ b/src/main/java/org/cryptomator/common/vaults/VaultState.java
@@ -46,7 +46,7 @@ public class VaultState extends ObservableValueBase implements
UNLOCKED,
/**
- * Unknown state due to preceeding unrecoverable exceptions.
+ * Unknown state due to preceding unrecoverable exceptions.
*/
ERROR;
}
diff --git a/src/main/java/org/cryptomator/common/vaults/VaultStats.java b/src/main/java/org/cryptomator/common/vaults/VaultStats.java
index 649be3a09..ac0b8df38 100644
--- a/src/main/java/org/cryptomator/common/vaults/VaultStats.java
+++ b/src/main/java/org/cryptomator/common/vaults/VaultStats.java
@@ -35,8 +35,8 @@ public class VaultStats {
private final LongProperty bytesPerSecondEncrypted = new SimpleLongProperty();
private final LongProperty bytesPerSecondDecrypted = new SimpleLongProperty();
private final DoubleProperty cacheHitRate = new SimpleDoubleProperty();
- private final LongProperty toalBytesRead = new SimpleLongProperty();
- private final LongProperty toalBytesWritten = new SimpleLongProperty();
+ private final LongProperty totalBytesRead = new SimpleLongProperty();
+ private final LongProperty totalBytesWritten = new SimpleLongProperty();
private final LongProperty totalBytesEncrypted = new SimpleLongProperty();
private final LongProperty totalBytesDecrypted = new SimpleLongProperty();
private final LongProperty filesRead = new SimpleLongProperty();
@@ -75,8 +75,8 @@ public class VaultStats {
cacheHitRate.set(stats.map(this::getCacheHitRate).orElse(0.0));
bytesPerSecondDecrypted.set(stats.map(CryptoFileSystemStats::pollBytesDecrypted).orElse(0L));
bytesPerSecondEncrypted.set(stats.map(CryptoFileSystemStats::pollBytesEncrypted).orElse(0L));
- toalBytesRead.set(stats.map(CryptoFileSystemStats::pollTotalBytesRead).orElse(0L));
- toalBytesWritten.set(stats.map(CryptoFileSystemStats::pollTotalBytesWritten).orElse(0L));
+ totalBytesRead.set(stats.map(CryptoFileSystemStats::pollTotalBytesRead).orElse(0L));
+ totalBytesWritten.set(stats.map(CryptoFileSystemStats::pollTotalBytesWritten).orElse(0L));
totalBytesEncrypted.set(stats.map(CryptoFileSystemStats::pollTotalBytesEncrypted).orElse(0L));
totalBytesDecrypted.set(stats.map(CryptoFileSystemStats::pollTotalBytesDecrypted).orElse(0L));
var oldAccessCount = filesRead.get() + filesWritten.get();
@@ -146,7 +146,7 @@ public class VaultStats {
return bytesPerSecondEncrypted;
}
- public long getBytesPerSecondEnrypted() {
+ public long getBytesPerSecondEncrypted() {
return bytesPerSecondEncrypted.get();
}
@@ -164,13 +164,13 @@ public class VaultStats {
return cacheHitRate.get();
}
- public LongProperty toalBytesReadProperty() {return toalBytesRead;}
+ public LongProperty totalBytesReadProperty() {return totalBytesRead;}
- public long getTotalBytesRead() { return toalBytesRead.get();}
+ public long getTotalBytesRead() { return totalBytesRead.get();}
- public LongProperty toalBytesWrittenProperty() {return toalBytesWritten;}
+ public LongProperty totalBytesWrittenProperty() {return totalBytesWritten;}
- public long getTotalBytesWritten() { return toalBytesWritten.get();}
+ public long getTotalBytesWritten() { return totalBytesWritten.get();}
public LongProperty totalBytesEncryptedProperty() {return totalBytesEncrypted;}
diff --git a/src/main/java/org/cryptomator/common/vaults/Volume.java b/src/main/java/org/cryptomator/common/vaults/Volume.java
index f608122bf..5f434fa43 100644
--- a/src/main/java/org/cryptomator/common/vaults/Volume.java
+++ b/src/main/java/org/cryptomator/common/vaults/Volume.java
@@ -12,7 +12,7 @@ import java.util.function.Consumer;
import java.util.stream.Stream;
/**
- * Takes a Volume and usess it to mount an unlocked vault
+ * Takes a Volume and uses it to mount an unlocked vault
*/
public interface Volume {
@@ -24,7 +24,7 @@ public interface Volume {
boolean isSupported();
/**
- * Gets the coresponding enum type of the {@link VolumeImpl volume implementation ("VolumeImpl")} that is implemented by this Volume.
+ * Gets the corresponding enum type of the {@link VolumeImpl volume implementation ("VolumeImpl")} that is implemented by this Volume.
*
* @return the type of implementation as defined by the {@link VolumeImpl VolumeImpl enum}
*/
diff --git a/src/main/java/org/cryptomator/common/vaults/WebDavVolume.java b/src/main/java/org/cryptomator/common/vaults/WebDavVolume.java
index 03c83377c..b1850bc1e 100644
--- a/src/main/java/org/cryptomator/common/vaults/WebDavVolume.java
+++ b/src/main/java/org/cryptomator/common/vaults/WebDavVolume.java
@@ -67,7 +67,7 @@ public class WebDavVolume implements Volume {
throw new IllegalStateException("Mounting requires unlocked WebDAV servlet.");
}
- //on windows, prevent an automatic drive letter selection in the upstream library. Either we choose already a specifc one or there is no free.
+ //on windows, prevent an automatic drive letter selection in the upstream library. Either we choose already a specific one or there is no free.
Supplier driveLetterSupplier;
if (System.getProperty("os.name").toLowerCase().contains("windows") && vaultSettings.winDriveLetter().isEmpty().get()) {
driveLetterSupplier = () -> windowsDriveLetters.getAvailableDriveLetter().orElse(null);
diff --git a/src/main/java/org/cryptomator/ipc/Client.java b/src/main/java/org/cryptomator/ipc/Client.java
new file mode 100644
index 000000000..fcd084032
--- /dev/null
+++ b/src/main/java/org/cryptomator/ipc/Client.java
@@ -0,0 +1,65 @@
+package org.cryptomator.ipc;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.net.UnixDomainSocketAddress;
+import java.nio.channels.SocketChannel;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.concurrent.Executor;
+
+class Client implements IpcCommunicator {
+
+ private static final Logger LOG = LoggerFactory.getLogger(Client.class);
+
+ private final SocketChannel socketChannel;
+
+ private Client(SocketChannel socketChannel) {
+ this.socketChannel = socketChannel;
+ }
+
+ public static Client create(Path socketPath) throws IOException {
+ var address = UnixDomainSocketAddress.of(socketPath);
+ var socketChannel = SocketChannel.open(address);
+ LOG.info("Connected to IPC server on socket {}", socketPath);
+ return new Client(socketChannel);
+ }
+
+ @Override
+ public boolean isClient() {
+ return true;
+ }
+
+ @Override
+ public void listen(IpcMessageListener listener, Executor executor) {
+ executor.execute(() -> {
+ try {
+ while (socketChannel.isConnected()) {
+ var msg = IpcMessage.receive(socketChannel);
+ listener.handleMessage(msg);
+ }
+ } catch (IOException e) {
+ LOG.error("Failed to read IPC message", e);
+ }
+ });
+ }
+
+ @Override
+ public void send(IpcMessage message, Executor executor) {
+ executor.execute(() -> {
+ try {
+ message.send(socketChannel);
+ } catch (IOException e) {
+ LOG.error("Failed to send IPC message", e);
+ }
+ });
+ }
+
+ @Override
+ public void close() throws IOException {
+ socketChannel.close();
+ }
+}
diff --git a/src/main/java/org/cryptomator/ipc/HandleLaunchArgsMessage.java b/src/main/java/org/cryptomator/ipc/HandleLaunchArgsMessage.java
new file mode 100644
index 000000000..0a7916c16
--- /dev/null
+++ b/src/main/java/org/cryptomator/ipc/HandleLaunchArgsMessage.java
@@ -0,0 +1,30 @@
+package org.cryptomator.ipc;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Splitter;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+record HandleLaunchArgsMessage(List args) implements IpcMessage {
+
+ private static final char DELIMITER = '\n';
+
+ public static HandleLaunchArgsMessage decode(ByteBuffer encoded) {
+ var str = StandardCharsets.UTF_8.decode(encoded).toString();
+ var args = Splitter.on(DELIMITER).omitEmptyStrings().splitToList(str);
+ return new HandleLaunchArgsMessage(args);
+ }
+
+ @Override
+ public MessageType getMessageType() {
+ return MessageType.HANDLE_LAUNCH_ARGS;
+ }
+
+ @Override
+ public ByteBuffer encodePayload() {
+ var str = Joiner.on(DELIMITER).join(args);
+ return StandardCharsets.UTF_8.encode(str);
+ }
+}
diff --git a/src/main/java/org/cryptomator/ipc/IpcCommunicator.java b/src/main/java/org/cryptomator/ipc/IpcCommunicator.java
new file mode 100644
index 000000000..0120389c9
--- /dev/null
+++ b/src/main/java/org/cryptomator/ipc/IpcCommunicator.java
@@ -0,0 +1,96 @@
+package org.cryptomator.ipc;
+
+import com.google.common.base.Preconditions;
+import com.google.common.util.concurrent.MoreExecutors;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Future;
+
+public interface IpcCommunicator extends Closeable {
+
+ Logger LOG = LoggerFactory.getLogger(IpcCommunicator.class);
+
+ /**
+ * Attempts to establish a socket connection via one of the given paths.
+ *
+ * If no connection to an existing sockets can be established, a new socket is created for the first given path.
+ *
+ * If this fails as well, a fallback communicator is returned that allows process-internal communication mocking the API
+ * that would have been used for IPC.
+ *
+ * @param socketPaths The socket path(s)
+ * @return A communicator object that allows sending and receiving messages
+ */
+ static IpcCommunicator create(Iterable socketPaths) {
+ Preconditions.checkArgument(socketPaths.iterator().hasNext(), "socketPaths must contain at least one element");
+ for (var p : socketPaths) {
+ try {
+ var attr = Files.readAttributes(p, BasicFileAttributes.class);
+ if (attr.isOther()) {
+ return Client.create(p);
+ }
+ } catch (IOException e) {
+ // attempt next socket path
+ }
+ }
+ // Didn't get any connection yet? I.e. we're the first app instance, so let's launch a server:
+ try {
+ return Server.create(socketPaths.iterator().next());
+ } catch (IOException e) {
+ LOG.warn("Failed to create IPC server", e);
+ return new LoopbackCommunicator();
+ }
+ }
+
+ boolean isClient();
+
+ /**
+ * Listens to incoming messages until the connection gets closed.
+ * @param listener The listener that should be notified of incoming messages
+ * @param executor An executor on which to listen. Listening will block, so you might want to use a background thread.
+ * @return
+ */
+ void listen(IpcMessageListener listener, Executor executor);
+
+ /**
+ * Sends the given message.
+ *
+ * @param message The message to send
+ * @param executor An executor used to send the message. Sending will block, so you might want to use a background thread.
+ */
+ void send(IpcMessage message, Executor executor);
+
+ default void sendRevealRunningApp() {
+ send(new RevealRunningAppMessage(), MoreExecutors.directExecutor());
+ }
+
+ default void sendHandleLaunchargs(List args) {
+ send(new HandleLaunchArgsMessage(args), MoreExecutors.directExecutor());
+ }
+
+ /**
+ * Clean up resources.
+ *
+ * @implSpec Must be idempotent
+ * @throws IOException In case of I/O errors.
+ */
+ @Override
+ void close() throws IOException;
+
+ default void closeUnchecked() throws UncheckedIOException {
+ try {
+ close();
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+}
diff --git a/src/main/java/org/cryptomator/ipc/IpcMessage.java b/src/main/java/org/cryptomator/ipc/IpcMessage.java
new file mode 100644
index 000000000..9d1c8d3de
--- /dev/null
+++ b/src/main/java/org/cryptomator/ipc/IpcMessage.java
@@ -0,0 +1,68 @@
+package org.cryptomator.ipc;
+
+import org.cryptomator.cryptolib.common.ByteBuffers;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.WritableByteChannel;
+import java.util.function.Function;
+
+// TODO make sealed, remove enum
+interface IpcMessage {
+
+ enum MessageType {
+ REVEAL_RUNNING_APP(RevealRunningAppMessage::decode),
+ HANDLE_LAUNCH_ARGS(HandleLaunchArgsMessage::decode);
+
+ private final Function decoder;
+
+ MessageType(Function decoder) {
+ this.decoder = decoder;
+ }
+
+ static MessageType forOrdinal(int ordinal) {
+ try {
+ return values()[ordinal];
+ } catch (IndexOutOfBoundsException e) {
+ throw new IllegalArgumentException("No such message type: " + ordinal, e);
+ }
+ }
+
+ IpcMessage decodePayload(ByteBuffer payload) {
+ return decoder.apply(payload);
+ }
+ }
+
+ MessageType getMessageType();
+
+ ByteBuffer encodePayload();
+
+ static IpcMessage receive(ReadableByteChannel channel) throws IOException {
+ var header = ByteBuffer.allocate(2 * Integer.BYTES);
+ if (ByteBuffers.fill(channel, header) < header.capacity()) {
+ throw new EOFException();
+ }
+ header.flip();
+ int typeNo = header.getInt();
+ int length = header.getInt();
+ MessageType type = MessageType.forOrdinal(typeNo);
+ var payload = ByteBuffer.allocate(length);
+ ByteBuffers.fill(channel, payload);
+ payload.flip();
+ return type.decodePayload(payload);
+ }
+
+ default void send(WritableByteChannel channel) throws IOException {
+ var payload = encodePayload();
+ var buf = ByteBuffer.allocate(2 * Integer.BYTES + payload.remaining());
+ buf.putInt(getMessageType().ordinal()); // message type
+ buf.putInt(payload.remaining()); // message length
+ buf.put(payload); // message
+ buf.flip();
+ while (buf.hasRemaining()) {
+ channel.write(buf);
+ }
+ }
+}
diff --git a/src/main/java/org/cryptomator/ipc/IpcMessageListener.java b/src/main/java/org/cryptomator/ipc/IpcMessageListener.java
new file mode 100644
index 000000000..f49275824
--- /dev/null
+++ b/src/main/java/org/cryptomator/ipc/IpcMessageListener.java
@@ -0,0 +1,19 @@
+package org.cryptomator.ipc;
+
+import java.util.List;
+
+public interface IpcMessageListener {
+
+ default void handleMessage(IpcMessage message) {
+ if (message instanceof RevealRunningAppMessage) {
+ revealRunningApp();
+ } else if (message instanceof HandleLaunchArgsMessage m) {
+ handleLaunchArgs(m.args());
+ }
+ }
+
+ void revealRunningApp();
+
+ void handleLaunchArgs(List args);
+
+}
diff --git a/src/main/java/org/cryptomator/ipc/LoopbackCommunicator.java b/src/main/java/org/cryptomator/ipc/LoopbackCommunicator.java
new file mode 100644
index 000000000..ba5152c93
--- /dev/null
+++ b/src/main/java/org/cryptomator/ipc/LoopbackCommunicator.java
@@ -0,0 +1,50 @@
+package org.cryptomator.ipc;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.LinkedTransferQueue;
+import java.util.concurrent.TransferQueue;
+
+class LoopbackCommunicator implements IpcCommunicator {
+
+ private static final Logger LOG = LoggerFactory.getLogger(LoopbackCommunicator.class);
+
+ private final TransferQueue transferQueue = new LinkedTransferQueue<>();
+
+ @Override
+ public boolean isClient() {
+ return false;
+ }
+
+ @Override
+ public void listen(IpcMessageListener listener, Executor executor) {
+ executor.execute(() -> {
+ try {
+ var msg = transferQueue.take();
+ listener.handleMessage(msg);
+ } catch (InterruptedException e) {
+ LOG.error("Failed to read IPC message", e);
+ Thread.currentThread().interrupt();
+ }
+ });
+ }
+
+ @Override
+ public void send(IpcMessage message, Executor executor) {
+ executor.execute(() -> {
+ try {
+ transferQueue.put(message);
+ } catch (InterruptedException e) {
+ LOG.error("Failed to send IPC message", e);
+ Thread.currentThread().interrupt();
+ }
+ });
+ }
+
+ @Override
+ public void close() {
+ // no-op
+ }
+}
diff --git a/src/main/java/org/cryptomator/ipc/RevealRunningAppMessage.java b/src/main/java/org/cryptomator/ipc/RevealRunningAppMessage.java
new file mode 100644
index 000000000..fa4d1375b
--- /dev/null
+++ b/src/main/java/org/cryptomator/ipc/RevealRunningAppMessage.java
@@ -0,0 +1,20 @@
+package org.cryptomator.ipc;
+
+import java.nio.ByteBuffer;
+
+public record RevealRunningAppMessage() implements IpcMessage {
+
+ static RevealRunningAppMessage decode(ByteBuffer ignored) {
+ return new RevealRunningAppMessage();
+ }
+
+ @Override
+ public MessageType getMessageType() {
+ return MessageType.REVEAL_RUNNING_APP;
+ }
+
+ @Override
+ public ByteBuffer encodePayload() {
+ return ByteBuffer.allocate(0);
+ }
+}
diff --git a/src/main/java/org/cryptomator/ipc/Server.java b/src/main/java/org/cryptomator/ipc/Server.java
new file mode 100644
index 000000000..e9a82c328
--- /dev/null
+++ b/src/main/java/org/cryptomator/ipc/Server.java
@@ -0,0 +1,82 @@
+package org.cryptomator.ipc;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.net.StandardProtocolFamily;
+import java.net.UnixDomainSocketAddress;
+import java.nio.channels.AsynchronousCloseException;
+import java.nio.channels.ClosedChannelException;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.Executor;
+
+class Server implements IpcCommunicator {
+
+ private static final Logger LOG = LoggerFactory.getLogger(Server.class);
+
+ private final ServerSocketChannel serverSocketChannel;
+ private final Path socketPath;
+
+ private Server(ServerSocketChannel serverSocketChannel, Path socketPath) {
+ this.serverSocketChannel = serverSocketChannel;
+ this.socketPath = socketPath;
+ }
+
+ public static Server create(Path socketPath) throws IOException {
+ var address = UnixDomainSocketAddress.of(socketPath);
+ var serverSocketChannel = ServerSocketChannel.open(StandardProtocolFamily.UNIX);
+ serverSocketChannel.bind(address);
+ LOG.info("Spawning IPC server listening on socket {}", socketPath);
+ return new Server(serverSocketChannel, socketPath);
+ }
+
+ @Override
+ public boolean isClient() {
+ return false;
+ }
+
+ @Override
+ public void listen(IpcMessageListener listener, Executor executor) {
+ executor.execute(() -> {
+ while (serverSocketChannel.isOpen()) {
+ try (var ch = serverSocketChannel.accept()) {
+ while (ch.isConnected()) {
+ var msg = IpcMessage.receive(ch);
+ listener.handleMessage(msg);
+ }
+ } catch (AsynchronousCloseException e) {
+ return; // serverSocketChannel closed or listener interrupted
+ } catch (EOFException | ClosedChannelException e) {
+ // continue with next connected client
+ } catch (IOException e) {
+ LOG.error("Failed to read IPC message", e);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void send(IpcMessage message, Executor executor) {
+ executor.execute(() -> {
+ try (var ch = serverSocketChannel.accept()) {
+ message.send(ch);
+ } catch (IOException e) {
+ LOG.error("Failed to send IPC message", e);
+ }
+ });
+ }
+
+ @Override
+ public void close() throws IOException {
+ try {
+ serverSocketChannel.close();
+ } finally {
+ Files.deleteIfExists(socketPath);
+ LOG.debug("IPC server closed");
+ }
+ }
+}
diff --git a/src/main/java/org/cryptomator/launcher/Cryptomator.java b/src/main/java/org/cryptomator/launcher/Cryptomator.java
index 04eb9448d..18a748fd8 100644
--- a/src/main/java/org/cryptomator/launcher/Cryptomator.java
+++ b/src/main/java/org/cryptomator/launcher/Cryptomator.java
@@ -5,7 +5,12 @@
*******************************************************************************/
package org.cryptomator.launcher;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import dagger.Lazy;
import org.apache.commons.lang3.SystemUtils;
+import org.cryptomator.common.Environment;
+import org.cryptomator.common.ShutdownHook;
+import org.cryptomator.ipc.IpcCommunicator;
import org.cryptomator.logging.DebugMode;
import org.cryptomator.logging.LoggerConfiguration;
import org.cryptomator.ui.launcher.UiLauncher;
@@ -16,8 +21,10 @@ import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.io.IOException;
+import java.util.List;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
@Singleton
public class Cryptomator {
@@ -29,23 +36,28 @@ public class Cryptomator {
private final LoggerConfiguration logConfig;
private final DebugMode debugMode;
- private final IpcFactory ipcFactory;
+ private final Environment env;
+ private final Lazy ipcMessageHandler;
private final Optional applicationVersion;
private final CountDownLatch shutdownLatch;
- private final UiLauncher uiLauncher;
+ private final ShutdownHook shutdownHook;
+ private final Lazy uiLauncher;
@Inject
- Cryptomator(LoggerConfiguration logConfig, DebugMode debugMode, IpcFactory ipcFactory, @Named("applicationVersion") Optional applicationVersion, @Named("shutdownLatch") CountDownLatch shutdownLatch, UiLauncher uiLauncher) {
+ Cryptomator(LoggerConfiguration logConfig, DebugMode debugMode, Environment env, Lazy ipcMessageHandler, @Named("applicationVersion") Optional applicationVersion, @Named("shutdownLatch") CountDownLatch shutdownLatch, ShutdownHook shutdownHook, Lazy uiLauncher) {
this.logConfig = logConfig;
this.debugMode = debugMode;
- this.ipcFactory = ipcFactory;
+ this.env = env;
+ this.ipcMessageHandler = ipcMessageHandler;
this.applicationVersion = applicationVersion;
this.shutdownLatch = shutdownLatch;
+ this.shutdownHook = shutdownHook;
this.uiLauncher = uiLauncher;
}
public static void main(String[] args) {
int exitCode = CRYPTOMATOR_COMPONENT.application().run(args);
+ LOG.info("Exit {}", exitCode);
System.exit(exitCode); // end remaining non-daemon threads.
}
@@ -64,19 +76,24 @@ public class Cryptomator {
* 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.
*/
- try (IpcFactory.IpcEndpoint endpoint = ipcFactory.create()) {
- endpoint.getRemote().handleLaunchArgs(args); // if we are the server, getRemote() returns self.
- if (endpoint.isConnectedToRemote()) {
- endpoint.getRemote().revealRunningApp();
+ try (var communicator = IpcCommunicator.create(env.ipcSocketPath().toList())) {
+ if (communicator.isClient()) {
+ communicator.sendHandleLaunchargs(List.of(args));
+ communicator.sendRevealRunningApp();
LOG.info("Found running application instance. Shutting down...");
return 2;
} else {
+ shutdownHook.runOnShutdown(communicator::closeUnchecked);
+ var executor = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("IPC-%d").build());
+ var msgHandler = ipcMessageHandler.get();
+ msgHandler.handleLaunchArgs(List.of(args));
+ communicator.listen(msgHandler, executor);
LOG.debug("Did not find running application instance. Launching GUI...");
return runGuiApplication();
}
- } catch (IOException e) {
- LOG.error("Failed to initiate inter-process communication.", e);
- return runGuiApplication();
+ } catch (Throwable e) {
+ LOG.error("Running application failed", e);
+ return 1;
}
}
@@ -88,7 +105,7 @@ public class Cryptomator {
*/
private int runGuiApplication() {
try {
- uiLauncher.launch();
+ uiLauncher.get().launch();
shutdownLatch.await();
LOG.info("UI shut down");
return 0;
@@ -98,5 +115,4 @@ public class Cryptomator {
}
}
-
}
diff --git a/src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java b/src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java
index 9d1e3144d..b4e37e1f9 100644
--- a/src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java
+++ b/src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java
@@ -22,6 +22,7 @@ import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collection;
+import java.util.List;
import java.util.Objects;
import java.util.concurrent.BlockingQueue;
import java.util.stream.Collectors;
@@ -46,13 +47,13 @@ class FileOpenRequestHandler {
tryToEnqueueFileOpenRequest(launchEvent);
}
- public void handleLaunchArgs(String[] args) {
+ public void handleLaunchArgs(List args) {
handleLaunchArgs(FileSystems.getDefault(), args);
}
// visible for testing
- void handleLaunchArgs(FileSystem fs, String[] args) {
- Collection pathsToOpen = Arrays.stream(args).map(str -> {
+ void handleLaunchArgs(FileSystem fs, List args) {
+ Collection pathsToOpen = args.stream().map(str -> {
try {
return fs.getPath(str);
} catch (InvalidPathException e) {
diff --git a/src/main/java/org/cryptomator/launcher/IpcFactory.java b/src/main/java/org/cryptomator/launcher/IpcFactory.java
deleted file mode 100644
index 112050d26..000000000
--- a/src/main/java/org/cryptomator/launcher/IpcFactory.java
+++ /dev/null
@@ -1,258 +0,0 @@
-/*******************************************************************************
- * 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 com.google.common.io.MoreFiles;
-import org.cryptomator.common.Environment;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.inject.Inject;
-import javax.inject.Singleton;
-import java.io.Closeable;
-import java.io.IOException;
-import java.net.InetAddress;
-import java.net.ServerSocket;
-import java.net.Socket;
-import java.net.SocketException;
-import java.net.UnknownHostException;
-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.StandardOpenOption;
-import java.rmi.NotBoundException;
-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 java.util.List;
-import java.util.Optional;
-import java.util.stream.Collectors;
-
-/**
- * First running application on a machine opens a server socket. Further processes will connect as clients.
- */
-@Singleton
-class IpcFactory {
-
- private static final Logger LOG = LoggerFactory.getLogger(IpcFactory.class);
- private static final String RMI_NAME = "Cryptomator";
-
- private final List portFilePaths;
- private final IpcProtocolImpl ipcHandler;
-
- @Inject
- public IpcFactory(Environment env, IpcProtocolImpl ipcHandler) {
- this.portFilePaths = env.getIpcPortPath().collect(Collectors.toUnmodifiableList());
- this.ipcHandler = ipcHandler;
- }
-
- public IpcEndpoint create() {
- if (portFilePaths.isEmpty()) {
- LOG.warn("No IPC port file path specified.");
- return new SelfEndpoint(ipcHandler);
- } else {
- System.setProperty("java.rmi.server.hostname", "localhost");
- return attemptClientConnection().or(this::createServerEndpoint).orElseGet(() -> new SelfEndpoint(ipcHandler));
- }
- }
-
- private Optional attemptClientConnection() {
- for (Path portFilePath : portFilePaths) {
- try {
- int port = readPort(portFilePath);
- LOG.debug("[Client] Connecting to port {}...", port);
- Registry registry = LocateRegistry.getRegistry("localhost", port, new ClientSocketFactory());
- IpcProtocol remoteInterface = (IpcProtocol) registry.lookup(RMI_NAME);
- return Optional.of(new ClientEndpoint(remoteInterface));
- } catch (NotBoundException | IOException e) {
- LOG.debug("[Client] Failed to connect.");
- // continue with next portFilePath...
- }
- }
- return Optional.empty();
- }
-
- private int readPort(Path portFilePath) throws IOException {
- try (ReadableByteChannel ch = Files.newByteChannel(portFilePath, StandardOpenOption.READ)) {
- LOG.debug("[Client] Reading IPC port from {}", portFilePath);
- ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES);
- if (ch.read(buf) == Integer.BYTES) {
- buf.flip();
- return buf.getInt();
- } else {
- throw new IOException("Invalid IPC port file.");
- }
- }
- }
-
- private Optional createServerEndpoint() {
- assert !portFilePaths.isEmpty();
- Path portFilePath = portFilePaths.get(0);
- try {
- ServerSocket socket = new ServerSocket(0, Byte.MAX_VALUE, InetAddress.getByName("localhost"));
- RMIClientSocketFactory csf = RMISocketFactory.getDefaultSocketFactory();
- SingletonServerSocketFactory ssf = new SingletonServerSocketFactory(socket);
- Registry registry = LocateRegistry.createRegistry(0, csf, ssf);
- UnicastRemoteObject.exportObject(ipcHandler, 0);
- registry.rebind(RMI_NAME, ipcHandler);
- writePort(portFilePath, socket.getLocalPort());
- return Optional.of(new ServerEndpoint(ipcHandler, socket, registry, portFilePath));
- } catch (IOException e) {
- LOG.warn("[Server] Failed to create IPC server.", e);
- return Optional.empty();
- }
- }
-
- private void writePort(Path portFilePath, int port) throws IOException {
- ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES);
- buf.putInt(port);
- buf.flip();
- MoreFiles.createParentDirectories(portFilePath);
- try (WritableByteChannel ch = Files.newByteChannel(portFilePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
- if (ch.write(buf) != Integer.BYTES) {
- throw new IOException("Did not write expected number of bytes.");
- }
- }
- LOG.debug("[Server] Wrote IPC port {} to {}", port, portFilePath);
- }
-
- interface IpcEndpoint extends Closeable {
-
- boolean isConnectedToRemote();
-
- IpcProtocol getRemote();
-
- }
-
- static class SelfEndpoint implements IpcEndpoint {
-
- protected final IpcProtocol remoteObject;
-
- SelfEndpoint(IpcProtocol remoteObject) {
- this.remoteObject = remoteObject;
- }
-
- @Override
- public boolean isConnectedToRemote() {
- return false;
- }
-
- @Override
- public IpcProtocol getRemote() {
- return remoteObject;
- }
-
- @Override
- public void close() {
- // no-op
- }
- }
-
- static class ClientEndpoint implements IpcEndpoint {
-
- private final IpcProtocol remoteInterface;
-
- public ClientEndpoint(IpcProtocol remoteInterface) {
- this.remoteInterface = remoteInterface;
- }
-
- public IpcProtocol getRemote() {
- return remoteInterface;
- }
-
- @Override
- public boolean isConnectedToRemote() {
- return true;
- }
-
- @Override
- public void close() {
- // no-op
- }
-
- }
-
- class ServerEndpoint extends SelfEndpoint {
-
- private final ServerSocket socket;
- private final Registry registry;
- private final Path portFilePath;
-
- private ServerEndpoint(IpcProtocol remoteObject, ServerSocket socket, Registry registry, Path portFilePath) {
- super(remoteObject);
- this.socket = socket;
- this.registry = registry;
- this.portFilePath = portFilePath;
- }
-
- @Override
- public void close() {
- try {
- registry.unbind(RMI_NAME);
- UnicastRemoteObject.unexportObject(remoteObject, true);
- socket.close();
- Files.deleteIfExists(portFilePath);
- LOG.debug("[Server] Shut down");
- } catch (NotBoundException | IOException e) {
- LOG.warn("[Server] Error shutting down:", e);
- }
- }
-
- }
-
- /**
- * 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;
- }
-
- }
-
- /**
- * Creates client sockets with short timeouts.
- */
- private static class ClientSocketFactory implements RMIClientSocketFactory {
-
- @Override
- public Socket createSocket(String host, int port) throws IOException {
- return new SocketWithFixedTimeout(host, port, 1000);
- }
-
- }
-
- private static class SocketWithFixedTimeout extends Socket {
-
- public SocketWithFixedTimeout(String host, int port, int timeoutInMs) throws UnknownHostException, IOException {
- super(host, port);
- super.setSoTimeout(timeoutInMs);
- }
-
- @Override
- public synchronized void setSoTimeout(int timeout) throws SocketException {
- // do nothing, timeout is fixed
- }
-
- }
-
-}
diff --git a/src/main/java/org/cryptomator/launcher/IpcProtocolImpl.java b/src/main/java/org/cryptomator/launcher/IpcMessageHandler.java
similarity index 60%
rename from src/main/java/org/cryptomator/launcher/IpcProtocolImpl.java
rename to src/main/java/org/cryptomator/launcher/IpcMessageHandler.java
index 44f67e0cd..5c28d05a4 100644
--- a/src/main/java/org/cryptomator/launcher/IpcProtocolImpl.java
+++ b/src/main/java/org/cryptomator/launcher/IpcMessageHandler.java
@@ -1,5 +1,6 @@
package org.cryptomator.launcher;
+import org.cryptomator.ipc.IpcMessageListener;
import org.cryptomator.ui.launcher.AppLaunchEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -7,20 +8,20 @@ import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
-import java.util.Arrays;
import java.util.Collections;
+import java.util.List;
import java.util.concurrent.BlockingQueue;
@Singleton
-class IpcProtocolImpl implements IpcProtocol {
+class IpcMessageHandler implements IpcMessageListener {
- private static final Logger LOG = LoggerFactory.getLogger(IpcProtocolImpl.class);
+ private static final Logger LOG = LoggerFactory.getLogger(IpcMessageHandler.class);
private final FileOpenRequestHandler fileOpenRequestHandler;
private final BlockingQueue launchEventQueue;
@Inject
- public IpcProtocolImpl(FileOpenRequestHandler fileOpenRequestHandler, @Named("launchEventQueue") BlockingQueue launchEventQueue) {
+ public IpcMessageHandler(FileOpenRequestHandler fileOpenRequestHandler, @Named("launchEventQueue") BlockingQueue launchEventQueue) {
this.fileOpenRequestHandler = fileOpenRequestHandler;
this.launchEventQueue = launchEventQueue;
}
@@ -31,8 +32,8 @@ class IpcProtocolImpl implements IpcProtocol {
}
@Override
- public void handleLaunchArgs(String... args) {
- LOG.debug("Received launch args: {}", Arrays.stream(args).reduce((a, b) -> a + ", " + b).orElse(""));
+ public void handleLaunchArgs(List args) {
+ LOG.debug("Received launch args: {}", args.stream().reduce((a, b) -> a + ", " + b).orElse(""));
fileOpenRequestHandler.handleLaunchArgs(args);
}
diff --git a/src/main/java/org/cryptomator/launcher/IpcProtocol.java b/src/main/java/org/cryptomator/launcher/IpcProtocol.java
deleted file mode 100644
index 3e0596d77..000000000
--- a/src/main/java/org/cryptomator/launcher/IpcProtocol.java
+++ /dev/null
@@ -1,17 +0,0 @@
-/*******************************************************************************
- * 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.rmi.Remote;
-import java.rmi.RemoteException;
-
-interface IpcProtocol extends Remote {
-
- void revealRunningApp() throws RemoteException;
-
- void handleLaunchArgs(String... args) throws RemoteException;
-
-}
\ No newline at end of file
diff --git a/src/main/java/org/cryptomator/logging/LaunchAndSizeBasedTriggerinPolicy.java b/src/main/java/org/cryptomator/logging/LaunchAndSizeBasedTriggeringPolicy.java
similarity index 86%
rename from src/main/java/org/cryptomator/logging/LaunchAndSizeBasedTriggerinPolicy.java
rename to src/main/java/org/cryptomator/logging/LaunchAndSizeBasedTriggeringPolicy.java
index 3879fbdc6..25f5239ab 100644
--- a/src/main/java/org/cryptomator/logging/LaunchAndSizeBasedTriggerinPolicy.java
+++ b/src/main/java/org/cryptomator/logging/LaunchAndSizeBasedTriggeringPolicy.java
@@ -11,12 +11,12 @@ import java.io.File;
*
* @param Event type the policy possibly reacts to
*/
-public class LaunchAndSizeBasedTriggerinPolicy extends TriggeringPolicyBase {
+public class LaunchAndSizeBasedTriggeringPolicy extends TriggeringPolicyBase {
LaunchBasedTriggeringPolicy launchBasedTriggeringPolicy;
SizeBasedTriggeringPolicy sizeBasedTriggeringPolicy;
- public LaunchAndSizeBasedTriggerinPolicy(FileSize threshold) {
+ public LaunchAndSizeBasedTriggeringPolicy(FileSize threshold) {
this.launchBasedTriggeringPolicy = new LaunchBasedTriggeringPolicy<>();
this.sizeBasedTriggeringPolicy = new SizeBasedTriggeringPolicy<>();
sizeBasedTriggeringPolicy.setMaxFileSize(threshold);
diff --git a/src/main/java/org/cryptomator/logging/LoggerModule.java b/src/main/java/org/cryptomator/logging/LoggerModule.java
index 0b24e0e24..4866655e3 100644
--- a/src/main/java/org/cryptomator/logging/LoggerModule.java
+++ b/src/main/java/org/cryptomator/logging/LoggerModule.java
@@ -85,7 +85,7 @@ public class LoggerModule {
appender.setContext(context);
appender.setFile(logDir.resolve(LOGFILE_NAME).toString());
appender.setEncoder(encoder);
- LaunchAndSizeBasedTriggerinPolicy triggeringPolicy = new LaunchAndSizeBasedTriggerinPolicy(FileSize.valueOf(LOG_MAX_SIZE));
+ LaunchAndSizeBasedTriggeringPolicy triggeringPolicy = new LaunchAndSizeBasedTriggeringPolicy(FileSize.valueOf(LOG_MAX_SIZE));
triggeringPolicy.setContext(context);
triggeringPolicy.start();
appender.setTriggeringPolicy(triggeringPolicy);
diff --git a/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java b/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java
index 4b4e02ed2..627793d6e 100644
--- a/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java
+++ b/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java
@@ -5,8 +5,8 @@ import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.cryptofs.CryptoFileSystemProperties;
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
-import org.cryptomator.cryptofs.VaultCipherCombo;
import org.cryptomator.cryptolib.api.CryptoException;
+import org.cryptomator.cryptolib.api.CryptorProvider;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.api.MasterkeyLoader;
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
@@ -76,6 +76,7 @@ public class CreateNewVaultPasswordController implements FxController {
private final BooleanProperty readyToCreateVault;
private final ObjectBinding createVaultButtonState;
+ /* FXML */
public ToggleGroup recoveryKeyChoice;
public Toggle showRecoveryKey;
public Toggle skipRecoveryKey;
@@ -106,7 +107,7 @@ public class CreateNewVaultPasswordController implements FxController {
@FXML
public void initialize() {
- readyToCreateVault.bind(newPasswordSceneController.passwordsMatchAndSufficientProperty().and(recoveryKeyChoice.selectedToggleProperty().isNotNull()).and(processing.not()));
+ readyToCreateVault.bind(newPasswordSceneController.goodPasswordProperty().and(recoveryKeyChoice.selectedToggleProperty().isNotNull()).and(processing.not()));
window.setOnHiding(event -> {
newPasswordSceneController.passwordField.wipe();
newPasswordSceneController.reenterField.wipe();
@@ -182,7 +183,7 @@ public class CreateNewVaultPasswordController implements FxController {
// 2. initialize vault:
try {
MasterkeyLoader loader = ignored -> masterkey.clone();
- CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties().withCipherCombo(VaultCipherCombo.SIV_CTRMAC).withKeyLoader(loader).build();
+ CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties().withCipherCombo(CryptorProvider.Scheme.SIV_CTRMAC).withKeyLoader(loader).build();
CryptoFileSystemProvider.initialize(path, fsProps, DEFAULT_KEY_ID);
// 3. write vault-internal readme file:
diff --git a/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java b/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java
index ccc0184ac..54519f21f 100644
--- a/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java
+++ b/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java
@@ -62,7 +62,7 @@ public class ChangePasswordController implements FxController {
public void initialize() {
BooleanBinding checkboxNotConfirmed = finalConfirmationCheckbox.selectedProperty().not();
BooleanBinding oldPasswordFieldEmpty = oldPasswordField.textProperty().isEmpty();
- finishButton.disableProperty().bind(checkboxNotConfirmed.or(oldPasswordFieldEmpty).or(newPasswordController.passwordsMatchAndSufficientProperty().not()));
+ finishButton.disableProperty().bind(checkboxNotConfirmed.or(oldPasswordFieldEmpty).or(newPasswordController.goodPasswordProperty().not()));
window.setOnHiding(event -> {
oldPasswordField.wipe();
newPasswordController.passwordField.wipe();
diff --git a/src/main/java/org/cryptomator/ui/common/Animations.java b/src/main/java/org/cryptomator/ui/common/Animations.java
index d71309933..3c703a50d 100644
--- a/src/main/java/org/cryptomator/ui/common/Animations.java
+++ b/src/main/java/org/cryptomator/ui/common/Animations.java
@@ -1,11 +1,17 @@
package org.cryptomator.ui.common;
+import javafx.animation.Animation;
+import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
+import javafx.animation.RotateTransition;
+import javafx.animation.SequentialTransition;
import javafx.animation.Timeline;
import javafx.beans.value.WritableValue;
+import javafx.scene.Node;
import javafx.stage.Window;
import javafx.util.Duration;
+import java.util.stream.IntStream;
public class Animations {
@@ -33,4 +39,19 @@ public class Animations {
);
}
+ public static SequentialTransition createDiscrete360Rotation(Node toAnimate) {
+ var animation = new SequentialTransition(IntStream.range(0, 8).mapToObj(i -> Animations.createDiscrete45Rotation()).toArray(Animation[]::new));
+ animation.setCycleCount(Animation.INDEFINITE);
+ animation.setNode(toAnimate);
+ return animation;
+ }
+
+ private static RotateTransition createDiscrete45Rotation() {
+ var animation = new RotateTransition(Duration.millis(100));
+ animation.setInterpolator(Interpolator.DISCRETE);
+ animation.setByAngle(45);
+ animation.setCycleCount(1);
+ return animation;
+ }
+
}
diff --git a/src/main/java/org/cryptomator/ui/common/AutoAnimator.java b/src/main/java/org/cryptomator/ui/common/AutoAnimator.java
new file mode 100644
index 000000000..b5398339c
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/common/AutoAnimator.java
@@ -0,0 +1,84 @@
+package org.cryptomator.ui.common;
+
+import com.tobiasdiez.easybind.EasyBind;
+import com.tobiasdiez.easybind.Subscription;
+
+import javafx.animation.Animation;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.value.ObservableValue;
+
+/**
+ * Animation which starts and stops automatically based on an observable condition.
+ *
+ * During creation the consumer can optionally define actions to be executed everytime before the animation starts and after it stops.
+ */
+public class AutoAnimator {
+
+ private final T animation;
+ private final ObservableValue condition;
+ private final Runnable beforeStart;
+ private final Runnable afterStop;
+ private final Subscription sub;
+
+ AutoAnimator(T animation, ObservableValue condition, Runnable beforeStart, Runnable afterStop) {
+ this.animation = animation;
+ this.condition = condition;
+ this.beforeStart = beforeStart;
+ this.afterStop = afterStop;
+ this.sub = EasyBind.subscribe(condition, this::togglePlay);
+ }
+
+ public void playFromStart() {
+ beforeStart.run();
+ animation.playFromStart();
+ }
+
+ public void stop() {
+ animation.stop();
+ afterStop.run();
+ }
+
+ private void togglePlay(boolean play) {
+ if (play) {
+ this.playFromStart();
+ } else {
+ this.stop();
+ }
+ }
+
+ public static Builder animate(Animation animation) {
+ return new Builder(animation);
+ }
+
+ public static class Builder {
+
+ private Animation animation;
+ private ObservableValue condition = new SimpleBooleanProperty(true);
+ private Runnable beforeStart = () -> {};
+ private Runnable afterStop = () -> {};
+
+ private Builder(Animation animation) {
+ this.animation = animation;
+ }
+
+ public Builder onCondition(ObservableValue condition) {
+ this.condition = condition;
+ return this;
+ }
+
+ public Builder beforeStart(Runnable beforeStart) {
+ this.beforeStart = beforeStart;
+ return this;
+ }
+
+ public Builder afterStop(Runnable afterStop) {
+ this.afterStop = afterStop;
+ return this;
+ }
+
+ public AutoAnimator build() {
+ return new AutoAnimator(animation, condition, beforeStart, afterStop);
+ }
+
+ }
+}
diff --git a/src/main/java/org/cryptomator/ui/common/ErrorComponent.java b/src/main/java/org/cryptomator/ui/common/ErrorComponent.java
index 285270b4c..92276f5bd 100644
--- a/src/main/java/org/cryptomator/ui/common/ErrorComponent.java
+++ b/src/main/java/org/cryptomator/ui/common/ErrorComponent.java
@@ -2,8 +2,8 @@ package org.cryptomator.ui.common;
import dagger.BindsInstance;
import dagger.Subcomponent;
+import org.cryptomator.common.Nullable;
-import javax.annotation.Nullable;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.stage.Stage;
diff --git a/src/main/java/org/cryptomator/ui/common/ErrorController.java b/src/main/java/org/cryptomator/ui/common/ErrorController.java
index c7f19a9e6..85b335b15 100644
--- a/src/main/java/org/cryptomator/ui/common/ErrorController.java
+++ b/src/main/java/org/cryptomator/ui/common/ErrorController.java
@@ -1,6 +1,7 @@
package org.cryptomator.ui.common;
-import javax.annotation.Nullable;
+import org.cryptomator.common.Nullable;
+
import javax.inject.Inject;
import javax.inject.Named;
import javafx.fxml.FXML;
diff --git a/src/main/java/org/cryptomator/ui/common/FxmlFile.java b/src/main/java/org/cryptomator/ui/common/FxmlFile.java
index b8d5bbff0..ea0c1ed38 100644
--- a/src/main/java/org/cryptomator/ui/common/FxmlFile.java
+++ b/src/main/java/org/cryptomator/ui/common/FxmlFile.java
@@ -12,6 +12,7 @@ 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"), //
diff --git a/src/main/java/org/cryptomator/ui/common/FxmlLoaderFactory.java b/src/main/java/org/cryptomator/ui/common/FxmlLoaderFactory.java
index cf8940cc2..c10054ef4 100644
--- a/src/main/java/org/cryptomator/ui/common/FxmlLoaderFactory.java
+++ b/src/main/java/org/cryptomator/ui/common/FxmlLoaderFactory.java
@@ -5,7 +5,6 @@ import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import java.io.IOException;
-import java.io.InputStream;
import java.io.UncheckedIOException;
import java.util.Map;
import java.util.ResourceBundle;
@@ -26,11 +25,9 @@ public class FxmlLoaderFactory {
/**
* @return A new FXMLLoader instance
*/
- public FXMLLoader construct() {
- FXMLLoader loader = new FXMLLoader();
- loader.setControllerFactory(this::constructController);
- loader.setResources(resourceBundle);
- return loader;
+ private FXMLLoader construct(String fxmlResourceName) {
+ var url = getClass().getResource(fxmlResourceName);
+ return new FXMLLoader(url, resourceBundle, null, this::constructController);
}
/**
@@ -41,10 +38,8 @@ public class FxmlLoaderFactory {
* @throws IOException if an error occurs while loading the FXML file
*/
public FXMLLoader load(String fxmlResourceName) throws IOException {
- FXMLLoader loader = construct();
- try (InputStream in = getClass().getResourceAsStream(fxmlResourceName)) {
- loader.load(in);
- }
+ FXMLLoader loader = construct(fxmlResourceName);
+ loader.load();
return loader;
}
@@ -67,8 +62,8 @@ public class FxmlLoaderFactory {
}
Parent root = loader.getRoot();
// TODO: discuss if we can remove language-specific stylesheets
- // List addtionalStyleSheets = Splitter.on(',').omitEmptyStrings().splitToList(resourceBundle.getString("additionalStyleSheets"));
- // addtionalStyleSheets.forEach(styleSheet -> root.getStylesheets().add("/css/" + styleSheet));
+ // List additionalStyleSheets = Splitter.on(',').omitEmptyStrings().splitToList(resourceBundle.getString("additionalStyleSheets"));
+ // additionalStyleSheets.forEach(styleSheet -> root.getStylesheets().add("/css/" + styleSheet));
return sceneFactory.apply(root);
}
diff --git a/src/main/java/org/cryptomator/ui/common/NewPasswordController.java b/src/main/java/org/cryptomator/ui/common/NewPasswordController.java
index 13e59f2cd..caa0962f8 100644
--- a/src/main/java/org/cryptomator/ui/common/NewPasswordController.java
+++ b/src/main/java/org/cryptomator/ui/common/NewPasswordController.java
@@ -4,12 +4,12 @@ import com.tobiasdiez.easybind.EasyBind;
import org.cryptomator.ui.controls.FontAwesome5IconView;
import org.cryptomator.ui.controls.NiceSecurePasswordField;
-import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
+import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
-import javafx.beans.property.ReadOnlyBooleanWrapper;
+import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
@@ -20,7 +20,7 @@ public class NewPasswordController implements FxController {
private final ResourceBundle resourceBundle;
private final PasswordStrengthUtil strengthRater;
private final IntegerProperty passwordStrength = new SimpleIntegerProperty(-1);
- private final ReadOnlyBooleanWrapper passwordsMatchAndSufficient = new ReadOnlyBooleanWrapper();
+ private final BooleanProperty goodPassword = new SimpleBooleanProperty();
public NiceSecurePasswordField passwordField;
public NiceSecurePasswordField reenterField;
@@ -50,11 +50,10 @@ public class NewPasswordController implements FxController {
passwordMatchLabel.graphicProperty().bind(Bindings.when(passwordsMatch.and(reenterFieldNotEmpty)).then(passwordMatchCheckmark).otherwise(passwordMatchCross));
passwordMatchLabel.textProperty().bind(Bindings.when(passwordsMatch.and(reenterFieldNotEmpty)).then(resourceBundle.getString("newPassword.passwordsMatch")).otherwise(resourceBundle.getString("newPassword.passwordsDoNotMatch")));
- passwordField.textProperty().addListener(this::passwordsDidChange);
- reenterField.textProperty().addListener(this::passwordsDidChange);
+ BooleanBinding sufficientStrength = Bindings.createBooleanBinding(this::sufficientStrength, passwordField.textProperty());
+ goodPassword.bind(passwordsMatch.and(sufficientStrength));
}
-
private FontAwesome5IconView getIconViewForPasswordStrengthLabel() {
if (passwordField.getCharacters().length() == 0) {
return null;
@@ -67,22 +66,24 @@ public class NewPasswordController implements FxController {
}
}
- private void passwordsDidChange(@SuppressWarnings("unused") Observable observable) {
- if (passwordFieldsMatch() && strengthRater.fulfillsMinimumRequirements(passwordField.getCharacters())) {
- passwordsMatchAndSufficient.setValue(true);
- }
- }
-
private boolean passwordFieldsMatch() {
return CharSequence.compare(passwordField.getCharacters(), reenterField.getCharacters()) == 0;
}
- public ReadOnlyBooleanProperty passwordsMatchAndSufficientProperty() {
- return passwordsMatchAndSufficient.getReadOnlyProperty();
+ private boolean sufficientStrength() {
+ return strengthRater.fulfillsMinimumRequirements(passwordField.getCharacters());
}
/* Getter/Setter */
+ public ReadOnlyBooleanProperty goodPasswordProperty() {
+ return goodPassword;
+ }
+
+ public boolean isGoodPassword() {
+ return goodPassword.get();
+ }
+
public IntegerProperty passwordStrengthProperty() {
return passwordStrength;
}
diff --git a/src/main/java/org/cryptomator/ui/common/WeakBindings.java b/src/main/java/org/cryptomator/ui/common/WeakBindings.java
index e6071df1d..6efa747c9 100644
--- a/src/main/java/org/cryptomator/ui/common/WeakBindings.java
+++ b/src/main/java/org/cryptomator/ui/common/WeakBindings.java
@@ -77,7 +77,7 @@ public final class WeakBindings {
* @param observable The observable
* @return a IntegerBinding weakly referenced from the given observable
*/
- public static IntegerBinding bindInterger(ObservableValue observable) {
+ public static IntegerBinding bindInteger(ObservableValue observable) {
return new IntegerBinding() {
{
bind(observable);
diff --git a/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java b/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java
index a273447bb..15b1718e1 100644
--- a/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java
+++ b/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java
@@ -8,6 +8,8 @@ public enum FontAwesome5Icon {
ARROW_UP("\uF062"), //
BAN("\uF05E"), //
BUG("\uF188"), //
+ CARET_DOWN("\uF0D7"), //
+ CARET_RIGHT("\uF0Da"), //
CHECK("\uF00C"), //
CLOCK("\uF017"), //
COG("\uF013"), //
@@ -20,6 +22,7 @@ public enum FontAwesome5Icon {
EXCLAMATION_TRIANGLE("\uF071"), //
EYE("\uF06E"), //
EYE_SLASH("\uF070"), //
+ FAST_FORWARD("\uF050"), //
FILE("\uF15B"), //
FILE_IMPORT("\uF56F"), //
FOLDER_OPEN("\uF07C"), //
@@ -39,7 +42,7 @@ public enum FontAwesome5Icon {
REDO("\uF01E"), //
SEARCH("\uF002"), //
SPINNER("\uF110"), //
- STOPWATCH("\uF2F2"), //
+ STETHOSCOPE("\uF0f1"), //
SYNC("\uF021"), //
TIMES("\uF00D"), //
TRASH("\uF1F8"), //
diff --git a/src/main/java/org/cryptomator/ui/controls/FontAwesome5IconView.java b/src/main/java/org/cryptomator/ui/controls/FontAwesome5IconView.java
index 4d0797eaa..4c89ca674 100644
--- a/src/main/java/org/cryptomator/ui/controls/FontAwesome5IconView.java
+++ b/src/main/java/org/cryptomator/ui/controls/FontAwesome5IconView.java
@@ -21,8 +21,8 @@ public class FontAwesome5IconView extends Text {
private static final String FONT_PATH = "/css/fontawesome5-free-solid.otf";
private static final Font FONT;
- private ObjectProperty glyph = new SimpleObjectProperty<>(this, "glyph", DEFAULT_GLYPH);
- private DoubleProperty glyphSize = new SimpleDoubleProperty(this, "glyphSize", DEFAULT_GLYPH_SIZE);
+ protected final ObjectProperty glyph = new SimpleObjectProperty<>(this, "glyph", DEFAULT_GLYPH);
+ protected final DoubleProperty glyphSize = new SimpleDoubleProperty(this, "glyphSize", DEFAULT_GLYPH_SIZE);
static {
try {
@@ -42,7 +42,7 @@ public class FontAwesome5IconView extends Text {
}
private void glyphChanged(@SuppressWarnings("unused") ObservableValue extends FontAwesome5Icon> observable, @SuppressWarnings("unused") FontAwesome5Icon oldValue, FontAwesome5Icon newValue) {
- setText(newValue.unicode());
+ setText(newValue == null ? null : newValue.unicode());
}
private void glyphSizeChanged(@SuppressWarnings("unused") ObservableValue extends Number> observable, @SuppressWarnings("unused") Number oldValue, Number newValue) {
diff --git a/src/main/java/org/cryptomator/ui/controls/FontAwesome5Spinner.java b/src/main/java/org/cryptomator/ui/controls/FontAwesome5Spinner.java
new file mode 100644
index 000000000..eb28a90a9
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/controls/FontAwesome5Spinner.java
@@ -0,0 +1,44 @@
+package org.cryptomator.ui.controls;
+
+import org.cryptomator.ui.common.Animations;
+import org.cryptomator.ui.common.AutoAnimator;
+
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+
+/**
+ * An animated progress spinner using the {@link FontAwesome5IconView} with the spinner glyph.
+ *
+ * Using the default constructor, the animation is always played if the icon is visible. To animate on other conditions, use the constructor with the "spinning" property.
+ */
+public class FontAwesome5Spinner extends FontAwesome5IconView {
+
+ protected final BooleanProperty spinning = new SimpleBooleanProperty(this, "spinning", true);
+
+ private AutoAnimator animator;
+
+ public FontAwesome5Spinner() {
+ setGlyph(FontAwesome5Icon.SPINNER);
+ var animation = Animations.createDiscrete360Rotation(this);
+ this.animator = AutoAnimator.animate(animation) //
+ .afterStop(() -> setRotate(0)) //
+ .onCondition(spinning.and(visibleProperty())) //
+ .build();
+ }
+
+ /* Getter/Setter */
+
+ public BooleanProperty spinningProperty() {
+ return spinning;
+ }
+
+ public boolean isSpinning() {
+ return spinning.get();
+ }
+
+ public void setSpinning(boolean spinning) {
+ this.spinning.set(spinning);
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/ui/controls/NiceSecurePasswordField.java b/src/main/java/org/cryptomator/ui/controls/NiceSecurePasswordField.java
index 928cfc40e..4a4e43fff 100644
--- a/src/main/java/org/cryptomator/ui/controls/NiceSecurePasswordField.java
+++ b/src/main/java/org/cryptomator/ui/controls/NiceSecurePasswordField.java
@@ -12,8 +12,8 @@ import javafx.scene.layout.StackPane;
public class NiceSecurePasswordField extends StackPane {
private static final String STYLE_CLASS = "nice-secure-password-field";
- private static final String ICONS_STLYE_CLASS = "icons";
- private static final String REVEAL_BUTTON_STLYE_CLASS = "reveal-button";
+ private static final String ICONS_STYLE_CLASS = "icons";
+ private static final String REVEAL_BUTTON_STYLE_CLASS = "reveal-button";
private static final int ICON_SPACING = 6;
private static final double ICON_SIZE = 14.0;
@@ -30,7 +30,7 @@ public class NiceSecurePasswordField extends StackPane {
iconContainer.setAlignment(Pos.CENTER_RIGHT);
iconContainer.setMaxWidth(Double.NEGATIVE_INFINITY);
iconContainer.setPrefWidth(42); // TODO
- iconContainer.getStyleClass().add(ICONS_STLYE_CLASS);
+ iconContainer.getStyleClass().add(ICONS_STYLE_CLASS);
StackPane.setAlignment(iconContainer, Pos.CENTER_RIGHT);
capsLockedIcon.setGlyph(FontAwesome5Icon.ARROW_UP);
@@ -51,7 +51,7 @@ public class NiceSecurePasswordField extends StackPane {
revealPasswordButton.setFocusTraversable(false);
revealPasswordButton.visibleProperty().bind(passwordField.focusedProperty());
revealPasswordButton.managedProperty().bind(passwordField.focusedProperty());
- revealPasswordButton.getStyleClass().add(REVEAL_BUTTON_STLYE_CLASS);
+ revealPasswordButton.getStyleClass().add(REVEAL_BUTTON_STYLE_CLASS);
passwordField.revealPasswordProperty().bind(revealPasswordButton.selectedProperty());
diff --git a/src/main/java/org/cryptomator/ui/controls/ThrougputLabel.java b/src/main/java/org/cryptomator/ui/controls/ThroughputLabel.java
similarity index 96%
rename from src/main/java/org/cryptomator/ui/controls/ThrougputLabel.java
rename to src/main/java/org/cryptomator/ui/controls/ThroughputLabel.java
index a21e6d916..19999ea11 100644
--- a/src/main/java/org/cryptomator/ui/controls/ThrougputLabel.java
+++ b/src/main/java/org/cryptomator/ui/controls/ThroughputLabel.java
@@ -8,7 +8,7 @@ import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.control.Label;
-public class ThrougputLabel extends Label {
+public class ThroughputLabel extends Label {
private static final long KIBS_THRESHOLD = 1l << 7; // 0.128 kiB/s
private static final long MIBS_THRESHOLD = 1l << 19; // 0.512 MiB/s
@@ -18,7 +18,7 @@ public class ThrougputLabel extends Label {
private final StringProperty mibsFormat = new SimpleStringProperty("%.3f");
private final LongProperty bytesPerSecond = new SimpleLongProperty();
- public ThrougputLabel() {
+ public ThroughputLabel() {
textProperty().bind(createStringBinding());
}
diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java
index 02a2e5b9e..1812d38bd 100644
--- a/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java
+++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java
@@ -188,9 +188,12 @@ public class FxApplication extends Application {
}
private void applySystemTheme() {
- appearanceProvider.ifPresent(appearanceProvider -> {
- systemInterfaceThemeChanged(appearanceProvider.getSystemTheme());
- });
+ if (appearanceProvider.isPresent()) {
+ systemInterfaceThemeChanged(appearanceProvider.get().getSystemTheme());
+ } else {
+ LOG.warn("No UiAppearanceProvider present, assuming LIGHT theme...");
+ applyLightTheme();
+ }
}
private void applyLightTheme() {
diff --git a/src/main/java/org/cryptomator/ui/health/BatchService.java b/src/main/java/org/cryptomator/ui/health/BatchService.java
deleted file mode 100644
index f3968c27d..000000000
--- a/src/main/java/org/cryptomator/ui/health/BatchService.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package org.cryptomator.ui.health;
-
-import com.google.common.base.Preconditions;
-import com.google.common.base.Suppliers;
-import dagger.Lazy;
-
-import javax.inject.Inject;
-import javafx.concurrent.Service;
-import javafx.concurrent.Task;
-import java.util.Collection;
-import java.util.Iterator;
-import java.util.concurrent.ExecutorService;
-import java.util.function.Supplier;
-
-public class BatchService extends Service {
-
- private final Iterator remainingTasks;
-
- @Inject
- public BatchService(Iterable tasks) {
- this.remainingTasks = tasks.iterator();
- }
-
- @Override
- protected Task createTask() {
- Preconditions.checkState(remainingTasks.hasNext(), "No remaining tasks");
- return remainingTasks.next();
- }
-
- @Override
- protected void succeeded() {
- if (remainingTasks.hasNext()) {
- this.restart();
- }
- }
-}
diff --git a/src/main/java/org/cryptomator/ui/health/Check.java b/src/main/java/org/cryptomator/ui/health/Check.java
new file mode 100644
index 000000000..52bee578c
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/health/Check.java
@@ -0,0 +1,103 @@
+package org.cryptomator.ui.health;
+
+import org.cryptomator.cryptofs.health.api.DiagnosticResult;
+import org.cryptomator.cryptofs.health.api.HealthCheck;
+
+import javafx.beans.Observable;
+import javafx.beans.binding.BooleanBinding;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+
+public class Check {
+
+ private final HealthCheck check;
+
+ private final BooleanProperty chosenForExecution = new SimpleBooleanProperty(false);
+ private final ObjectProperty state = new SimpleObjectProperty<>(CheckState.RUNNABLE);
+ private final ObservableList results = FXCollections.observableArrayList(Result::observables);
+ private final ObjectProperty highestResultSeverity = new SimpleObjectProperty<>(null);
+ private final ObjectProperty error = new SimpleObjectProperty<>(null);
+ private final BooleanBinding isInReRunState = state.isNotEqualTo(CheckState.RUNNING).or(state.isNotEqualTo(CheckState.SCHEDULED));
+
+ Check(HealthCheck check) {
+ this.check = check;
+ }
+
+ String getName() {
+ return check.name();
+ }
+
+ HealthCheck getHealthCheck() {
+ return check;
+ }
+
+ BooleanProperty chosenForExecutionProperty() {
+ return chosenForExecution;
+ }
+
+ boolean isChosenForExecution() {
+ return chosenForExecution.get();
+ }
+
+ ObjectProperty stateProperty() {
+ return state;
+ }
+
+ CheckState getState() {
+ return state.get();
+ }
+
+ void setState(CheckState newState) {
+ state.set(newState);
+ }
+
+ ObjectProperty errorProperty() {
+ return error;
+ }
+
+ Throwable getError() {
+ return error.get();
+ }
+
+ void setError(Throwable t) {
+ error.set(t);
+ }
+
+ ObjectProperty highestResultSeverityProperty() {
+ return highestResultSeverity;
+ }
+
+ DiagnosticResult.Severity getHighestResultSeverity() {
+ return highestResultSeverity.get();
+ }
+
+ void setHighestResultSeverity(DiagnosticResult.Severity severity) {
+ highestResultSeverity.set(severity);
+ }
+
+ boolean isInReRunState() {
+ return isInReRunState.get();
+ }
+
+ enum CheckState {
+ RUNNABLE,
+ SCHEDULED,
+ RUNNING,
+ SUCCEEDED,
+ SKIPPED,
+ ERROR,
+ CANCELLED;
+ }
+
+ ObservableList getResults() {
+ return results;
+ }
+
+ Observable[] observables() {
+ return new Observable[]{chosenForExecution, state, results, error};
+ }
+}
diff --git a/src/main/java/org/cryptomator/ui/health/CheckDetailController.java b/src/main/java/org/cryptomator/ui/health/CheckDetailController.java
index d579ff709..66f2e9bf5 100644
--- a/src/main/java/org/cryptomator/ui/health/CheckDetailController.java
+++ b/src/main/java/org/cryptomator/ui/health/CheckDetailController.java
@@ -12,7 +12,6 @@ import javafx.beans.binding.Binding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
-import javafx.concurrent.Worker;
import javafx.fxml.FXML;
import javafx.scene.control.ListView;
import java.util.function.Function;
@@ -21,54 +20,56 @@ import java.util.stream.Stream;
@HealthCheckScoped
public class CheckDetailController implements FxController {
- private final EasyObservableList results;
- private final OptionalBinding taskState;
- private final Binding taskName;
- private final Binding taskDuration;
- private final ResultListCellFactory resultListCellFactory;
- private final Binding taskRunning;
- private final Binding taskScheduled;
- private final Binding taskFinished;
- private final Binding taskNotStarted;
- private final Binding taskSucceeded;
- private final Binding taskFailed;
- private final Binding taskCancelled;
+ private final EasyObservableList results;
+ private final ObjectProperty check;
+ private final OptionalBinding checkState;
+ private final Binding checkName;
+ private final Binding checkRunning;
+ private final Binding checkScheduled;
+ private final Binding checkFinished;
+ private final Binding checkSkipped;
+ private final Binding checkSucceeded;
+ private final Binding checkFailed;
+ private final Binding checkCancelled;
private final Binding countOfWarnSeverity;
private final Binding countOfCritSeverity;
+ private final Binding warnOrCritsExist;
+ private final ResultListCellFactory resultListCellFactory;
- public ListView resultsListView;
+ public ListView resultsListView;
private Subscription resultSubscription;
@Inject
- public CheckDetailController(ObjectProperty selectedTask, ResultListCellFactory resultListCellFactory) {
- this.results = EasyBind.wrapList(FXCollections.observableArrayList());
- this.taskState = EasyBind.wrapNullable(selectedTask).mapObservable(HealthCheckTask::stateProperty);
- this.taskName = EasyBind.wrapNullable(selectedTask).map(HealthCheckTask::getTitle).orElse("");
- this.taskDuration = EasyBind.wrapNullable(selectedTask).mapObservable(HealthCheckTask::durationInMillisProperty).orElse(-1L);
+ public CheckDetailController(ObjectProperty selectedTask, ResultListCellFactory resultListCellFactory) {
this.resultListCellFactory = resultListCellFactory;
- this.taskRunning = EasyBind.wrapNullable(selectedTask).mapObservable(HealthCheckTask::runningProperty).orElse(false); //TODO: DOES NOT WORK
- this.taskScheduled = taskState.map(Worker.State.SCHEDULED::equals).orElse(false);
- this.taskNotStarted = taskState.map(Worker.State.READY::equals).orElse(false);
- this.taskSucceeded = taskState.map(Worker.State.SUCCEEDED::equals).orElse(false);
- this.taskFailed = taskState.map(Worker.State.FAILED::equals).orElse(false);
- this.taskCancelled = taskState.map(Worker.State.CANCELLED::equals).orElse(false);
- this.taskFinished = EasyBind.combine(taskSucceeded, taskFailed, taskCancelled, (a, b, c) -> a || b || c);
+ this.results = EasyBind.wrapList(FXCollections.observableArrayList());
+ this.check = selectedTask;
+ this.checkState = EasyBind.wrapNullable(selectedTask).mapObservable(Check::stateProperty);
+ this.checkName = EasyBind.wrapNullable(selectedTask).map(Check::getName).orElse("");
+ this.checkRunning = checkState.map(Check.CheckState.RUNNING::equals).orElse(false);
+ this.checkScheduled = checkState.map(Check.CheckState.SCHEDULED::equals).orElse(false);
+ this.checkSkipped = checkState.map(Check.CheckState.SKIPPED::equals).orElse(false);
+ this.checkSucceeded = checkState.map(Check.CheckState.SUCCEEDED::equals).orElse(false);
+ this.checkFailed = checkState.map(Check.CheckState.ERROR::equals).orElse(false);
+ this.checkCancelled = checkState.map(Check.CheckState.CANCELLED::equals).orElse(false);
+ this.checkFinished = EasyBind.combine(checkSucceeded, checkFailed, checkCancelled, (a, b, c) -> a || b || c);
this.countOfWarnSeverity = results.reduce(countSeverity(DiagnosticResult.Severity.WARN));
this.countOfCritSeverity = results.reduce(countSeverity(DiagnosticResult.Severity.CRITICAL));
+ this.warnOrCritsExist = EasyBind.combine(checkSucceeded, countOfWarnSeverity, countOfCritSeverity, (suceeded, warns, crits) -> suceeded && (warns.longValue() > 0 || crits.longValue() > 0) );
selectedTask.addListener(this::selectedTaskChanged);
}
- private void selectedTaskChanged(ObservableValue extends HealthCheckTask> observable, HealthCheckTask oldValue, HealthCheckTask newValue) {
+ private void selectedTaskChanged(ObservableValue extends Check> observable, Check oldValue, Check newValue) {
if (resultSubscription != null) {
resultSubscription.unsubscribe();
}
if (newValue != null) {
- resultSubscription = EasyBind.bindContent(results, newValue.results());
+ resultSubscription = EasyBind.bindContent(results, newValue.getResults());
}
}
- private Function, Long> countSeverity(DiagnosticResult.Severity severity) {
- return stream -> stream.filter(item -> severity.equals(item.getServerity())).count();
+ private Function, Long> countSeverity(DiagnosticResult.Severity severity) {
+ return stream -> stream.filter(item -> severity.equals(item.diagnosis().getSeverity())).count();
}
@FXML
@@ -79,20 +80,12 @@ public class CheckDetailController implements FxController {
/* Getter/Setter */
- public String getTaskName() {
- return taskName.getValue();
+ public String getCheckName() {
+ return checkName.getValue();
}
- public Binding taskNameProperty() {
- return taskName;
- }
-
- public Number getTaskDuration() {
- return taskDuration.getValue();
- }
-
- public Binding taskDurationProperty() {
- return taskDuration;
+ public Binding checkNameProperty() {
+ return checkName;
}
public long getCountOfWarnSeverity() {
@@ -111,60 +104,75 @@ public class CheckDetailController implements FxController {
return countOfCritSeverity;
}
- public boolean isTaskRunning() {
- return taskRunning.getValue();
+ public boolean isCheckRunning() {
+ return checkRunning.getValue();
}
- public Binding taskRunningProperty() {
- return taskRunning;
+ public Binding checkRunningProperty() {
+ return checkRunning;
}
- public boolean isTaskFinished() {
- return taskFinished.getValue();
+ public boolean isCheckFinished() {
+ return checkFinished.getValue();
}
- public Binding taskFinishedProperty() {
- return taskFinished;
+ public Binding checkFinishedProperty() {
+ return checkFinished;
}
- public boolean isTaskScheduled() {
- return taskScheduled.getValue();
+ public boolean isCheckScheduled() {
+ return checkScheduled.getValue();
}
- public Binding taskScheduledProperty() {
- return taskScheduled;
+ public Binding checkScheduledProperty() {
+ return checkScheduled;
}
- public boolean isTaskNotStarted() {
- return taskNotStarted.getValue();
+ public boolean isCheckSkipped() {
+ return checkSkipped.getValue();
}
- public Binding taskNotStartedProperty() {
- return taskNotStarted;
+ public Binding checkSkippedProperty() {
+ return checkSkipped;
}
- public boolean isTaskSucceeded() {
- return taskSucceeded.getValue();
+ public boolean isCheckSucceeded() {
+ return checkSucceeded.getValue();
}
- public Binding taskSucceededProperty() {
- return taskSucceeded;
+ public Binding checkSucceededProperty() {
+ return checkSucceeded;
}
- public boolean isTaskFailed() {
- return taskFailed.getValue();
+ public boolean isCheckFailed() {
+ return checkFailed.getValue();
}
- public Binding taskFailedProperty() {
- return taskFailed;
+ public Binding checkFailedProperty() {
+ return checkFailed;
}
- public boolean isTaskCancelled() {
- return taskCancelled.getValue();
+ public boolean isCheckCancelled() {
+ return checkCancelled.getValue();
}
- public Binding taskCancelledProperty() {
- return taskCancelled;
+ public Binding warnOrCritsExistProperty() {
+ return warnOrCritsExist;
}
+ public boolean isWarnOrCritsExist() {
+ return warnOrCritsExist.getValue();
+ }
+
+ public Binding checkCancelledProperty() {
+ return checkCancelled;
+ }
+
+ public ObjectProperty checkProperty() {
+ return check;
+ }
+
+ public Check getCheck() {
+ return check.get();
+ }
}
diff --git a/src/main/java/org/cryptomator/ui/health/CheckExecutor.java b/src/main/java/org/cryptomator/ui/health/CheckExecutor.java
new file mode 100644
index 000000000..5b14bd17c
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/health/CheckExecutor.java
@@ -0,0 +1,109 @@
+package org.cryptomator.ui.health;
+
+import com.google.common.collect.Comparators;
+import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.cryptofs.VaultConfig;
+import org.cryptomator.cryptofs.health.api.DiagnosticResult;
+import org.cryptomator.cryptolib.api.CryptorProvider;
+import org.cryptomator.cryptolib.api.Masterkey;
+
+import javax.inject.Inject;
+import javafx.application.Platform;
+import javafx.concurrent.Task;
+import java.nio.file.Path;
+import java.security.SecureRandom;
+import java.util.List;
+import java.util.concurrent.BlockingDeque;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.atomic.AtomicReference;
+
+@HealthCheckScoped
+public class CheckExecutor {
+
+ private final Path vaultPath;
+ private final SecureRandom csprng;
+ private final Masterkey masterkey;
+ private final VaultConfig vaultConfig;
+ private final ExecutorService sequentialExecutor;
+ private final BlockingDeque tasksToExecute;
+
+
+ @Inject
+ public CheckExecutor(@HealthCheckWindow Vault vault, AtomicReference masterkeyRef, AtomicReference vaultConfigRef, SecureRandom csprng) {
+ this.vaultPath = vault.getPath();
+ this.masterkey = masterkeyRef.get();
+ this.vaultConfig = vaultConfigRef.get();
+ this.csprng = csprng;
+ this.tasksToExecute = new LinkedBlockingDeque<>();
+ this.sequentialExecutor = Executors.newSingleThreadExecutor();
+ }
+
+ public synchronized void executeBatch(List checks) {
+ checks.stream().map(c -> {
+ c.setState(Check.CheckState.SCHEDULED);
+ var task = new CheckTask(c);
+ tasksToExecute.addLast(task);
+ return task;
+ }).forEach(sequentialExecutor::submit);
+ }
+
+ public synchronized void cancel() {
+ CheckTask task;
+ while ((task = tasksToExecute.pollLast()) != null) {
+ task.cancel(true);
+ }
+ }
+
+ private class CheckTask extends Task {
+
+ private final Check c;
+ private DiagnosticResult.Severity highestResultSeverity = DiagnosticResult.Severity.GOOD;
+
+ CheckTask(Check c) {
+ this.c = c;
+ }
+
+ @Override
+ protected Void call() throws Exception {
+ try (var masterkeyClone = masterkey.clone(); //
+ var cryptor = CryptorProvider.forScheme(vaultConfig.getCipherCombo()).provide(masterkeyClone, csprng)) {
+ c.getHealthCheck().check(vaultPath, vaultConfig, masterkeyClone, cryptor, diagnosis -> {
+ Platform.runLater(() -> c.getResults().add(Result.create(diagnosis)));
+ highestResultSeverity = Comparators.max(highestResultSeverity, diagnosis.getSeverity());
+ });
+ }
+ return null;
+ }
+
+ @Override
+ protected void running() {
+ c.setState(Check.CheckState.RUNNING);
+ }
+
+ @Override
+ protected void cancelled() {
+ c.setState(Check.CheckState.CANCELLED);
+ }
+
+ @Override
+ protected void succeeded() {
+ c.setState(Check.CheckState.SUCCEEDED);
+ c.setHighestResultSeverity(highestResultSeverity);
+ }
+
+ @Override
+ protected void failed() {
+ c.setState(Check.CheckState.ERROR);
+ c.setError(this.getException());
+ }
+
+ @Override
+ protected void done() {
+ tasksToExecute.remove(this);
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/org/cryptomator/ui/health/CheckListCell.java b/src/main/java/org/cryptomator/ui/health/CheckListCell.java
deleted file mode 100644
index 78f8b1b33..000000000
--- a/src/main/java/org/cryptomator/ui/health/CheckListCell.java
+++ /dev/null
@@ -1,64 +0,0 @@
-package org.cryptomator.ui.health;
-
-import org.cryptomator.ui.controls.FontAwesome5Icon;
-import org.cryptomator.ui.controls.FontAwesome5IconView;
-
-import javafx.beans.binding.Bindings;
-import javafx.beans.value.ObservableValue;
-import javafx.concurrent.Worker;
-import javafx.geometry.Insets;
-import javafx.geometry.Pos;
-import javafx.scene.Node;
-import javafx.scene.control.ContentDisplay;
-import javafx.scene.control.ListCell;
-
-class CheckListCell extends ListCell {
-
- private final FontAwesome5IconView stateIcon = new FontAwesome5IconView();
-
- CheckListCell() {
- setPadding(new Insets(6));
- setAlignment(Pos.CENTER_LEFT);
- setContentDisplay(ContentDisplay.LEFT);
- }
-
- @Override
- protected void updateItem(HealthCheckTask item, boolean empty) {
- super.updateItem(item, empty);
-
- if (item != null) {
- textProperty().bind(item.titleProperty());
- item.stateProperty().addListener(this::stateChanged);
- graphicProperty().bind(Bindings.createObjectBinding(() -> graphicForState(item.getState()),item.stateProperty()));
- stateIcon.setGlyph(glyphForState(item.getState()));
- } else {
- textProperty().unbind();
- graphicProperty().unbind();
- setGraphic(null);
- setText(null);
- }
- }
-
- private void stateChanged(ObservableValue extends Worker.State> observable, Worker.State oldState, Worker.State newState) {
- stateIcon.setGlyph(glyphForState(newState));
- stateIcon.setVisible(true);
- }
-
- private Node graphicForState(Worker.State state) {
- return switch (state) {
- case READY -> null;
- case SCHEDULED, RUNNING, FAILED, CANCELLED, SUCCEEDED -> stateIcon;
- };
- }
-
- private FontAwesome5Icon glyphForState(Worker.State state) {
- return switch (state) {
- case READY -> FontAwesome5Icon.COG; //just a placeholder
- case SCHEDULED -> FontAwesome5Icon.CLOCK;
- case RUNNING -> FontAwesome5Icon.SPINNER;
- case FAILED -> FontAwesome5Icon.EXCLAMATION_TRIANGLE;
- case CANCELLED -> FontAwesome5Icon.BAN;
- case SUCCEEDED -> FontAwesome5Icon.CHECK;
- };
- }
-}
diff --git a/src/main/java/org/cryptomator/ui/health/CheckListCellController.java b/src/main/java/org/cryptomator/ui/health/CheckListCellController.java
new file mode 100644
index 000000000..799b73358
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/health/CheckListCellController.java
@@ -0,0 +1,70 @@
+package org.cryptomator.ui.health;
+
+import com.tobiasdiez.easybind.EasyBind;
+import com.tobiasdiez.easybind.Subscription;
+import org.cryptomator.ui.common.FxController;
+
+import javax.inject.Inject;
+import javafx.beans.binding.Binding;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.scene.control.CheckBox;
+import java.util.ArrayList;
+import java.util.List;
+
+public class CheckListCellController implements FxController {
+
+
+ private final ObjectProperty check;
+ private final Binding checkName;
+ private final Binding checkRunnable;
+ private final List subscriptions;
+
+ /* FXML */
+ public CheckBox forRunSelectedCheckBox;
+
+ @Inject
+ public CheckListCellController() {
+ check = new SimpleObjectProperty<>();
+ checkRunnable = EasyBind.wrapNullable(check).mapObservable(Check::stateProperty).map(Check.CheckState.RUNNABLE::equals).orElse(false);
+ checkName = EasyBind.wrapNullable(check).map(Check::getName).orElse("");
+ subscriptions = new ArrayList<>();
+ }
+
+ public void initialize() {
+ subscriptions.add(EasyBind.subscribe(check, c -> {
+ forRunSelectedCheckBox.selectedProperty().unbind();
+ if (c != null) {
+ forRunSelectedCheckBox.selectedProperty().bindBidirectional(c.chosenForExecutionProperty());
+ }
+ }));
+ }
+
+ public ObjectProperty checkProperty() {
+ return check;
+ }
+
+ public Check getCheck() {
+ return check.get();
+ }
+
+ public void setCheck(Check c) {
+ check.set(c);
+ }
+
+ public Binding checkNameProperty() {
+ return checkName;
+ }
+
+ public String getCheckName() {
+ return checkName.getValue();
+ }
+
+ public Binding checkRunnableProperty() {
+ return checkRunnable;
+ }
+
+ public boolean isCheckRunnable() {
+ return checkRunnable.getValue();
+ }
+}
diff --git a/src/main/java/org/cryptomator/ui/health/CheckListCellFactory.java b/src/main/java/org/cryptomator/ui/health/CheckListCellFactory.java
new file mode 100644
index 000000000..d8ccc8d48
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/health/CheckListCellFactory.java
@@ -0,0 +1,58 @@
+package org.cryptomator.ui.health;
+
+import org.cryptomator.ui.common.FxmlLoaderFactory;
+
+import javax.inject.Inject;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Parent;
+import javafx.scene.control.ContentDisplay;
+import javafx.scene.control.ListCell;
+import javafx.scene.control.ListView;
+import javafx.util.Callback;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+
+// unscoped because each cell needs its own controller
+public class CheckListCellFactory implements Callback, ListCell> {
+
+ private final FxmlLoaderFactory fxmlLoaders;
+
+ @Inject
+ CheckListCellFactory(@HealthCheckWindow FxmlLoaderFactory fxmlLoaders) {
+ this.fxmlLoaders = fxmlLoaders;
+ }
+
+ @Override
+ public ListCell call(ListView param) {
+ try {
+ FXMLLoader fxmlLoader = fxmlLoaders.load("/fxml/health_check_listcell.fxml");
+ return new CheckListCellFactory.Cell(fxmlLoader.getRoot(), fxmlLoader.getController());
+ } catch (IOException e) {
+ throw new UncheckedIOException("Failed to load /fxml/health_check_listcell.fxml.", e);
+ }
+ }
+
+ private static class Cell extends ListCell {
+
+ private final Parent node;
+ private final CheckListCellController controller;
+
+ public Cell(Parent node, CheckListCellController controller) {
+ this.node = node;
+ this.controller = controller;
+ }
+
+ @Override
+ protected void updateItem(Check item, boolean empty) {
+ super.updateItem(item, empty);
+ if (item == null || empty) {
+ setText(null);
+ setGraphic(null);
+ } else {
+ controller.setCheck(item);
+ setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
+ setGraphic(node);
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/cryptomator/ui/health/CheckListController.java b/src/main/java/org/cryptomator/ui/health/CheckListController.java
index ccb41d56b..75ecdef52 100644
--- a/src/main/java/org/cryptomator/ui/health/CheckListController.java
+++ b/src/main/java/org/cryptomator/ui/health/CheckListController.java
@@ -1,7 +1,6 @@
package org.cryptomator.ui.health;
import com.google.common.base.Preconditions;
-import com.tobiasdiez.easybind.EasyBind;
import dagger.Lazy;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxController;
@@ -9,147 +8,113 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
-import javafx.beans.binding.Binding;
+import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
-import javafx.beans.property.BooleanProperty;
-import javafx.beans.property.IntegerProperty;
+import javafx.beans.binding.IntegerBinding;
import javafx.beans.property.ObjectProperty;
-import javafx.beans.property.SimpleBooleanProperty;
-import javafx.beans.property.SimpleIntegerProperty;
-import javafx.beans.property.SimpleObjectProperty;
-import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
-import javafx.concurrent.Worker;
+import javafx.collections.transformation.FilteredList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ListView;
-import javafx.scene.control.cell.CheckBoxListCell;
+import javafx.scene.control.SelectionMode;
import javafx.stage.Stage;
-import javafx.util.StringConverter;
import java.io.IOException;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ExecutorService;
+import java.util.List;
@HealthCheckScoped
public class CheckListController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(CheckListController.class);
- private static final Set END_STATES = Set.of(Worker.State.FAILED, Worker.State.CANCELLED, Worker.State.SUCCEEDED);
private final Stage window;
- private final ObservableList tasks;
+ private final ObservableList checks;
+ private final CheckExecutor checkExecutor;
+ private final FilteredList chosenChecks;
private final ReportWriter reportWriter;
- private final ExecutorService executorService;
- private final ObjectProperty selectedTask;
- private final Lazy errorComponenBuilder;
- private final SimpleObjectProperty> runningTask;
- private final Binding running;
- private final Binding finished;
- private final Map listPickIndicators;
- private final IntegerProperty numberOfPickedChecks;
+ private final ObjectProperty selectedCheck;
+ private final BooleanBinding mainRunStarted; //TODO: rerunning not considered for now
+ private final BooleanBinding somethingsRunning;
+ private final Lazy errorComponentBuilder;
+ private final IntegerBinding chosenTaskCount;
private final BooleanBinding anyCheckSelected;
- private final BooleanProperty showResultScreen;
+ private final CheckListCellFactory listCellFactory;
/* FXML */
- public ListView checksListView;
-
+ public ListView checksListView;
@Inject
- public CheckListController(@HealthCheckWindow Stage window, Lazy> tasks, ReportWriter reportWriteTask, ObjectProperty selectedTask, ExecutorService executorService, Lazy errorComponenBuilder) {
+ public CheckListController(@HealthCheckWindow Stage window, List checks, CheckExecutor checkExecutor, ReportWriter reportWriteTask, ObjectProperty selectedCheck, Lazy errorComponentBuilder, CheckListCellFactory listCellFactory) {
this.window = window;
- this.tasks = FXCollections.observableArrayList(tasks.get());
+ this.checks = FXCollections.observableList(checks, Check::observables);
+ this.checkExecutor = checkExecutor;
+ this.listCellFactory = listCellFactory;
+ this.chosenChecks = this.checks.filtered(Check::isChosenForExecution);
this.reportWriter = reportWriteTask;
- this.executorService = executorService;
- this.selectedTask = selectedTask;
- this.errorComponenBuilder = errorComponenBuilder;
- this.runningTask = new SimpleObjectProperty<>();
- this.running = EasyBind.wrapNullable(runningTask).mapObservable(Worker::runningProperty).orElse(false);
- this.finished = EasyBind.wrapNullable(runningTask).mapObservable(Worker::stateProperty).map(END_STATES::contains).orElse(false);
- this.listPickIndicators = new HashMap<>();
- this.numberOfPickedChecks = new SimpleIntegerProperty(0);
- this.tasks.forEach(task -> {
- var entrySelectedProp = new SimpleBooleanProperty(false);
- entrySelectedProp.addListener((observable, oldValue, newValue) -> numberOfPickedChecks.set(numberOfPickedChecks.get() + (newValue ? 1 : -1)));
- listPickIndicators.put(task, entrySelectedProp);
- });
- this.anyCheckSelected = selectedTask.isNotNull();
- this.showResultScreen = new SimpleBooleanProperty(false);
+ this.selectedCheck = selectedCheck;
+ this.errorComponentBuilder = errorComponentBuilder;
+ this.chosenTaskCount = Bindings.size(this.chosenChecks);
+ this.mainRunStarted = Bindings.isEmpty(this.checks.filtered(c -> c.getState() == Check.CheckState.RUNNABLE));
+ this.somethingsRunning = Bindings.isNotEmpty(this.checks.filtered(c -> c.getState() == Check.CheckState.SCHEDULED || c.getState() == Check.CheckState.RUNNING));
+ this.anyCheckSelected = selectedCheck.isNotNull();
}
@FXML
public void initialize() {
- checksListView.setItems(tasks);
- checksListView.setCellFactory(CheckBoxListCell.forListView(listPickIndicators::get, new StringConverter() {
- @Override
- public String toString(HealthCheckTask object) {
- return object.getTitle();
- }
-
- @Override
- public HealthCheckTask fromString(String string) {
- return null;
- }
- }));
- selectedTask.bind(checksListView.getSelectionModel().selectedItemProperty());
+ checksListView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
+ checksListView.setItems(checks);
+ checksListView.setCellFactory(listCellFactory);
+ selectedCheck.bind(checksListView.getSelectionModel().selectedItemProperty());
}
@FXML
- public void toggleSelectAll(ActionEvent event) {
- if (event.getSource() instanceof CheckBox c) {
- listPickIndicators.forEach( (task, pickProperty) -> pickProperty.set(c.isSelected()));
- }
+ public void selectAllChecks() {
+ checks.forEach(t -> t.chosenForExecutionProperty().set(true));
+ }
+
+ @FXML
+ public void deselectAllChecks() {
+ checks.forEach(t -> t.chosenForExecutionProperty().set(false));
}
@FXML
public void runSelectedChecks() {
- Preconditions.checkState(runningTask.get() == null);
- var batch = checksListView.getItems().filtered(item -> listPickIndicators.get(item).get());
- var batchService = new BatchService(batch);
- batchService.setExecutor(executorService);
- batchService.start();
- runningTask.set(batchService);
- showResultScreen.set(true);
- checksListView.getSelectionModel().select(batch.get(0));
- checksListView.setCellFactory(view -> new CheckListCell());
+ Preconditions.checkState(!mainRunStarted.get());
+ Preconditions.checkState(!somethingsRunning.get());
+ Preconditions.checkState(!chosenChecks.isEmpty());
+
+ checks.filtered(c -> !c.isChosenForExecution()).forEach(c -> c.setState(Check.CheckState.SKIPPED));
+ checkExecutor.executeBatch(chosenChecks);
+ checksListView.getSelectionModel().select(chosenChecks.get(0));
+ checksListView.refresh();
window.sizeToScene();
}
@FXML
- public synchronized void cancelCheck() {
- Preconditions.checkState(runningTask.get() != null);
- runningTask.get().cancel();
+ public synchronized void cancelRun() {
+ Preconditions.checkState(somethingsRunning.get());
+ checkExecutor.cancel();
}
@FXML
public void exportResults() {
try {
- reportWriter.writeReport(tasks);
+ reportWriter.writeReport(chosenChecks);
} catch (IOException e) {
LOG.error("Failed to write health check report.", e);
- errorComponenBuilder.get().cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
+ errorComponentBuilder.get().cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
}
}
/* Getter/Setter */
public boolean isRunning() {
- return running.getValue();
+ return somethingsRunning.getValue();
}
- public Binding runningProperty() {
- return running;
- }
-
- public boolean isFinished() {
- return finished.getValue();
- }
-
- public Binding finishedProperty() {
- return finished;
+ public BooleanBinding runningProperty() {
+ return somethingsRunning;
}
public boolean isAnyCheckSelected() {
@@ -160,21 +125,20 @@ public class CheckListController implements FxController {
return anyCheckSelected;
}
- public boolean getShowResultScreen() {
- return showResultScreen.get();
+ public boolean isMainRunStarted() {
+ return mainRunStarted.get();
}
- public BooleanProperty showResultScreenProperty() {
- return showResultScreen;
+ public BooleanBinding mainRunStartedProperty() {
+ return mainRunStarted;
}
- public int getNumberOfPickedChecks() {
- return numberOfPickedChecks.get();
+ public int getChosenTaskCount() {
+ return chosenTaskCount.getValue();
}
- public IntegerProperty numberOfPickedChecksProperty() {
- return numberOfPickedChecks;
+ public IntegerBinding chosenTaskCountProperty() {
+ return chosenTaskCount;
}
-
}
diff --git a/src/main/java/org/cryptomator/ui/health/CheckStateIconView.java b/src/main/java/org/cryptomator/ui/health/CheckStateIconView.java
new file mode 100644
index 000000000..4f1a35f7a
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/health/CheckStateIconView.java
@@ -0,0 +1,83 @@
+package org.cryptomator.ui.health;
+
+import com.tobiasdiez.easybind.EasyBind;
+import com.tobiasdiez.easybind.Subscription;
+import org.cryptomator.cryptofs.health.api.DiagnosticResult;
+import org.cryptomator.ui.common.Animations;
+import org.cryptomator.ui.common.AutoAnimator;
+import org.cryptomator.ui.controls.FontAwesome5Icon;
+import org.cryptomator.ui.controls.FontAwesome5IconView;
+
+import javafx.beans.binding.Bindings;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.value.ObservableObjectValue;
+import java.util.List;
+
+/**
+ * A {@link FontAwesome5IconView} that automatically sets the glyph depending on
+ * the {@link Check#stateProperty() state} and {@link Check#highestResultSeverityProperty() severity} of a HealthCheck.
+ */
+public class CheckStateIconView extends FontAwesome5IconView {
+
+ private final ObjectProperty check = new SimpleObjectProperty<>();
+ private final ObservableObjectValue state;
+ private final ObservableObjectValue severity;
+ private final List subscriptions;
+ private final AutoAnimator onRunningRotator;
+
+ public CheckStateIconView() {
+ this.state = EasyBind.wrapNullable(check).mapObservable(Check::stateProperty).asOrdinary();
+ this.severity = EasyBind.wrapNullable(check).mapObservable(Check::highestResultSeverityProperty).asOrdinary();
+ this.glyph.bind(Bindings.createObjectBinding(this::glyphForState, state, severity));
+ this.subscriptions = List.of( //
+ EasyBind.includeWhen(getStyleClass(), "glyph-icon-muted", Bindings.equal(state, Check.CheckState.SKIPPED).or(Bindings.equal(state, Check.CheckState.CANCELLED))), //
+ EasyBind.includeWhen(getStyleClass(), "glyph-icon-primary", Bindings.equal(severity, DiagnosticResult.Severity.GOOD)), //
+ EasyBind.includeWhen(getStyleClass(), "glyph-icon-orange", Bindings.equal(severity, DiagnosticResult.Severity.WARN).or(Bindings.equal(severity, DiagnosticResult.Severity.CRITICAL))), //
+ EasyBind.includeWhen(getStyleClass(), "glyph-icon-red", Bindings.equal(state, Check.CheckState.ERROR)) //
+ );
+ var animation = Animations.createDiscrete360Rotation(this);
+ this.onRunningRotator = AutoAnimator.animate(animation) //
+ .onCondition(Bindings.equal(state, Check.CheckState.RUNNING)) //
+ .afterStop(() -> setRotate(0)) //
+ .build();
+ }
+
+ private FontAwesome5Icon glyphForState() {
+ if (state.getValue() == null) {
+ return null;
+ }
+ return switch (state.getValue()) {
+ case RUNNABLE -> null;
+ case SKIPPED -> FontAwesome5Icon.FAST_FORWARD;
+ case SCHEDULED -> FontAwesome5Icon.CLOCK;
+ case RUNNING -> FontAwesome5Icon.SPINNER;
+ case ERROR -> FontAwesome5Icon.TIMES;
+ case CANCELLED -> FontAwesome5Icon.BAN;
+ case SUCCEEDED -> glyphIconForSeverity();
+ };
+ }
+
+ private FontAwesome5Icon glyphIconForSeverity() {
+ if (severity.getValue() == null) {
+ return null;
+ }
+ return switch (severity.getValue()) {
+ case GOOD, INFO -> FontAwesome5Icon.CHECK;
+ case WARN, CRITICAL -> FontAwesome5Icon.EXCLAMATION_TRIANGLE;
+ };
+ }
+
+ public ObjectProperty checkProperty() {
+ return check;
+ }
+
+ public void setCheck(Check c) {
+ check.set(c);
+ }
+
+ public Check getCheck() {
+ return check.get();
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/ui/health/HealthCheckComponent.java b/src/main/java/org/cryptomator/ui/health/HealthCheckComponent.java
index 48b16f694..f78e815c6 100644
--- a/src/main/java/org/cryptomator/ui/health/HealthCheckComponent.java
+++ b/src/main/java/org/cryptomator/ui/health/HealthCheckComponent.java
@@ -4,9 +4,11 @@ import dagger.BindsInstance;
import dagger.Lazy;
import dagger.Subcomponent;
import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.cryptofs.VaultConfig;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
+import javax.inject.Named;
import javafx.scene.Scene;
import javafx.stage.Stage;
@@ -14,15 +16,26 @@ import javafx.stage.Stage;
@Subcomponent(modules = {HealthCheckModule.class})
public interface HealthCheckComponent {
+ LoadUnverifiedConfigResult loadConfig();
+
@HealthCheckWindow
Stage window();
@FxmlScene(FxmlFile.HEALTH_START)
- Lazy scene();
+ Lazy startScene();
+
+ @FxmlScene(FxmlFile.HEALTH_START_FAIL)
+ Lazy failScene();
default Stage showHealthCheckWindow() {
Stage stage = window();
- stage.setScene(scene().get());
+ // 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.show();
return stage;
}
@@ -33,7 +46,11 @@ public interface HealthCheckComponent {
@BindsInstance
Builder vault(@HealthCheckWindow Vault vault);
+ @BindsInstance
+ Builder owner(@Named("healthCheckOwner") Stage owner);
+
HealthCheckComponent build();
}
+ record LoadUnverifiedConfigResult(VaultConfig.UnverifiedVaultConfig config, Throwable error) {}
}
diff --git a/src/main/java/org/cryptomator/ui/health/HealthCheckModule.java b/src/main/java/org/cryptomator/ui/health/HealthCheckModule.java
index e33a9f2f1..ad5ac6156 100644
--- a/src/main/java/org/cryptomator/ui/health/HealthCheckModule.java
+++ b/src/main/java/org/cryptomator/ui/health/HealthCheckModule.java
@@ -17,8 +17,8 @@ import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.keyloading.KeyLoadingComponent;
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
-import org.cryptomator.ui.mainwindow.MainWindow;
+import javax.inject.Named;
import javax.inject.Provider;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
@@ -26,8 +26,9 @@ import javafx.beans.value.ChangeListener;
import javafx.scene.Scene;
import javafx.stage.Modality;
import javafx.stage.Stage;
-import java.security.SecureRandom;
-import java.util.Collection;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
@@ -36,6 +37,18 @@ 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 provideMasterkeyRef() {
@@ -50,27 +63,20 @@ abstract class HealthCheckModule {
@Provides
@HealthCheckScoped
- static Collection provideAvailableHealthChecks() {
- return HealthCheck.allChecks();
- }
-
- @Provides
- @HealthCheckScoped
- static ObjectProperty provideSelectedHealthCheckTask() {
+ static ObjectProperty provideSelectedCheck() {
return new SimpleObjectProperty<>();
}
- /* Only inject with Lazy-Wrapper!*/
@Provides
@HealthCheckScoped
- static Collection provideAvailableHealthCheckTasks(Collection availableHealthChecks, @HealthCheckWindow Vault vault, AtomicReference masterkeyRef, AtomicReference vaultConfigRef, SecureRandom csprng, ResourceBundle resourceBundle) {
- return availableHealthChecks.stream().map(check -> new HealthCheckTask(vault.getPath(), vaultConfigRef.get(), masterkeyRef.get(), csprng, check, resourceBundle)).toList();
+ static List provideAvailableChecks() {
+ return HealthCheck.allChecks().stream().map(Check::new).toList();
}
@Provides
@HealthCheckWindow
@HealthCheckScoped
- static KeyLoadingStrategy provideKeyLoadingStrategy(KeyLoadingComponent.Builder compBuilder, @HealthCheckWindow Vault vault, @HealthCheckWindow Stage window) {
+ static KeyLoadingStrategy provideKeyLoadingStrategy(KeyLoadingComponent.Builder compBuilder, @HealthCheckWindow Vault vault, @Named("unlockWindow") Stage window ) {
return compBuilder.vault(vault).window(window).build().keyloadingStrategy();
}
@@ -81,15 +87,27 @@ abstract class HealthCheckModule {
return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle);
}
+ @Provides
+ @Named("unlockWindow")
+ @HealthCheckScoped
+ static Stage provideUnlockWindow (@HealthCheckWindow Stage window, @HealthCheckWindow Vault vault, StageFactory factory, ResourceBundle resourceBundle) {
+ Stage stage = factory.create();
+ stage.initModality(Modality.WINDOW_MODAL);
+ stage.initOwner(window);
+ stage.setTitle(String.format(resourceBundle.getString("unlock.title"), vault.getDisplayName()));
+ stage.setResizable(false);
+ return stage;
+ }
+
@Provides
@HealthCheckWindow
@HealthCheckScoped
- static Stage provideStage(StageFactory factory, @MainWindow Stage owner, ResourceBundle resourceBundle, ChangeListener showingListener) {
+ static Stage provideStage(StageFactory factory, @Named("healthCheckOwner") Stage owner, @HealthCheckWindow Vault vault, ChangeListener showingListener, ResourceBundle resourceBundle) {
Stage stage = factory.create();
- stage.setTitle(resourceBundle.getString("health.title"));
- stage.setResizable(true);
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner);
+ stage.setTitle(String.format(resourceBundle.getString("health.title"), vault.getDisplayName()));
+ stage.setResizable(true);
stage.showingProperty().addListener(showingListener); // bind masterkey lifecycle to window
return stage;
}
@@ -111,6 +129,13 @@ 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
@@ -123,6 +148,11 @@ 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)
@@ -138,4 +168,8 @@ abstract class HealthCheckModule {
@FxControllerKey(ResultListCellController.class)
abstract FxController bindResultListCellController(ResultListCellController controller);
+ @Binds
+ @IntoMap
+ @FxControllerKey(CheckListCellController.class)
+ abstract FxController bindCheckListCellController(CheckListCellController controller);
}
diff --git a/src/main/java/org/cryptomator/ui/health/HealthCheckTask.java b/src/main/java/org/cryptomator/ui/health/HealthCheckTask.java
deleted file mode 100644
index 7acbfc1c2..000000000
--- a/src/main/java/org/cryptomator/ui/health/HealthCheckTask.java
+++ /dev/null
@@ -1,108 +0,0 @@
-package org.cryptomator.ui.health;
-
-import org.cryptomator.cryptofs.VaultConfig;
-import org.cryptomator.cryptofs.health.api.DiagnosticResult;
-import org.cryptomator.cryptofs.health.api.HealthCheck;
-import org.cryptomator.cryptolib.api.Masterkey;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javafx.application.Platform;
-import javafx.beans.property.LongProperty;
-import javafx.beans.property.SimpleLongProperty;
-import javafx.collections.FXCollections;
-import javafx.collections.ObservableList;
-import javafx.concurrent.Task;
-import java.nio.file.Path;
-import java.security.SecureRandom;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.MissingResourceException;
-import java.util.Objects;
-import java.util.ResourceBundle;
-import java.util.concurrent.CancellationException;
-
-class HealthCheckTask extends Task {
-
- private static final Logger LOG = LoggerFactory.getLogger(HealthCheckTask.class);
-
- private final Path vaultPath;
- private final VaultConfig vaultConfig;
- private final Masterkey masterkey;
- private final SecureRandom csprng;
- private final HealthCheck check;
- private final ObservableList results;
- private final LongProperty durationInMillis;
-
- public HealthCheckTask(Path vaultPath, VaultConfig vaultConfig, Masterkey masterkey, SecureRandom csprng, HealthCheck check, ResourceBundle resourceBundle) {
- this.vaultPath = Objects.requireNonNull(vaultPath);
- this.vaultConfig = Objects.requireNonNull(vaultConfig);
- this.masterkey = Objects.requireNonNull(masterkey);
- this.csprng = Objects.requireNonNull(csprng);
- this.check = Objects.requireNonNull(check);
- this.results = FXCollections.observableArrayList();
- try {
- updateTitle(resourceBundle.getString("health." + check.identifier()));
- } catch (MissingResourceException e) {
- LOG.warn("Missing proper name for health check {}, falling back to default.", check.identifier());
- updateTitle(check.identifier());
- }
- this.durationInMillis = new SimpleLongProperty(-1);
- }
-
- @Override
- protected Void call() {
- Instant start = Instant.now();
- try (var masterkeyClone = masterkey.clone(); //
- var cryptor = vaultConfig.getCipherCombo().getCryptorProvider(csprng).withKey(masterkeyClone)) {
- check.check(vaultPath, vaultConfig, masterkeyClone, cryptor, result -> {
- if (isCancelled()) {
- throw new CancellationException();
- }
- // FIXME: slowdown for demonstration purposes only:
- try {
- Thread.sleep(2000);
- } catch (InterruptedException e) {
- if (isCancelled()) {
- return;
- } else {
- Thread.currentThread().interrupt();
- throw new RuntimeException(e);
- }
- }
- Platform.runLater(() -> results.add(result));
- });
- }
- Platform.runLater(() ->durationInMillis.set(Duration.between(start, Instant.now()).toMillis()));
- return null;
- }
-
- @Override
- protected void scheduled() {
- LOG.info("starting {}", check.identifier());
- }
-
- @Override
- protected void done() {
- LOG.info("finished {}", check.identifier());
- }
-
- /* Getter */
-
- public ObservableList results() {
- return results;
- }
-
- public HealthCheck getCheck() {
- return check;
- }
-
- public LongProperty durationInMillisProperty() {
- return durationInMillis;
- }
-
- public long getDurationInMillis() {
- return durationInMillis.get();
- }
-
-}
diff --git a/src/main/java/org/cryptomator/ui/health/ReportWriter.java b/src/main/java/org/cryptomator/ui/health/ReportWriter.java
index fb74cbd51..18b785e91 100644
--- a/src/main/java/org/cryptomator/ui/health/ReportWriter.java
+++ b/src/main/java/org/cryptomator/ui/health/ReportWriter.java
@@ -1,15 +1,12 @@
package org.cryptomator.ui.health;
-import org.apache.commons.lang3.exception.ExceptionUtils;
+import com.google.common.base.Throwables;
import org.cryptomator.common.Environment;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.VaultConfig;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.application.Application;
-import javafx.concurrent.Worker;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
@@ -28,11 +25,10 @@ import java.util.stream.Collectors;
@HealthCheckScoped
public class ReportWriter {
- private static final Logger LOG = LoggerFactory.getLogger(ReportWriter.class);
private static final String REPORT_HEADER = """
- **************************************
- * Cryptomator Vault Health Report *
- **************************************
+ *******************************************
+ * Cryptomator Vault Health Report *
+ *******************************************
Analyzed vault: %s (Current name "%s")
Vault storage path: %s
""";
@@ -58,38 +54,35 @@ public class ReportWriter {
this.exportDestination = env.getLogDir().orElse(Path.of(System.getProperty("user.home"))).resolve("healthReport_" + vault.getDisplayName() + "_" + TIME_STAMP.format(Instant.now()) + ".log");
}
- protected void writeReport(Collection tasks) throws IOException {
+ protected void writeReport(Collection performedChecks) throws IOException {
try (var out = Files.newOutputStream(exportDestination, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); //
var writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8))) {
writer.write(REPORT_HEADER.formatted(vaultConfig.getId(), vault.getDisplayName(), vault.getPath()));
- for (var task : tasks) {
- if (task.getState() == Worker.State.READY) {
- LOG.debug("Skipping not performed check {}.", task.getCheck().identifier());
- continue;
- }
- writer.write(REPORT_CHECK_HEADER.formatted(task.getCheck().identifier()));
- switch (task.getState()) {
+ for (var check : performedChecks) {
+ writer.write(REPORT_CHECK_HEADER.formatted(check.getHealthCheck().name()));
+ switch (check.getState()) {
case SUCCEEDED -> {
writer.write("STATUS: SUCCESS\nRESULTS:\n");
- for (var result : task.results()) {
- writer.write(REPORT_CHECK_RESULT.formatted(result.getServerity(), result.toString()));
+ for (var result : check.getResults()) {
+ writer.write(REPORT_CHECK_RESULT.formatted(result.diagnosis().getSeverity(), result.getDescription()));
}
}
case CANCELLED -> writer.write("STATUS: CANCELED\n");
- case FAILED -> {
- writer.write("STATUS: FAILED\nREASON:\n" + task.getCheck().identifier());
- writer.write(prepareFailureMsg(task));
+ case ERROR -> {
+ writer.write("STATUS: FAILED\nREASON:\n");
+ writer.write(prepareFailureMsg(check));
}
- case RUNNING, SCHEDULED -> throw new IllegalStateException("Checks are still running.");
+ case RUNNABLE, RUNNING, SCHEDULED -> throw new IllegalStateException("Checks are still running.");
+ case SKIPPED -> {} //noop
}
}
}
reveal();
}
- private String prepareFailureMsg(HealthCheckTask task) {
- if (task.getException() != null) {
- return ExceptionUtils.getStackTrace(task.getException()) //
+ private String prepareFailureMsg(Check check) {
+ if (check.getError() != null) {
+ return Throwables.getStackTraceAsString(check.getError()) //
.lines() //
.map(line -> "\t\t" + line + "\n") //
.collect(Collectors.joining());
diff --git a/src/main/java/org/cryptomator/ui/health/Result.java b/src/main/java/org/cryptomator/ui/health/Result.java
new file mode 100644
index 000000000..8327a1130
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/health/Result.java
@@ -0,0 +1,40 @@
+package org.cryptomator.ui.health;
+
+import org.cryptomator.cryptofs.health.api.DiagnosticResult;
+
+import javafx.beans.Observable;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+
+record Result(DiagnosticResult diagnosis, ObjectProperty fixState) {
+
+ enum FixState {
+ NOT_FIXABLE,
+ FIXABLE,
+ FIXING,
+ FIXED,
+ FIX_FAILED
+ }
+
+ public static Result create(DiagnosticResult diagnosis) {
+ FixState initialState = diagnosis.getSeverity() == DiagnosticResult.Severity.WARN ? FixState.FIXABLE : FixState.NOT_FIXABLE;
+ return new Result(diagnosis, new SimpleObjectProperty<>(initialState));
+ }
+
+ public Observable[] observables() {
+ return new Observable[]{fixState};
+ }
+
+ public String getDescription() {
+ return diagnosis.toString();
+ }
+
+ public FixState getState() {
+ return fixState.get();
+ }
+
+ public void setState(FixState state) {
+ this.fixState.set(state);
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/ui/health/ResultFixApplier.java b/src/main/java/org/cryptomator/ui/health/ResultFixApplier.java
index 9a639e8a0..841a8f5c4 100644
--- a/src/main/java/org/cryptomator/ui/health/ResultFixApplier.java
+++ b/src/main/java/org/cryptomator/ui/health/ResultFixApplier.java
@@ -4,25 +4,30 @@ import com.google.common.base.Preconditions;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.VaultConfig;
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
+import org.cryptomator.cryptolib.api.CryptorProvider;
import org.cryptomator.cryptolib.api.Masterkey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
-import javafx.scene.control.Alert;
+import javafx.application.Platform;
import java.nio.file.Path;
import java.security.SecureRandom;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;
@HealthCheckScoped
class ResultFixApplier {
- private static final Logger LOG = LoggerFactory.getLogger(ResultFixApplier.class);
-
private final Path vaultPath;
private final SecureRandom csprng;
private final Masterkey masterkey;
private final VaultConfig vaultConfig;
+ private final ExecutorService sequentialExecutor;
@Inject
public ResultFixApplier(@HealthCheckWindow Vault vault, AtomicReference masterkeyRef, AtomicReference vaultConfigRef, SecureRandom csprng) {
@@ -30,18 +35,32 @@ class ResultFixApplier {
this.masterkey = masterkeyRef.get();
this.vaultConfig = vaultConfigRef.get();
this.csprng = csprng;
+ this.sequentialExecutor = Executors.newSingleThreadExecutor();
}
- public void fix(DiagnosticResult result) {
- Preconditions.checkArgument(result.getServerity() == DiagnosticResult.Severity.WARN, "Unfixable result");
+ public CompletionStage fix(Result result) {
+ Preconditions.checkArgument(result.getState() == Result.FixState.FIXABLE);
+ result.setState(Result.FixState.FIXING);
+ return CompletableFuture.runAsync(() -> fix(result.diagnosis()), sequentialExecutor)
+ .whenCompleteAsync((unused, throwable) -> {
+ var fixed = throwable == null ? Result.FixState.FIXED : Result.FixState.FIX_FAILED;
+ result.setState(fixed);
+ }, Platform::runLater);
+ }
+
+ public void fix(DiagnosticResult diagnosis) {
+ Preconditions.checkArgument(diagnosis.getSeverity() == DiagnosticResult.Severity.WARN, "Unfixable result");
try (var masterkeyClone = masterkey.clone(); //
- var cryptor = vaultConfig.getCipherCombo().getCryptorProvider(csprng).withKey(masterkeyClone)) {
- result.fix(vaultPath, vaultConfig, masterkeyClone, cryptor);
+ var cryptor = CryptorProvider.forScheme(vaultConfig.getCipherCombo()).provide(masterkeyClone, csprng)) {
+ diagnosis.fix(vaultPath, vaultConfig, masterkeyClone, cryptor);
} catch (Exception e) {
- LOG.error("Failed to apply fix", e);
- Alert alert = new Alert(Alert.AlertType.ERROR, e.getMessage());
- alert.showAndWait();
- //TODO: real error/not supported handling
+ throw new FixFailedException(e);
+ }
+ }
+
+ public static class FixFailedException extends CompletionException {
+ private FixFailedException(Throwable cause) {
+ super(cause);
}
}
}
diff --git a/src/main/java/org/cryptomator/ui/health/ResultListCellController.java b/src/main/java/org/cryptomator/ui/health/ResultListCellController.java
index f2bca059a..59ee2fa67 100644
--- a/src/main/java/org/cryptomator/ui/health/ResultListCellController.java
+++ b/src/main/java/org/cryptomator/ui/health/ResultListCellController.java
@@ -1,92 +1,212 @@
package org.cryptomator.ui.health;
import com.tobiasdiez.easybind.EasyBind;
+import com.tobiasdiez.easybind.Subscription;
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
+import org.cryptomator.ui.common.Animations;
+import org.cryptomator.ui.common.AutoAnimator;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.controls.FontAwesome5Icon;
import org.cryptomator.ui.controls.FontAwesome5IconView;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import javax.inject.Inject;
+import javafx.application.Platform;
import javafx.beans.binding.Binding;
+import javafx.beans.binding.Bindings;
+import javafx.beans.binding.BooleanBinding;
+import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
-import javafx.beans.value.ObservableValue;
+import javafx.beans.value.ObservableObjectValue;
import javafx.fxml.FXML;
-import javafx.scene.control.Button;
+import javafx.scene.control.Tooltip;
+import javafx.util.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.ResourceBundle;
// unscoped because each cell needs its own controller
public class ResultListCellController implements FxController {
- private final ResultFixApplier fixApplier;
- private final ObjectProperty result;
- private final Binding description;
+ private static final FontAwesome5Icon INFO_ICON = FontAwesome5Icon.INFO_CIRCLE;
+ private static final FontAwesome5Icon GOOD_ICON = FontAwesome5Icon.CHECK;
+ private static final FontAwesome5Icon WARN_ICON = FontAwesome5Icon.EXCLAMATION_TRIANGLE;
+ private static final FontAwesome5Icon CRIT_ICON = FontAwesome5Icon.TIMES;
- public FontAwesome5IconView iconView;
- public Button actionButton;
+ private final Logger LOG = LoggerFactory.getLogger(ResultListCellController.class);
+
+ private final ObjectProperty result;
+ private final ObservableObjectValue severity;
+ private final Binding description;
+ private final ResultFixApplier fixApplier;
+ private final ObservableObjectValue fixState;
+ private final ObjectBinding severityGlyph;
+ private final ObjectBinding fixGlyph;
+ private final BooleanBinding fixable;
+ private final BooleanBinding fixing;
+ private final BooleanBinding fixed;
+ private final BooleanBinding fixFailed;
+ private final BooleanBinding fixRunningOrDone;
+ private final List subscriptions;
+ private final Tooltip fixSuccess;
+ private final Tooltip fixFail;
+
+ private AutoAnimator fixRunningRotator;
+
+ /* FXML */
+ public FontAwesome5IconView severityView;
+ public FontAwesome5IconView fixView;
@Inject
- public ResultListCellController(ResultFixApplier fixApplier) {
+ public ResultListCellController(ResultFixApplier fixApplier, ResourceBundle resourceBundle) {
this.result = new SimpleObjectProperty<>(null);
- this.description = EasyBind.wrapNullable(result).map(DiagnosticResult::toString).orElse("");
+ this.severity = EasyBind.wrapNullable(result).map(r -> r.diagnosis().getSeverity()).asOrdinary();
+ this.description = EasyBind.wrapNullable(result).map(Result::getDescription).orElse("");
this.fixApplier = fixApplier;
- result.addListener(this::updateCellContent);
- }
-
- private void updateCellContent(ObservableValue extends DiagnosticResult> observable, DiagnosticResult oldVal, DiagnosticResult newVal) {
- iconView.getStyleClass().clear();
- actionButton.setVisible(false);
- //TODO: see comment in case WARN
- actionButton.setManaged(false);
- switch (newVal.getServerity()) {
- case INFO -> {
- iconView.setGlyph(FontAwesome5Icon.INFO_CIRCLE);
- iconView.getStyleClass().add("glyph-icon-muted");
- }
- case GOOD -> {
- iconView.setGlyph(FontAwesome5Icon.CHECK);
- iconView.getStyleClass().add("glyph-icon-primary");
- }
- case WARN -> {
- iconView.setGlyph(FontAwesome5Icon.EXCLAMATION_TRIANGLE);
- iconView.getStyleClass().add("glyph-icon-orange");
- //TODO: Neither is any fix implemented, nor it is ensured, that only fix is executed at a time with good ui indication
- // before both are not fix, do not show the button
- //actionButton.setVisible(true);
- }
- case CRITICAL -> {
- iconView.setGlyph(FontAwesome5Icon.TIMES);
- iconView.getStyleClass().add("glyph-icon-red");
- }
- }
+ this.fixState = EasyBind.wrapNullable(result).mapObservable(Result::fixState).asOrdinary();
+ this.severityGlyph = Bindings.createObjectBinding(this::getSeverityGlyph, result);
+ this.fixGlyph = Bindings.createObjectBinding(this::getFixGlyph, fixState);
+ this.fixable = Bindings.createBooleanBinding(this::isFixable, fixState);
+ this.fixing = Bindings.createBooleanBinding(this::isFixing, fixState);
+ this.fixed = Bindings.createBooleanBinding(this::isFixed, fixState);
+ this.fixFailed = Bindings.createBooleanBinding(this::isFixFailed, fixState);
+ this.fixRunningOrDone = fixing.or(fixed).or(fixFailed);
+ this.subscriptions = new ArrayList<>();
+ this.fixSuccess = new Tooltip(resourceBundle.getString("health.fix.successTip"));
+ this.fixFail = new Tooltip(resourceBundle.getString("health.fix.failTip"));
+ fixSuccess.setShowDelay(Duration.millis(100));
+ fixFail.setShowDelay(Duration.millis(100));
}
@FXML
- public void runResultAction() {
- final var realResult = result.get();
- if (realResult != null) {
- fixApplier.fix(realResult);
+ public void initialize() {
+ // see getGlyph() for relevant glyphs:
+ subscriptions.addAll(List.of(EasyBind.includeWhen(severityView.getStyleClass(), "glyph-icon-muted", Bindings.equal(severity, DiagnosticResult.Severity.INFO)), //
+ EasyBind.includeWhen(severityView.getStyleClass(), "glyph-icon-primary", Bindings.equal(severity, DiagnosticResult.Severity.GOOD)), //
+ EasyBind.includeWhen(severityView.getStyleClass(), "glyph-icon-orange", Bindings.equal(severity, DiagnosticResult.Severity.WARN)), //
+ EasyBind.includeWhen(severityView.getStyleClass(), "glyph-icon-red", Bindings.equal(severity, DiagnosticResult.Severity.CRITICAL)) //
+ ));
+ var animation = Animations.createDiscrete360Rotation(fixView);
+ this.fixRunningRotator = AutoAnimator.animate(animation) //
+ .onCondition(Bindings.equal(fixState, Result.FixState.FIXING)) //
+ .afterStop(() -> fixView.setRotate(0)) //
+ .build();
+ }
+
+ @FXML
+ public void fix() {
+ Result r = result.get();
+ if (r != null) {
+ fixApplier.fix(r).whenCompleteAsync(this::fixFinished, Platform::runLater);
}
}
+
+ private void fixFinished(Void unused, Throwable exception) {
+ if (exception != null) {
+ LOG.error("Failed to apply fix", exception);
+ Tooltip.install(fixView, fixFail);
+ } else {
+ Tooltip.install(fixView, fixSuccess);
+ }
+ }
+
+
/* Getter & Setter */
-
- public DiagnosticResult getResult() {
+ public Result getResult() {
return result.get();
}
- public void setResult(DiagnosticResult result) {
+ public void setResult(Result result) {
this.result.set(result);
}
- public ObjectProperty resultProperty() {
+ public ObjectProperty resultProperty() {
return result;
}
+ public Binding descriptionProperty() {
+ return description;
+ }
+
public String getDescription() {
return description.getValue();
}
- public Binding descriptionProperty() {
- return description;
+ public ObjectBinding severityGlyphProperty() {
+ return severityGlyph;
}
+
+ public FontAwesome5Icon getSeverityGlyph() {
+ var r = result.get();
+ if (r == null) {
+ return null;
+ }
+ return switch (r.diagnosis().getSeverity()) {
+ case INFO -> INFO_ICON;
+ case GOOD -> GOOD_ICON;
+ case WARN -> WARN_ICON;
+ case CRITICAL -> CRIT_ICON;
+ };
+ }
+
+ public ObjectBinding fixGlyphProperty() {
+ return fixGlyph;
+ }
+
+ public FontAwesome5Icon getFixGlyph() {
+ if (fixState.getValue() == null) {
+ return null;
+ }
+ return switch (fixState.getValue()) {
+ case NOT_FIXABLE, FIXABLE -> null;
+ case FIXING -> FontAwesome5Icon.SPINNER;
+ case FIXED -> FontAwesome5Icon.CHECK;
+ case FIX_FAILED -> FontAwesome5Icon.TIMES;
+ };
+ }
+
+ public BooleanBinding fixableProperty() {
+ return fixable;
+ }
+
+ public boolean isFixable() {
+ return Result.FixState.FIXABLE.equals(fixState.get());
+ }
+
+ public BooleanBinding fixingProperty() {
+ return fixing;
+ }
+
+ public boolean isFixing() {
+ return Result.FixState.FIXING.equals(fixState.get());
+ }
+
+ public BooleanBinding fixedProperty() {
+ return fixed;
+ }
+
+ public boolean isFixed() {
+ return Result.FixState.FIXED.equals(fixState.get());
+ }
+
+ public BooleanBinding fixFailedProperty() {
+ return fixFailed;
+ }
+
+ public Boolean isFixFailed() {
+ return Result.FixState.FIX_FAILED.equals(fixState.get());
+ }
+
+ public BooleanBinding fixRunningOrDoneProperty() {
+ return fixRunningOrDone;
+ }
+
+ public boolean isFixRunningOrDone() {
+ return fixRunningOrDone.get();
+ }
+
+
}
diff --git a/src/main/java/org/cryptomator/ui/health/ResultListCellFactory.java b/src/main/java/org/cryptomator/ui/health/ResultListCellFactory.java
index 7acada487..86c793bf7 100644
--- a/src/main/java/org/cryptomator/ui/health/ResultListCellFactory.java
+++ b/src/main/java/org/cryptomator/ui/health/ResultListCellFactory.java
@@ -1,7 +1,6 @@
package org.cryptomator.ui.health;
-import org.cryptomator.cryptofs.health.api.DiagnosticResult;
import org.cryptomator.ui.common.FxmlLoaderFactory;
import javax.inject.Inject;
@@ -15,7 +14,7 @@ import java.io.IOException;
import java.io.UncheckedIOException;
@HealthCheckScoped
-public class ResultListCellFactory implements Callback, ListCell> {
+public class ResultListCellFactory implements Callback, ListCell> {
private final FxmlLoaderFactory fxmlLoaders;
@@ -25,7 +24,7 @@ public class ResultListCellFactory implements Callback call(ListView param) {
+ public ListCell call(ListView param) {
try {
FXMLLoader fxmlLoader = fxmlLoaders.load("/fxml/health_result_listcell.fxml");
return new ResultListCellFactory.Cell(fxmlLoader.getRoot(), fxmlLoader.getController());
@@ -34,7 +33,7 @@ public class ResultListCellFactory implements Callback {
+ private static class Cell extends ListCell {
private final Parent node;
private final ResultListCellController controller;
@@ -45,7 +44,7 @@ public class ResultListCellFactory implements Callback unverifiedVaultConfig;
+ private final Stage unlockWindow;
+ private final ObjectProperty unverifiedVaultConfig;
private final KeyLoadingStrategy keyLoadingStrategy;
private final ExecutorService executor;
private final AtomicReference masterkeyRef;
@@ -40,29 +43,18 @@ public class StartController implements FxController {
private final Lazy checkScene;
private final Lazy errorComponent;
- /* FXML */
-
@Inject
- public StartController(@HealthCheckWindow Vault vault, @HealthCheckWindow Stage window, @HealthCheckWindow KeyLoadingStrategy keyLoadingStrategy, ExecutorService executor, AtomicReference masterkeyRef, AtomicReference vaultConfigRef, @FxmlScene(FxmlFile.HEALTH_CHECK_LIST) Lazy checkScene, Lazy errorComponent) {
+ public StartController(@HealthCheckWindow Stage window, HealthCheckComponent.LoadUnverifiedConfigResult configLoadResult, @HealthCheckWindow KeyLoadingStrategy keyLoadingStrategy, ExecutorService executor, AtomicReference masterkeyRef, AtomicReference vaultConfigRef, @FxmlScene(FxmlFile.HEALTH_CHECK_LIST) Lazy checkScene, Lazy errorComponent, @Named("unlockWindow") Stage unlockWindow) {
+ Preconditions.checkNotNull(configLoadResult.config());
this.window = window;
+ this.unlockWindow = unlockWindow;
+ this.unverifiedVaultConfig = new SimpleObjectProperty<>(configLoadResult.config());
this.keyLoadingStrategy = keyLoadingStrategy;
this.executor = executor;
this.masterkeyRef = masterkeyRef;
this.vaultConfigRef = vaultConfigRef;
this.checkScene = checkScene;
this.errorComponent = errorComponent;
-
- //TODO: this is ugly
- //idea: delay the loading of the vault config and show a spinner (something like "check/load config") and react to the result of the loading
- //or: load vault config in a previous step to see if it is loadable.
- VaultConfig.UnverifiedVaultConfig tmp;
- try {
- tmp = vault.getUnverifiedVaultConfig();
- } catch (IOException e) {
- e.printStackTrace();
- tmp = null;
- }
- this.unverifiedVaultConfig = Optional.ofNullable(tmp);
}
@FXML
@@ -74,41 +66,45 @@ public class StartController implements FxController {
@FXML
public void next() {
LOG.trace("StartController.next()");
- executor.submit(this::loadKey);
+ CompletableFuture.runAsync(this::loadKey, executor).whenCompleteAsync(this::loadedKey, Platform::runLater);
}
private void loadKey() {
assert !Platform.isFxApplicationThread();
- assert unverifiedVaultConfig.isPresent();
- try (var masterkey = keyLoadingStrategy.loadKey(unverifiedVaultConfig.orElseThrow().getKeyId())) {
- var unverifiedCfg = unverifiedVaultConfig.get();
+ assert unverifiedVaultConfig.get() != null;
+ try {
+ keyLoadingStrategy.use(this::verifyVaultConfig);
+ } catch (VaultConfigLoadException | UnlockCancelledException e) {
+ throw new LoadingFailedException(e);
+ }
+ }
+
+ private void verifyVaultConfig(KeyLoadingStrategy keyLoadingStrategy) throws VaultConfigLoadException {
+ var unverifiedCfg = unverifiedVaultConfig.get();
+ try (var masterkey = keyLoadingStrategy.loadKey(unverifiedCfg.getKeyId())) {
var verifiedCfg = unverifiedCfg.verify(masterkey.getEncoded(), unverifiedCfg.allegedVaultVersion());
vaultConfigRef.set(verifiedCfg);
var old = masterkeyRef.getAndSet(masterkey.clone());
if (old != null) {
old.destroy();
}
- Platform.runLater(this::loadedKey);
- } catch (MasterkeyLoadingFailedException e) {
- if (keyLoadingStrategy.recoverFromException(e)) {
- // retry
- loadKey();
- } else {
- Platform.runLater(() -> loadingKeyFailed(e));
- }
- } catch (VaultKeyInvalidException e) {
- Platform.runLater(() -> loadingKeyFailed(e));
- } catch (VaultConfigLoadException e) {
- Platform.runLater(() -> loadingKeyFailed(e));
}
}
- private void loadedKey() {
- LOG.debug("Loaded valid key");
- window.setScene(checkScene.get());
+ private void loadedKey(Void unused, Throwable exception) {
+ assert Platform.isFxApplicationThread();
+ if (exception instanceof LoadingFailedException) {
+ loadingKeyFailed(exception.getCause());
+ } else if (exception != null) {
+ loadingKeyFailed(exception);
+ } else {
+ LOG.debug("Loaded valid key");
+ unlockWindow.close();
+ window.setScene(checkScene.get());
+ }
}
- private void loadingKeyFailed(Exception e) {
+ private void loadingKeyFailed(Throwable e) {
if (e instanceof UnlockCancelledException) {
// ok
} else if (e instanceof VaultKeyInvalidException) {
@@ -120,8 +116,12 @@ public class StartController implements FxController {
}
}
- public boolean isInvalidConfig() {
- return unverifiedVaultConfig.isEmpty();
- }
+ /* internal types */
+ private static class LoadingFailedException extends CompletionException {
+
+ LoadingFailedException(Throwable cause) {
+ super(cause);
+ }
+ }
}
diff --git a/src/main/java/org/cryptomator/ui/health/StartFailController.java b/src/main/java/org/cryptomator/ui/health/StartFailController.java
new file mode 100644
index 000000000..826766026
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/health/StartFailController.java
@@ -0,0 +1,79 @@
+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 loadError;
+ private final ObjectProperty 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 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();
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingStrategy.java b/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingStrategy.java
index ed8ca0540..614247ebc 100644
--- a/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingStrategy.java
+++ b/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingStrategy.java
@@ -3,6 +3,8 @@ package org.cryptomator.ui.keyloading;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.api.MasterkeyLoader;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.net.URI;
@@ -12,6 +14,8 @@ import java.net.URI;
@FunctionalInterface
public interface KeyLoadingStrategy extends MasterkeyLoader {
+ Logger LOG = LoggerFactory.getLogger(KeyLoadingStrategy.class);
+
/**
* Loads a master key. This might be a long-running operation, as it may require user input or expensive computations.
*