Browse Source

Merge pull request #1618 from cryptomator/feature/#1508-observable-mounts

Closes #1508
Armin Schrenk 4 years ago
parent
commit
0144cbb99f
22 changed files with 327 additions and 179 deletions
  1. 6 8
      main/commons/src/main/java/org/cryptomator/common/vaults/DokanyVolume.java
  2. 9 11
      main/commons/src/main/java/org/cryptomator/common/vaults/FuseVolume.java
  3. 12 0
      main/commons/src/main/java/org/cryptomator/common/vaults/LockNotCompletedException.java
  4. 36 19
      main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java
  5. 1 1
      main/commons/src/main/java/org/cryptomator/common/vaults/VaultComponent.java
  6. 13 13
      main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java
  7. 0 6
      main/commons/src/main/java/org/cryptomator/common/vaults/VaultModule.java
  8. 128 21
      main/commons/src/main/java/org/cryptomator/common/vaults/VaultState.java
  9. 5 5
      main/commons/src/main/java/org/cryptomator/common/vaults/VaultStats.java
  10. 3 1
      main/commons/src/main/java/org/cryptomator/common/vaults/Volume.java
  11. 8 3
      main/commons/src/main/java/org/cryptomator/common/vaults/WebDavVolume.java
  12. 2 2
      main/pom.xml
  13. 10 4
      main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java
  14. 21 9
      main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java
  15. 6 1
      main/ui/src/main/java/org/cryptomator/ui/launcher/AppLifecycleListener.java
  16. 18 14
      main/ui/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java
  17. 2 1
      main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailController.java
  18. 2 1
      main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultListCellController.java
  19. 2 9
      main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultListContextMenuController.java
  20. 12 8
      main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java
  21. 2 2
      main/ui/src/main/java/org/cryptomator/ui/stats/VaultStatisticsModule.java
  22. 29 40
      main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java

+ 6 - 8
main/commons/src/main/java/org/cryptomator/common/vaults/DokanyVolume.java

@@ -5,15 +5,15 @@ import org.cryptomator.common.mountpoint.MountPointChooser;
 import org.cryptomator.common.settings.VaultSettings;
 import org.cryptomator.common.settings.VolumeImpl;
 import org.cryptomator.cryptofs.CryptoFileSystem;
+import org.cryptomator.frontend.dokany.DokanyMountFailedException;
 import org.cryptomator.frontend.dokany.Mount;
 import org.cryptomator.frontend.dokany.MountFactory;
-import org.cryptomator.frontend.dokany.MountFailedException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import javax.inject.Inject;
 import javax.inject.Named;
-import java.util.concurrent.ExecutorService;
+import java.util.function.Consumer;
 
 public class DokanyVolume extends AbstractVolume {
 
@@ -22,15 +22,13 @@ public class DokanyVolume extends AbstractVolume {
 	private static final String FS_TYPE_NAME = "CryptomatorFS";
 
 	private final VaultSettings vaultSettings;
-	private final MountFactory mountFactory;
 
 	private Mount mount;
 
 	@Inject
-	public DokanyVolume(VaultSettings vaultSettings, ExecutorService executorService, @Named("orderedMountPointChoosers") Iterable<MountPointChooser> choosers) {
+	public DokanyVolume(VaultSettings vaultSettings, @Named("orderedMountPointChoosers") Iterable<MountPointChooser> choosers) {
 		super(choosers);
 		this.vaultSettings = vaultSettings;
-		this.mountFactory = new MountFactory(executorService);
 	}
 
 	@Override
@@ -39,11 +37,11 @@ public class DokanyVolume extends AbstractVolume {
 	}
 
 	@Override
-	public void mount(CryptoFileSystem fs, String mountFlags) throws InvalidMountPointException, VolumeException {
+	public void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws InvalidMountPointException, VolumeException {
 		this.mountPoint = determineMountPoint();
 		try {
-			this.mount = mountFactory.mount(fs.getPath("/"), mountPoint, vaultSettings.mountName().get(), FS_TYPE_NAME, mountFlags.strip());
-		} catch (MountFailedException e) {
+			this.mount = MountFactory.mount(fs.getPath("/"), mountPoint, vaultSettings.mountName().get(), FS_TYPE_NAME, mountFlags.strip(), onExitAction);
+		} catch (DokanyMountFailedException e) {
 			if (vaultSettings.getCustomMountPath().isPresent()) {
 				LOG.warn("Failed to mount vault into {}. Is this directory currently accessed by another process (e.g. Windows Explorer)?", mountPoint);
 			}

+ 9 - 11
main/commons/src/main/java/org/cryptomator/common/vaults/FuseVolume.java

@@ -6,8 +6,8 @@ import org.cryptomator.common.mountpoint.InvalidMountPointException;
 import org.cryptomator.common.mountpoint.MountPointChooser;
 import org.cryptomator.common.settings.VolumeImpl;
 import org.cryptomator.cryptofs.CryptoFileSystem;
-import org.cryptomator.frontend.fuse.mount.CommandFailedException;
 import org.cryptomator.frontend.fuse.mount.EnvironmentVariables;
+import org.cryptomator.frontend.fuse.mount.FuseMountException;
 import org.cryptomator.frontend.fuse.mount.FuseMountFactory;
 import org.cryptomator.frontend.fuse.mount.FuseNotSupportedException;
 import org.cryptomator.frontend.fuse.mount.Mount;
@@ -20,6 +20,7 @@ import javax.inject.Named;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.function.Consumer;
 import java.util.regex.Pattern;
 
 public class FuseVolume extends AbstractVolume {
@@ -35,13 +36,12 @@ public class FuseVolume extends AbstractVolume {
 	}
 
 	@Override
-	public void mount(CryptoFileSystem fs, String mountFlags) throws InvalidMountPointException, VolumeException {
+	public void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws InvalidMountPointException, VolumeException {
 		this.mountPoint = determineMountPoint();
-
-		mount(fs.getPath("/"), mountFlags);
+		mount(fs.getPath("/"), mountFlags, onExitAction);
 	}
 
-	private void mount(Path root, String mountFlags) throws VolumeException {
+	private void mount(Path root, String mountFlags, Consumer<Throwable> onExitAction) throws VolumeException {
 		try {
 			Mounter mounter = FuseMountFactory.getMounter();
 			EnvironmentVariables envVars = EnvironmentVariables.create() //
@@ -49,8 +49,8 @@ public class FuseVolume extends AbstractVolume {
 					.withMountPoint(mountPoint) //
 					.withFileNameTranscoder(mounter.defaultFileNameTranscoder()) //
 					.build();
-			this.mount = mounter.mount(root, envVars);
-		} catch (CommandFailedException | FuseNotSupportedException e) {
+			this.mount = mounter.mount(root, envVars, onExitAction);
+		} catch ( FuseMountException | FuseNotSupportedException e) {
 			throw new VolumeException("Unable to mount Filesystem", e);
 		}
 	}
@@ -91,8 +91,7 @@ public class FuseVolume extends AbstractVolume {
 	public synchronized void unmountForced() throws VolumeException {
 		try {
 			mount.unmountForced();
-			mount.close();
-		} catch (CommandFailedException e) {
+		} catch (FuseMountException e) {
 			throw new VolumeException(e);
 		}
 		cleanupMountPoint();
@@ -102,8 +101,7 @@ public class FuseVolume extends AbstractVolume {
 	public synchronized void unmount() throws VolumeException {
 		try {
 			mount.unmount();
-			mount.close();
-		} catch (CommandFailedException e) {
+		} catch (FuseMountException e) {
 			throw new VolumeException(e);
 		}
 		cleanupMountPoint();

+ 12 - 0
main/commons/src/main/java/org/cryptomator/common/vaults/LockNotCompletedException.java

@@ -0,0 +1,12 @@
+package org.cryptomator.common.vaults;
+
+public class LockNotCompletedException extends Exception {
+
+	public LockNotCompletedException(String reason) {
+		super(reason);
+	}
+
+	public LockNotCompletedException(Throwable cause) {
+		super(cause);
+	}
+}

+ 36 - 19
main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java

@@ -42,6 +42,7 @@ import java.util.EnumSet;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
 
 import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
@@ -56,7 +57,7 @@ public class Vault {
 	private final Provider<Volume> volumeProvider;
 	private final StringBinding defaultMountFlags;
 	private final AtomicReference<CryptoFileSystem> cryptoFileSystem;
-	private final ObjectProperty<VaultState> state;
+	private final VaultState state;
 	private final ObjectProperty<Exception> lastKnownException;
 	private final VaultStats stats;
 	private final StringBinding displayName;
@@ -74,7 +75,7 @@ public class Vault {
 	private volatile Volume volume;
 
 	@Inject
-	Vault(VaultSettings vaultSettings, Provider<Volume> volumeProvider, @DefaultMountFlags StringBinding defaultMountFlags, AtomicReference<CryptoFileSystem> cryptoFileSystem, ObjectProperty<VaultState> state, @Named("lastKnownException") ObjectProperty<Exception> lastKnownException, VaultStats stats) {
+	Vault(VaultSettings vaultSettings, Provider<Volume> volumeProvider, @DefaultMountFlags StringBinding defaultMountFlags, AtomicReference<CryptoFileSystem> cryptoFileSystem, VaultState state, @Named("lastKnownException") ObjectProperty<Exception> lastKnownException, VaultStats stats) {
 		this.vaultSettings = vaultSettings;
 		this.volumeProvider = volumeProvider;
 		this.defaultMountFlags = defaultMountFlags;
@@ -147,7 +148,7 @@ public class Vault {
 			cryptoFileSystem.set(fs);
 			try {
 				volume = volumeProvider.get();
-				volume.mount(fs, getEffectiveMountFlags());
+				volume.mount(fs, getEffectiveMountFlags(), this::lockOnVolumeExit);
 			} catch (Exception e) {
 				destroyCryptoFileSystem();
 				throw e;
@@ -157,13 +158,33 @@ public class Vault {
 		}
 	}
 
-	public synchronized void lock(boolean forced) throws VolumeException {
+	private void lockOnVolumeExit(Throwable t) {
+		LOG.info("Unmounted vault '{}'", getDisplayName());
+		destroyCryptoFileSystem();
+		state.set(VaultState.Value.LOCKED);
+		if (t != null) {
+			LOG.warn("Unexpected unmount and lock of vault " + getDisplayName(), t);
+		}
+	}
+
+	public synchronized void lock(boolean forced) throws VolumeException, LockNotCompletedException {
+		//initiate unmount
 		if (forced && volume.supportsForcedUnmount()) {
 			volume.unmountForced();
 		} else {
 			volume.unmount();
 		}
-		destroyCryptoFileSystem();
+
+		//wait for lockOnVolumeExit to be executed
+		try {
+			boolean locked = state.awaitState(VaultState.Value.LOCKED, 3000, TimeUnit.MILLISECONDS);
+			if (!locked) {
+				throw new LockNotCompletedException("Locking of vault " + this.getDisplayName() + " still in progress.");
+			}
+		} catch (InterruptedException e) {
+			Thread.currentThread().interrupt();
+			throw new LockNotCompletedException(e);
+		}
 	}
 
 	public void reveal(Volume.Revealer vaultRevealer) throws VolumeException {
@@ -174,16 +195,12 @@ public class Vault {
 	// Observable Properties
 	// *******************************************************************************
 
-	public ObjectProperty<VaultState> stateProperty() {
+	public VaultState stateProperty() {
 		return state;
 	}
 
-	public VaultState getState() {
-		return state.get();
-	}
-
-	public void setState(VaultState value) {
-		state.setValue(value);
+	public VaultState.Value getState() {
+		return state.getValue();
 	}
 
 	public ObjectProperty<Exception> lastKnownExceptionProperty() {
@@ -203,7 +220,7 @@ public class Vault {
 	}
 
 	public boolean isLocked() {
-		return state.get() == VaultState.LOCKED;
+		return state.get() == VaultState.Value.LOCKED;
 	}
 
 	public BooleanBinding processingProperty() {
@@ -211,7 +228,7 @@ public class Vault {
 	}
 
 	public boolean isProcessing() {
-		return state.get() == VaultState.PROCESSING;
+		return state.get() == VaultState.Value.PROCESSING;
 	}
 
 	public BooleanBinding unlockedProperty() {
@@ -219,7 +236,7 @@ public class Vault {
 	}
 
 	public boolean isUnlocked() {
-		return state.get() == VaultState.UNLOCKED;
+		return state.get() == VaultState.Value.UNLOCKED;
 	}
 
 	public BooleanBinding missingProperty() {
@@ -227,7 +244,7 @@ public class Vault {
 	}
 
 	public boolean isMissing() {
-		return state.get() == VaultState.MISSING;
+		return state.get() == VaultState.Value.MISSING;
 	}
 
 	public BooleanBinding needsMigrationProperty() {
@@ -235,7 +252,7 @@ public class Vault {
 	}
 
 	public boolean isNeedsMigration() {
-		return state.get() == VaultState.NEEDS_MIGRATION;
+		return state.get() == VaultState.Value.NEEDS_MIGRATION;
 	}
 
 	public BooleanBinding unknownErrorProperty() {
@@ -243,7 +260,7 @@ public class Vault {
 	}
 
 	public boolean isUnknownError() {
-		return state.get() == VaultState.ERROR;
+		return state.get() == VaultState.Value.ERROR;
 	}
 
 	public StringBinding displayNameProperty() {
@@ -259,7 +276,7 @@ public class Vault {
 	}
 
 	public String getAccessPoint() {
-		if (state.get() == VaultState.UNLOCKED) {
+		if (state.getValue() == VaultState.Value.UNLOCKED) {
 			assert volume != null;
 			return volume.getMountPoint().orElse(Path.of("")).toString();
 		} else {

+ 1 - 1
main/commons/src/main/java/org/cryptomator/common/vaults/VaultComponent.java

@@ -26,7 +26,7 @@ public interface VaultComponent {
 		Builder vaultSettings(VaultSettings vaultSettings);
 
 		@BindsInstance
-		Builder initialVaultState(VaultState vaultState);
+		Builder initialVaultState(VaultState.Value vaultState);
 
 		@BindsInstance
 		Builder initialErrorCause(@Nullable @Named("lastKnownException") Exception initialErrorCause);

+ 13 - 13
main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java

@@ -25,7 +25,6 @@ import java.nio.file.Path;
 import java.util.Collection;
 import java.util.Optional;
 import java.util.ResourceBundle;
-import java.util.stream.Collectors;
 
 import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
 
@@ -94,42 +93,43 @@ public class VaultListManager {
 	private Vault create(VaultSettings vaultSettings) {
 		VaultComponent.Builder compBuilder = vaultComponentBuilder.vaultSettings(vaultSettings);
 		try {
-			VaultState vaultState = determineVaultState(vaultSettings.path().get());
+			VaultState.Value vaultState = determineVaultState(vaultSettings.path().get());
 			compBuilder.initialVaultState(vaultState);
 		} catch (IOException e) {
 			LOG.warn("Failed to determine vault state for " + vaultSettings.path().get(), e);
-			compBuilder.initialVaultState(VaultState.ERROR);
+			compBuilder.initialVaultState(VaultState.Value.ERROR);
 			compBuilder.initialErrorCause(e);
 		}
 		return compBuilder.build().vault();
 	}
 
-	public static VaultState redetermineVaultState(Vault vault) {
-		VaultState previousState = vault.getState();
+	public static VaultState.Value redetermineVaultState(Vault vault) {
+		VaultState state = vault.stateProperty();
+		VaultState.Value previousState = state.getValue();
 		return switch (previousState) {
 			case LOCKED, NEEDS_MIGRATION, MISSING -> {
 				try {
-					VaultState determinedState = determineVaultState(vault.getPath());
-					vault.setState(determinedState);
+					VaultState.Value determinedState = determineVaultState(vault.getPath());
+					state.set(determinedState);
 					yield determinedState;
 				} catch (IOException e) {
 					LOG.warn("Failed to determine vault state for " + vault.getPath(), e);
-					vault.setState(VaultState.ERROR);
+					state.set(VaultState.Value.ERROR);
 					vault.setLastKnownException(e);
-					yield VaultState.ERROR;
+					yield VaultState.Value.ERROR;
 				}
 			}
 			case ERROR, UNLOCKED, PROCESSING -> previousState;
 		};
 	}
 
-	private static VaultState determineVaultState(Path pathToVault) throws IOException {
+	private static VaultState.Value determineVaultState(Path pathToVault) throws IOException {
 		if (!CryptoFileSystemProvider.containsVault(pathToVault, MASTERKEY_FILENAME)) {
-			return VaultState.MISSING;
+			return VaultState.Value.MISSING;
 		} else if (Migrators.get().needsMigration(pathToVault, MASTERKEY_FILENAME)) {
-			return VaultState.NEEDS_MIGRATION;
+			return VaultState.Value.NEEDS_MIGRATION;
 		} else {
-			return VaultState.LOCKED;
+			return VaultState.Value.LOCKED;
 		}
 	}
 

+ 0 - 6
main/commons/src/main/java/org/cryptomator/common/vaults/VaultModule.java

@@ -40,12 +40,6 @@ public class VaultModule {
 		return new AtomicReference<>();
 	}
 
-	@Provides
-	@PerVault
-	public ObjectProperty<VaultState> provideVaultState(VaultState initialState) {
-		return new SimpleObjectProperty<>(initialState);
-	}
-
 	@Provides
 	@Named("lastKnownException")
 	@PerVault

+ 128 - 21
main/commons/src/main/java/org/cryptomator/common/vaults/VaultState.java

@@ -1,34 +1,141 @@
 package org.cryptomator.common.vaults;
 
-public enum VaultState {
-	/**
-	 * No vault found at the provided path
-	 */
-	MISSING,
+import com.google.common.base.Preconditions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
-	/**
-	 * Vault requires migration to a newer vault format
-	 */
-	NEEDS_MIGRATION,
+import javax.inject.Inject;
+import javafx.application.Platform;
+import javafx.beans.value.ObservableObjectValue;
+import javafx.beans.value.ObservableValueBase;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 
-	/**
-	 * Vault ready to be unlocked
-	 */
-	LOCKED,
+@PerVault
+public class VaultState extends ObservableValueBase<VaultState.Value> implements ObservableObjectValue<VaultState.Value> {
 
-	/**
-	 * Vault in transition between two other states
-	 */
-	PROCESSING,
+	private static final Logger LOG = LoggerFactory.getLogger(VaultState.class);
+
+	public enum Value {
+		/**
+		 * No vault found at the provided path
+		 */
+		MISSING,
+
+		/**
+		 * Vault requires migration to a newer vault format
+		 */
+		NEEDS_MIGRATION,
+
+		/**
+		 * Vault ready to be unlocked
+		 */
+		LOCKED,
+
+		/**
+		 * Vault in transition between two other states
+		 */
+		PROCESSING,
+
+		/**
+		 * Vault is unlocked
+		 */
+		UNLOCKED,
+
+		/**
+		 * Unknown state due to preceeding unrecoverable exceptions.
+		 */
+		ERROR;
+	}
+
+	private final AtomicReference<Value> value;
+	private final Lock lock = new ReentrantLock();
+	private final Condition valueChanged = lock.newCondition();
+
+	@Inject
+	public VaultState(VaultState.Value initialValue) {
+		this.value = new AtomicReference<>(initialValue);
+	}
+
+	@Override
+	public Value get() {
+		return getValue();
+	}
+
+	@Override
+	public Value getValue() {
+		return value.get();
+	}
 
 	/**
-	 * Vault is unlocked
+	 * Transitions from <code>fromState</code> to <code>toState</code>.
+	 *
+	 * @param fromState Previous state
+	 * @param toState New state
+	 * @return <code>true</code> if successful
 	 */
-	UNLOCKED,
+	public boolean transition(Value fromState, Value toState) {
+		Preconditions.checkArgument(fromState != toState, "fromState must be different than toState");
+		boolean success = value.compareAndSet(fromState, toState);
+		if (success) {
+			fireValueChangedEvent();
+		} else {
+			LOG.debug("Failed transiting into state {}: Expected state was {}, but actual state is {}.", fromState, toState, value.get());
+		}
+		return success;
+	}
+
+	public void set(Value newState) {
+		var oldState = value.getAndSet(newState);
+		if (oldState != newState) {
+			fireValueChangedEvent();
+		}
+	}
 
 	/**
-	 * Unknown state due to preceeding unrecoverable exceptions.
+	 * Waits for the specified time, until the desired state is reached.
+	 *
+	 * @param desiredState what state to wait for
+	 * @param time the maximum time to wait
+	 * @param unit the time unit of the {@code time} argument
+	 * @return {@code false} if the waiting time detectably elapsed before reaching {@code desiredState}
+	 * @throws InterruptedException if the current thread is interrupted
 	 */
-	ERROR;
+	public boolean awaitState(Value desiredState, long time, TimeUnit unit) throws InterruptedException {
+		lock.lock();
+		try {
+			long remaining = TimeUnit.NANOSECONDS.convert(time, unit);
+			while (value.get() != desiredState) {
+				if (remaining <= 0L) {
+					return false;
+				}
+				remaining = valueChanged.awaitNanos(remaining);
+			}
+			return true;
+		} finally {
+			lock.unlock();
+		}
+	}
+
+	private void signal() {
+		lock.lock();
+		try {
+			valueChanged.signalAll();
+		} finally {
+			lock.unlock();
+		}
+	}
 
+	@Override
+	protected void fireValueChangedEvent() {
+		signal();
+		if (Platform.isFxApplicationThread()) {
+			super.fireValueChangedEvent();
+		} else {
+			Platform.runLater(super::fireValueChangedEvent);
+		}
+	}
 }

+ 5 - 5
main/commons/src/main/java/org/cryptomator/common/vaults/VaultStats.java

@@ -26,7 +26,7 @@ public class VaultStats {
 	private static final Logger LOG = LoggerFactory.getLogger(VaultStats.class);
 
 	private final AtomicReference<CryptoFileSystem> fs;
-	private final ObjectProperty<VaultState> state;
+	private final VaultState state;
 	private final ScheduledService<Optional<CryptoFileSystemStats>> updateService;
 	private final LongProperty bytesPerSecondRead = new SimpleLongProperty();
 	private final LongProperty bytesPerSecondWritten = new SimpleLongProperty();
@@ -41,7 +41,7 @@ public class VaultStats {
 	private final LongProperty filesWritten = new SimpleLongProperty();
 
 	@Inject
-	VaultStats(AtomicReference<CryptoFileSystem> fs, ObjectProperty<VaultState> state, ExecutorService executor) {
+	VaultStats(AtomicReference<CryptoFileSystem> fs, VaultState state, ExecutorService executor) {
 		this.fs = fs;
 		this.state = state;
 		this.updateService = new UpdateStatsService();
@@ -52,13 +52,13 @@ public class VaultStats {
 	}
 
 	private void vaultStateChanged(@SuppressWarnings("unused") Observable observable) {
-		if (VaultState.UNLOCKED.equals(state.get())) {
+		if (VaultState.Value.UNLOCKED == state.get()) {
 			assert fs.get() != null;
 			LOG.debug("start recording stats");
-			updateService.restart();
+			Platform.runLater(() -> updateService.restart());
 		} else {
 			LOG.debug("stop recording stats");
-			updateService.cancel();
+			Platform.runLater(() -> updateService.cancel());
 		}
 	}
 

+ 3 - 1
main/commons/src/main/java/org/cryptomator/common/vaults/Volume.java

@@ -7,6 +7,8 @@ import org.cryptomator.cryptofs.CryptoFileSystem;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.util.Optional;
+import java.util.concurrent.CompletionStage;
+import java.util.function.Consumer;
 import java.util.stream.Stream;
 
 /**
@@ -32,7 +34,7 @@ public interface Volume {
 	 * @param fs
 	 * @throws IOException
 	 */
-	void mount(CryptoFileSystem fs, String mountFlags) throws IOException, VolumeException, InvalidMountPointException;
+	void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws IOException, VolumeException, InvalidMountPointException;
 
 	/**
 	 * Reveals the mounted volume.

+ 8 - 3
main/commons/src/main/java/org/cryptomator/common/vaults/WebDavVolume.java

@@ -17,6 +17,7 @@ import java.net.InetAddress;
 import java.net.UnknownHostException;
 import java.nio.file.Path;
 import java.util.Optional;
+import java.util.function.Consumer;
 import java.util.function.Supplier;
 
 public class WebDavVolume implements Volume {
@@ -31,6 +32,7 @@ public class WebDavVolume implements Volume {
 	private WebDavServer server;
 	private WebDavServletController servlet;
 	private Mounter.Mount mount;
+	private Consumer<Throwable> onExitAction;
 
 	@Inject
 	public WebDavVolume(Provider<WebDavServer> serverProvider, VaultSettings vaultSettings, Settings settings, WindowsDriveLetters windowsDriveLetters) {
@@ -41,12 +43,13 @@ public class WebDavVolume implements Volume {
 	}
 
 	@Override
-	public void mount(CryptoFileSystem fs, String mountFlags) throws VolumeException {
+	public void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws VolumeException {
 		startServlet(fs);
 		mountServlet();
+		this.onExitAction = onExitAction;
 	}
 
-	private void startServlet(CryptoFileSystem fs){
+	private void startServlet(CryptoFileSystem fs) {
 		if (server == null) {
 			server = serverProvider.get();
 		}
@@ -66,7 +69,7 @@ public class WebDavVolume implements Volume {
 
 		//on windows, prevent an automatic drive letter selection in the upstream library. Either we choose already a specifc one or there is no free.
 		Supplier<String> driveLetterSupplier;
-		if(System.getProperty("os.name").toLowerCase().contains("windows") && vaultSettings.winDriveLetter().isEmpty().get()) {
+		if (System.getProperty("os.name").toLowerCase().contains("windows") && vaultSettings.winDriveLetter().isEmpty().get()) {
 			driveLetterSupplier = () -> windowsDriveLetters.getAvailableDriveLetter().orElse(null);
 		} else {
 			driveLetterSupplier = () -> vaultSettings.winDriveLetter().get();
@@ -101,6 +104,7 @@ public class WebDavVolume implements Volume {
 			throw new VolumeException(e);
 		}
 		cleanup();
+		onExitAction.accept(null);
 	}
 
 	@Override
@@ -111,6 +115,7 @@ public class WebDavVolume implements Volume {
 			throw new VolumeException(e);
 		}
 		cleanup();
+		onExitAction.accept(null);
 	}
 
 	@Override

+ 2 - 2
main/pom.xml

@@ -30,8 +30,8 @@
 		<cryptomator.integrations.win.version>1.0.0-beta2</cryptomator.integrations.win.version>
 		<cryptomator.integrations.mac.version>1.0.0-beta2</cryptomator.integrations.mac.version>
 		<cryptomator.integrations.linux.version>1.0.0-beta1</cryptomator.integrations.linux.version>
-		<cryptomator.fuse.version>1.3.0</cryptomator.fuse.version>
-		<cryptomator.dokany.version>1.2.4</cryptomator.dokany.version>
+		<cryptomator.fuse.version>1.3.1</cryptomator.fuse.version>
+		<cryptomator.dokany.version>1.3.0</cryptomator.dokany.version>
 		<cryptomator.webdav.version>1.2.0</cryptomator.webdav.version>
 
 		<!-- 3rd party dependencies -->

+ 10 - 4
main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java

@@ -1,5 +1,6 @@
 package org.cryptomator.ui.common;
 
+import org.cryptomator.common.vaults.LockNotCompletedException;
 import org.cryptomator.common.vaults.Vault;
 import org.cryptomator.common.vaults.VaultState;
 import org.cryptomator.common.vaults.Volume;
@@ -175,24 +176,29 @@ public class VaultService {
 		}
 
 		@Override
-		protected Vault call() throws Volume.VolumeException {
+		protected Vault call() throws Volume.VolumeException, LockNotCompletedException {
 			vault.lock(forced);
 			return vault;
 		}
 
 		@Override
 		protected void scheduled() {
-			vault.setState(VaultState.PROCESSING);
+			vault.stateProperty().transition(VaultState.Value.UNLOCKED, VaultState.Value.PROCESSING);
 		}
 
 		@Override
 		protected void succeeded() {
-			vault.setState(VaultState.LOCKED);
+			vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED);
 		}
 
 		@Override
 		protected void failed() {
-			vault.setState(VaultState.UNLOCKED);
+			vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED);
+		}
+
+		@Override
+		protected void cancelled() {
+			vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED);
 		}
 
 	}

+ 21 - 9
main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java

@@ -5,11 +5,13 @@ import org.cryptomator.common.LicenseHolder;
 import org.cryptomator.common.settings.Settings;
 import org.cryptomator.common.settings.UiTheme;
 import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.common.vaults.VaultState;
 import org.cryptomator.integrations.tray.TrayIntegrationProvider;
 import org.cryptomator.integrations.uiappearance.Theme;
 import org.cryptomator.integrations.uiappearance.UiAppearanceException;
 import org.cryptomator.integrations.uiappearance.UiAppearanceListener;
 import org.cryptomator.integrations.uiappearance.UiAppearanceProvider;
+import org.cryptomator.ui.common.ErrorComponent;
 import org.cryptomator.ui.common.VaultService;
 import org.cryptomator.ui.lock.LockComponent;
 import org.cryptomator.ui.mainwindow.MainWindowComponent;
@@ -44,8 +46,9 @@ public class FxApplication extends Application {
 	private final Lazy<MainWindowComponent> mainWindow;
 	private final Lazy<PreferencesComponent> preferencesWindow;
 	private final Lazy<QuitComponent> quitWindow;
-	private final Provider<UnlockComponent.Builder> unlockWindowBuilderProvider;
-	private final Provider<LockComponent.Builder> lockWindowBuilderProvider;
+	private final Provider<UnlockComponent.Builder> unlockWorkflowBuilderProvider;
+	private final Provider<LockComponent.Builder> lockWorkflowBuilderProvider;
+	private final ErrorComponent.Builder errorWindowBuilder;
 	private final Optional<TrayIntegrationProvider> trayIntegration;
 	private final Optional<UiAppearanceProvider> appearanceProvider;
 	private final VaultService vaultService;
@@ -55,13 +58,14 @@ public class FxApplication extends Application {
 	private final UiAppearanceListener systemInterfaceThemeListener = this::systemInterfaceThemeChanged;
 
 	@Inject
-	FxApplication(Settings settings, Lazy<MainWindowComponent> mainWindow, Lazy<PreferencesComponent> preferencesWindow, Provider<UnlockComponent.Builder> unlockWindowBuilderProvider, Provider<LockComponent.Builder> lockWindowBuilderProvider, Lazy<QuitComponent> quitWindow, Optional<TrayIntegrationProvider> trayIntegration, Optional<UiAppearanceProvider> appearanceProvider, VaultService vaultService, LicenseHolder licenseHolder) {
+	FxApplication(Settings settings, Lazy<MainWindowComponent> mainWindow, Lazy<PreferencesComponent> preferencesWindow, Provider<UnlockComponent.Builder> unlockWorkflowBuilderProvider, Provider<LockComponent.Builder> lockWorkflowBuilderProvider, Lazy<QuitComponent> quitWindow, ErrorComponent.Builder errorWindowBuilder, Optional<TrayIntegrationProvider> trayIntegration, Optional<UiAppearanceProvider> appearanceProvider, VaultService vaultService, LicenseHolder licenseHolder) {
 		this.settings = settings;
 		this.mainWindow = mainWindow;
 		this.preferencesWindow = preferencesWindow;
-		this.unlockWindowBuilderProvider = unlockWindowBuilderProvider;
-		this.lockWindowBuilderProvider = lockWindowBuilderProvider;
+		this.unlockWorkflowBuilderProvider = unlockWorkflowBuilderProvider;
+		this.lockWorkflowBuilderProvider = lockWorkflowBuilderProvider;
 		this.quitWindow = quitWindow;
+		this.errorWindowBuilder = errorWindowBuilder;
 		this.trayIntegration = trayIntegration;
 		this.appearanceProvider = appearanceProvider;
 		this.vaultService = vaultService;
@@ -113,15 +117,23 @@ public class FxApplication extends Application {
 
 	public void startUnlockWorkflow(Vault vault, Optional<Stage> owner) {
 		Platform.runLater(() -> {
-			unlockWindowBuilderProvider.get().vault(vault).owner(owner).build().startUnlockWorkflow();
-			LOG.debug("Showing UnlockWindow for {}", vault.getDisplayName());
+			if (vault.stateProperty().transition(VaultState.Value.LOCKED, VaultState.Value.PROCESSING)) {
+				unlockWorkflowBuilderProvider.get().vault(vault).owner(owner).build().startUnlockWorkflow();
+				LOG.debug("Start unlock workflow for {}", vault.getDisplayName());
+			} else {
+				showMainWindow().thenAccept(mainWindow -> errorWindowBuilder.window(mainWindow).cause(new IllegalStateException("Unable to unlock vault in non-locked state.")));
+			}
 		});
 	}
 
 	public void startLockWorkflow(Vault vault, Optional<Stage> owner) {
 		Platform.runLater(() -> {
-			lockWindowBuilderProvider.get().vault(vault).owner(owner).build().startLockWorkflow();
-			LOG.debug("Start lock workflow for {}", vault.getDisplayName());
+			if (vault.stateProperty().transition(VaultState.Value.UNLOCKED, VaultState.Value.PROCESSING)) {
+				lockWorkflowBuilderProvider.get().vault(vault).owner(owner).build().startLockWorkflow();
+				LOG.debug("Start lock workflow for {}", vault.getDisplayName());
+			} else {
+				showMainWindow().thenAccept(mainWindow -> errorWindowBuilder.window(mainWindow).cause(new IllegalStateException("Unable to lock vault in non-unlocked state.")));
+			}
 		});
 	}
 

+ 6 - 1
main/ui/src/main/java/org/cryptomator/ui/launcher/AppLifecycleListener.java

@@ -1,6 +1,7 @@
 package org.cryptomator.ui.launcher;
 
 import org.cryptomator.common.ShutdownHook;
+import org.cryptomator.common.vaults.LockNotCompletedException;
 import org.cryptomator.common.vaults.Vault;
 import org.cryptomator.common.vaults.VaultState;
 import org.cryptomator.common.vaults.Volume;
@@ -24,11 +25,13 @@ import java.util.Set;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.atomic.AtomicBoolean;
 
+import static org.cryptomator.common.vaults.VaultState.Value.*;
+
 @Singleton
 public class AppLifecycleListener {
 
 	private static final Logger LOG = LoggerFactory.getLogger(AppLifecycleListener.class);
-	public static final Set<VaultState> STATES_ALLOWING_TERMINATION = EnumSet.of(VaultState.LOCKED, VaultState.NEEDS_MIGRATION, VaultState.MISSING, VaultState.ERROR);
+	public static final Set<VaultState.Value> STATES_ALLOWING_TERMINATION = EnumSet.of(LOCKED, NEEDS_MIGRATION, MISSING, ERROR);
 
 	private final FxApplicationStarter fxApplicationStarter;
 	private final CountDownLatch shutdownLatch;
@@ -127,6 +130,8 @@ public class AppLifecycleListener {
 					vault.lock(true);
 				} catch (Volume.VolumeException e) {
 					LOG.error("Failed to unmount vault " + vault.getPath(), e);
+				} catch (LockNotCompletedException e) {
+					LOG.error("Failed to lock vault " + vault.getPath(), e);
 				}
 			}
 		}

+ 18 - 14
main/ui/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java

@@ -1,9 +1,11 @@
 package org.cryptomator.ui.lock;
 
 import dagger.Lazy;
+import org.cryptomator.common.vaults.LockNotCompletedException;
 import org.cryptomator.common.vaults.Vault;
 import org.cryptomator.common.vaults.VaultState;
 import org.cryptomator.common.vaults.Volume;
+import org.cryptomator.ui.common.ErrorComponent;
 import org.cryptomator.ui.common.FxmlFile;
 import org.cryptomator.ui.common.FxmlScene;
 import org.cryptomator.ui.common.UserInteractionLock;
@@ -35,21 +37,23 @@ public class LockWorkflow extends Task<Void> {
 	private final UserInteractionLock<LockModule.ForceLockDecision> forceLockDecisionLock;
 	private final Lazy<Scene> lockForcedScene;
 	private final Lazy<Scene> lockFailedScene;
+	private final ErrorComponent.Builder errorComponent;
 
 	@Inject
-	public LockWorkflow(@LockWindow Stage lockWindow, @LockWindow Vault vault, UserInteractionLock<LockModule.ForceLockDecision> forceLockDecisionLock, @FxmlScene(FxmlFile.LOCK_FORCED) Lazy<Scene> lockForcedScene, @FxmlScene(FxmlFile.LOCK_FAILED) Lazy<Scene> lockFailedScene) {
+	public LockWorkflow(@LockWindow Stage lockWindow, @LockWindow Vault vault, UserInteractionLock<LockModule.ForceLockDecision> forceLockDecisionLock, @FxmlScene(FxmlFile.LOCK_FORCED) Lazy<Scene> lockForcedScene, @FxmlScene(FxmlFile.LOCK_FAILED) Lazy<Scene> lockFailedScene, ErrorComponent.Builder errorComponent) {
 		this.lockWindow = lockWindow;
 		this.vault = vault;
 		this.forceLockDecisionLock = forceLockDecisionLock;
 		this.lockForcedScene = lockForcedScene;
 		this.lockFailedScene = lockFailedScene;
+		this.errorComponent = errorComponent;
 	}
 
 	@Override
-	protected Void call() throws Volume.VolumeException, InterruptedException {
+	protected Void call() throws Volume.VolumeException, InterruptedException, LockNotCompletedException {
 		try {
 			vault.lock(false);
-		} catch (Volume.VolumeException e) {
+		} catch (Volume.VolumeException | LockNotCompletedException e) {
 			LOG.debug("Regular lock of {} failed.", vault.getDisplayName(), e);
 			var decision = askUserForAction();
 			switch (decision) {
@@ -77,29 +81,29 @@ public class LockWorkflow extends Task<Void> {
 		return forceLockDecisionLock.awaitInteraction();
 	}
 
-	@Override
-	protected void scheduled() {
-		vault.setState(VaultState.PROCESSING);
-	}
-
 	@Override
 	protected void succeeded() {
 		LOG.info("Lock of {} succeeded.", vault.getDisplayName());
-		vault.setState(VaultState.LOCKED);
+		vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED);
 	}
 
 	@Override
 	protected void failed() {
-		LOG.warn("Failed to lock {}.", vault.getDisplayName());
-		vault.setState(VaultState.UNLOCKED);
-		lockWindow.setScene(lockFailedScene.get());
-		lockWindow.show();
+		final var throwable = super.getException();
+		LOG.warn("Lock of {} failed.", vault.getDisplayName(), throwable);
+		vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED);
+		if (throwable instanceof Volume.VolumeException) {
+			lockWindow.setScene(lockFailedScene.get());
+			lockWindow.show();
+		} else {
+			errorComponent.cause(throwable).window(lockWindow).build().showErrorScene();
+		}
 	}
 
 	@Override
 	protected void cancelled() {
 		LOG.debug("Lock of {} canceled.", vault.getDisplayName());
-		vault.setState(VaultState.UNLOCKED);
+		vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED);
 	}
 
 }

+ 2 - 1
main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailController.java

@@ -32,7 +32,8 @@ public class VaultDetailController implements FxController {
 		this.anyVaultSelected = vault.isNotNull();
 	}
 
-	private FontAwesome5Icon getGlyphForVaultState(VaultState state) {
+	// TODO deduplicate w/ VaultListCellController
+	private FontAwesome5Icon getGlyphForVaultState(VaultState.Value state) {
 		if (state != null) {
 			return switch (state) {
 				case LOCKED -> FontAwesome5Icon.LOCK;

+ 2 - 1
main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultListCellController.java

@@ -24,7 +24,8 @@ public class VaultListCellController implements FxController {
 				.map(this::getGlyphForVaultState);
 	}
 
-	private FontAwesome5Icon getGlyphForVaultState(VaultState state) {
+	// TODO deduplicate w/ VaultDetailController
+	private FontAwesome5Icon getGlyphForVaultState(VaultState.Value state) {
 		if (state != null) {
 			return switch (state) {
 				case LOCKED -> FontAwesome5Icon.LOCK;

+ 2 - 9
main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultListContextMenuController.java

@@ -14,19 +14,13 @@ import org.cryptomator.ui.vaultoptions.VaultOptionsComponent;
 
 import javax.inject.Inject;
 import javafx.beans.binding.Binding;
-import javafx.beans.binding.Bindings;
 import javafx.beans.property.ObjectProperty;
 import javafx.fxml.FXML;
 import javafx.stage.Stage;
-import java.util.Arrays;
 import java.util.EnumSet;
 import java.util.Optional;
 
-import static org.cryptomator.common.vaults.VaultState.ERROR;
-import static org.cryptomator.common.vaults.VaultState.LOCKED;
-import static org.cryptomator.common.vaults.VaultState.MISSING;
-import static org.cryptomator.common.vaults.VaultState.NEEDS_MIGRATION;
-import static org.cryptomator.common.vaults.VaultState.UNLOCKED;
+import static org.cryptomator.common.vaults.VaultState.Value.*;
 
 @MainWindowScoped
 public class VaultListContextMenuController implements FxController {
@@ -37,7 +31,7 @@ public class VaultListContextMenuController implements FxController {
 	private final KeychainManager keychain;
 	private final RemoveVaultComponent.Builder removeVault;
 	private final VaultOptionsComponent.Builder vaultOptionsWindow;
-	private final OptionalBinding<VaultState> selectedVaultState;
+	private final OptionalBinding<VaultState.Value> selectedVaultState;
 	private final Binding<Boolean> selectedVaultPassphraseStored;
 	private final Binding<Boolean> selectedVaultRemovable;
 	private final Binding<Boolean> selectedVaultUnlockable;
@@ -57,7 +51,6 @@ public class VaultListContextMenuController implements FxController {
 		this.selectedVaultRemovable = selectedVaultState.map(EnumSet.of(LOCKED, MISSING, ERROR, NEEDS_MIGRATION)::contains).orElse(false);
 		this.selectedVaultUnlockable = selectedVaultState.map(LOCKED::equals).orElse(false);
 		this.selectedVaultLockable = selectedVaultState.map(UNLOCKED::equals).orElse(false);
-
 	}
 
 	private boolean isPasswordStored(Vault vault) {

+ 12 - 8
main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java

@@ -26,6 +26,7 @@ import javax.inject.Named;
 import javafx.application.Platform;
 import javafx.beans.binding.Bindings;
 import javafx.beans.binding.ObjectBinding;
+import javafx.beans.binding.ObjectExpression;
 import javafx.beans.property.BooleanProperty;
 import javafx.beans.property.DoubleProperty;
 import javafx.beans.property.ObjectProperty;
@@ -89,7 +90,10 @@ public class MigrationRunController implements FxController {
 		if (keychain.isSupported()) {
 			loadStoredPassword();
 		}
-		migrationButtonDisabled.bind(vault.stateProperty().isNotEqualTo(VaultState.NEEDS_MIGRATION).or(passwordField.textProperty().isEmpty()));
+
+		migrationButtonDisabled.bind(ObjectExpression.objectExpression(vault.stateProperty())
+				.isNotEqualTo(VaultState.Value.NEEDS_MIGRATION)
+				.or(passwordField.textProperty().isEmpty()));
 	}
 
 	@FXML
@@ -101,7 +105,7 @@ public class MigrationRunController implements FxController {
 	public void migrate() {
 		LOG.info("Migrating vault {}", vault.getPath());
 		CharSequence password = passwordField.getCharacters();
-		vault.setState(VaultState.PROCESSING);
+		vault.stateProperty().transition(VaultState.Value.NEEDS_MIGRATION, VaultState.Value.PROCESSING);
 		passwordField.setDisable(true);
 		ScheduledFuture<?> progressSyncTask = scheduler.scheduleAtFixedRate(() -> {
 			Platform.runLater(() -> {
@@ -115,10 +119,10 @@ public class MigrationRunController implements FxController {
 		}).onSuccess(needsAnotherMigration -> {
 			if (needsAnotherMigration) {
 				LOG.info("Migration of '{}' succeeded, but another migration is required.", vault.getDisplayName());
-				vault.setState(VaultState.NEEDS_MIGRATION);
+				vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.NEEDS_MIGRATION);
 			} else {
 				LOG.info("Migration of '{}' succeeded.", vault.getDisplayName());
-				vault.setState(VaultState.LOCKED);
+				vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED);
 				passwordField.wipe();
 				window.setScene(successScene.get());
 			}
@@ -127,20 +131,20 @@ public class MigrationRunController implements FxController {
 			passwordField.setDisable(false);
 			passwordField.selectAll();
 			passwordField.requestFocus();
-			vault.setState(VaultState.NEEDS_MIGRATION);
+			vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.NEEDS_MIGRATION);
 		}).onError(FileSystemCapabilityChecker.MissingCapabilityException.class, e -> {
 			LOG.error("Underlying file system not supported.", e);
-			vault.setState(VaultState.NEEDS_MIGRATION);
+			vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.NEEDS_MIGRATION);
 			missingCapability.set(e.getMissingCapability());
 			window.setScene(capabilityErrorScene.get());
 		}).onError(FileNameTooLongException.class, e -> {
 			LOG.error("Migration failed because the underlying file system does not support long filenames.", e);
-			vault.setState(VaultState.NEEDS_MIGRATION);
+			vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.NEEDS_MIGRATION);
 			errorComponent.cause(e).window(window).returnToScene(startScene.get()).build().showErrorScene();
 			window.setScene(impossibleScene.get());
 		}).onError(Exception.class, e -> { // including RuntimeExceptions
 			LOG.error("Migration failed for technical reasons.", e);
-			vault.setState(VaultState.NEEDS_MIGRATION);
+			vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.NEEDS_MIGRATION);
 			errorComponent.cause(e).window(window).returnToScene(startScene.get()).build().showErrorScene();
 		}).andFinally(() -> {
 			passwordField.setDisable(false);

+ 2 - 2
main/ui/src/main/java/org/cryptomator/ui/stats/VaultStatisticsModule.java

@@ -43,8 +43,8 @@ abstract class VaultStatisticsModule {
 		var weakStage = new WeakReference<>(stage);
 		vault.stateProperty().addListener(new ChangeListener<>() {
 			@Override
-			public void changed(ObservableValue<? extends VaultState> observable, VaultState oldValue, VaultState newValue) {
-				if (newValue != VaultState.UNLOCKED) {
+			public void changed(ObservableValue<? extends VaultState.Value> observable, VaultState.Value oldValue, VaultState.Value newValue) {
+				if (newValue != VaultState.Value.UNLOCKED) {
 					Stage stage = weakStage.get();
 					if (stage != null) {
 						stage.hide();

+ 29 - 40
main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java

@@ -73,22 +73,15 @@ public class UnlockWorkflow extends Task<Boolean> {
 		this.successScene = successScene;
 		this.invalidMountPointScene = invalidMountPointScene;
 		this.errorComponent = errorComponent;
-
-		setOnFailed(event -> {
-			Throwable throwable = event.getSource().getException();
-			if (throwable instanceof InvalidMountPointException e) {
-				handleInvalidMountPoint(e);
-			} else {
-				handleGenericError(throwable);
-			}
-		});
 	}
 
 	@Override
 	protected Boolean call() throws InterruptedException, IOException, VolumeException, InvalidMountPointException {
 		try {
 			if (attemptUnlock()) {
-				handleSuccess();
+				if (savePassword.get()) {
+					savePasswordToSystemkeychain(); //savePassword will be wiped on method return, so it must be set here
+				}
 				return true;
 			} else {
 				cancel(false); // set Tasks state to cancelled
@@ -131,24 +124,6 @@ public class UnlockWorkflow extends Task<Boolean> {
 		return passwordEntryLock.awaitInteraction();
 	}
 
-	private void handleSuccess() {
-		LOG.info("Unlock of '{}' succeeded.", vault.getDisplayName());
-		if (savePassword.get()) {
-			savePasswordToSystemkeychain();
-		}
-		switch (vault.getVaultSettings().actionAfterUnlock().get()) {
-			case ASK -> Platform.runLater(() -> {
-				window.setScene(successScene.get());
-				window.show();
-			});
-			case REVEAL -> {
-				Platform.runLater(window::close);
-				vaultService.reveal(vault);
-			}
-			case IGNORE -> Platform.runLater(window::close);
-		}
-	}
-
 	private void savePasswordToSystemkeychain() {
 		if (keychain.isSupported()) {
 			try {
@@ -173,15 +148,12 @@ public class UnlockWorkflow extends Task<Boolean> {
 				LOG.error("Unlock failed. Mountpoint doesn't exist (needs to be a folder): {}", cause.getMessage());
 			}
 			showInvalidMountPointScene();
-			return;
 		} else if (cause instanceof FileAlreadyExistsException) {
 			LOG.error("Unlock failed. Mountpoint already exists: {}", cause.getMessage());
 			showInvalidMountPointScene();
-			return;
 		} else if (cause instanceof DirectoryNotEmptyException) {
 			LOG.error("Unlock failed. Mountpoint not an empty directory: {}", cause.getMessage());
 			showInvalidMountPointScene();
-			return;
 		} else {
 			handleGenericError(impExc);
 		}
@@ -196,7 +168,7 @@ public class UnlockWorkflow extends Task<Boolean> {
 
 	private void handleGenericError(Throwable e) {
 		LOG.error("Unlock failed for technical reasons.", e);
-		errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
+		errorComponent.cause(e).window(window).build().showErrorScene();
 	}
 
 	private void wipePassword(char[] pw) {
@@ -205,24 +177,41 @@ public class UnlockWorkflow extends Task<Boolean> {
 		}
 	}
 
-	@Override
-	protected void scheduled() {
-		vault.setState(VaultState.PROCESSING);
-	}
-
 	@Override
 	protected void succeeded() {
-		vault.setState(VaultState.UNLOCKED);
+		LOG.info("Unlock of '{}' succeeded.", vault.getDisplayName());
+
+		switch (vault.getVaultSettings().actionAfterUnlock().get()) {
+			case ASK -> Platform.runLater(() -> {
+				window.setScene(successScene.get());
+				window.show();
+			});
+			case REVEAL -> {
+				Platform.runLater(window::close);
+				vaultService.reveal(vault);
+			}
+			case IGNORE -> Platform.runLater(window::close);
+		}
+
+		vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED);
 	}
 
 	@Override
 	protected void failed() {
-		vault.setState(VaultState.LOCKED);
+		LOG.info("Unlock of '{}' failed.", vault.getDisplayName());
+		Throwable throwable = super.getException();
+		if (throwable instanceof InvalidMountPointException e) {
+			handleInvalidMountPoint(e);
+		} else {
+			handleGenericError(throwable);
+		}
+		vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED);
 	}
 
 	@Override
 	protected void cancelled() {
-		vault.setState(VaultState.LOCKED);
+		LOG.debug("Unlock of '{}' canceled.", vault.getDisplayName());
+		vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED);
 	}
 
 }