diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java b/main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java index 4a1295067..584c79456 100644 --- a/main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java +++ b/main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java @@ -27,6 +27,7 @@ import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.inject.Named; import javax.inject.Provider; +import javafx.application.Platform; import javafx.beans.Observable; import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; @@ -43,6 +44,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.StampedLock; import static org.cryptomator.common.Constants.MASTERKEY_FILENAME; @@ -52,6 +54,8 @@ public class Vault { private static final Logger LOG = LoggerFactory.getLogger(Vault.class); private static final Path HOME_DIR = Paths.get(SystemUtils.USER_HOME); + private final StampedLock stateLock; + private final VaultSettings vaultSettings; private final Provider volumeProvider; private final StringBinding defaultMountFlags; @@ -93,6 +97,8 @@ public class Vault { this.accessPoint = Bindings.createStringBinding(this::getAccessPoint, state); this.accessPointPresent = this.accessPoint.isNotEmpty(); this.showingStats = new SimpleBooleanProperty(false); + + this.stateLock = new StampedLock(); } // ****************************************************************************** @@ -141,9 +147,13 @@ public class Vault { volume = volumeProvider.get(); volume.mount(fs, getEffectiveMountFlags(), throwable -> { destroyCryptoFileSystem(); - setState(VaultState.LOCKED); //TODO: possible race conditions of the vault state. Use Platform.runLater()? + new Thread(() -> { //TODO: maybe use the executor service + long stamp = stateLock.writeLock(); + setState(VaultState.LOCKED, stamp); + stateLock.unlock(stamp); + }).start(); if (throwable != null) { - LOG.warn("Unexpected unmount and lock of vault" + getDisplayName(), throwable); + LOG.warn("Unexpected unmount and lock of vault " + getDisplayName(), throwable); } }); } catch (Exception e) { @@ -180,8 +190,24 @@ public class Vault { return state.get(); } - public void setState(VaultState value) { - state.setValue(value); + public long lockVaultState() { + return stateLock.writeLock(); + } + + public void unlockVaultState(long stamp) { + stateLock.unlock(stamp); + } + + public void setState(VaultState value, long stamp) { + if (stateLock.isWriteLockStamp(stamp)) { + if (Platform.isFxApplicationThread()) { + state.setValue(value); + } else { + Platform.runLater(() -> state.setValue(value)); + } + } else { + throw new IllegalCallerException("Stamp is not a valid write lock."); + } } public ObjectProperty lastKnownExceptionProperty() { diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java index b7c10a4d4..da9eee66e 100644 --- a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java +++ b/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; @@ -108,15 +107,18 @@ public class VaultListManager { VaultState previousState = vault.getState(); return switch (previousState) { case LOCKED, NEEDS_MIGRATION, MISSING -> { + long stamp = vault.lockVaultState(); try { VaultState determinedState = determineVaultState(vault.getPath()); - vault.setState(determinedState); + vault.setState(determinedState, stamp); yield determinedState; } catch (IOException e) { LOG.warn("Failed to determine vault state for " + vault.getPath(), e); - vault.setState(VaultState.ERROR); + vault.setState(VaultState.ERROR, stamp); vault.setLastKnownException(e); yield VaultState.ERROR; + } finally { + vault.unlockVaultState(stamp); } } case ERROR, UNLOCKED, PROCESSING -> previousState; diff --git a/main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java b/main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java index 57393b858..d96bd6ae2 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java +++ b/main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java @@ -163,6 +163,8 @@ public class VaultService { private final Vault vault; private final boolean forced; + private volatile long stamp; + /** * @param vault The vault to lock * @param forced Whether to attempt a forced lock @@ -176,23 +178,32 @@ public class VaultService { @Override protected Vault call() throws Volume.VolumeException { + this.stamp = vault.lockVaultState(); vault.lock(forced); return vault; } @Override protected void scheduled() { - vault.setState(VaultState.PROCESSING); + vault.setState(VaultState.PROCESSING, stamp); } @Override protected void succeeded() { - vault.setState(VaultState.LOCKED); + vault.setState(VaultState.LOCKED, stamp); + vault.unlockVaultState(stamp); } @Override protected void failed() { - vault.setState(VaultState.UNLOCKED); + vault.setState(VaultState.UNLOCKED, stamp); + vault.unlockVaultState(stamp); + } + + @Override + protected void cancelled() { + vault.setState(VaultState.UNLOCKED, stamp); + vault.unlockVaultState(stamp); } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java b/main/ui/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java index acb6d1355..e7f0eb025 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java +++ b/main/ui/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java @@ -36,6 +36,8 @@ public class LockWorkflow extends Task { private final Lazy lockForcedScene; private final Lazy lockFailedScene; + private volatile long stamp; + @Inject public LockWorkflow(@LockWindow Stage lockWindow, @LockWindow Vault vault, UserInteractionLock forceLockDecisionLock, @FxmlScene(FxmlFile.LOCK_FORCED) Lazy lockForcedScene, @FxmlScene(FxmlFile.LOCK_FAILED) Lazy lockFailedScene) { this.lockWindow = lockWindow; @@ -47,6 +49,7 @@ public class LockWorkflow extends Task { @Override protected Void call() throws Volume.VolumeException, InterruptedException { + this.stamp = vault.lockVaultState(); try { vault.lock(false); } catch (Volume.VolumeException e) { @@ -79,19 +82,21 @@ public class LockWorkflow extends Task { @Override protected void scheduled() { - vault.setState(VaultState.PROCESSING); + vault.setState(VaultState.PROCESSING, stamp); } @Override protected void succeeded() { LOG.info("Lock of {} succeeded.", vault.getDisplayName()); - vault.setState(VaultState.LOCKED); + vault.setState(VaultState.LOCKED, stamp); + vault.unlockVaultState(stamp); } @Override protected void failed() { LOG.warn("Failed to lock {}.", vault.getDisplayName()); - vault.setState(VaultState.UNLOCKED); + vault.setState(VaultState.UNLOCKED, stamp); + vault.unlockVaultState(stamp); lockWindow.setScene(lockFailedScene.get()); lockWindow.show(); } @@ -99,7 +104,8 @@ public class LockWorkflow extends Task { @Override protected void cancelled() { LOG.debug("Lock of {} canceled.", vault.getDisplayName()); - vault.setState(VaultState.UNLOCKED); + vault.setState(VaultState.UNLOCKED, stamp); + vault.unlockVaultState(stamp); } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java b/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java index 830e15819..75caddf5d 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java @@ -101,7 +101,8 @@ public class MigrationRunController implements FxController { public void migrate() { LOG.info("Migrating vault {}", vault.getPath()); CharSequence password = passwordField.getCharacters(); - vault.setState(VaultState.PROCESSING); + long stamp = vault.lockVaultState(); + vault.setState(VaultState.PROCESSING, stamp); passwordField.setDisable(true); ScheduledFuture progressSyncTask = scheduler.scheduleAtFixedRate(() -> { Platform.runLater(() -> { @@ -115,10 +116,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.setState(VaultState.NEEDS_MIGRATION, stamp); } else { LOG.info("Migration of '{}' succeeded.", vault.getDisplayName()); - vault.setState(VaultState.LOCKED); + vault.setState(VaultState.LOCKED, stamp); passwordField.wipe(); window.setScene(successScene.get()); } @@ -127,22 +128,23 @@ public class MigrationRunController implements FxController { passwordField.setDisable(false); passwordField.selectAll(); passwordField.requestFocus(); - vault.setState(VaultState.NEEDS_MIGRATION); + vault.setState(VaultState.NEEDS_MIGRATION, stamp); }).onError(FileSystemCapabilityChecker.MissingCapabilityException.class, e -> { LOG.error("Underlying file system not supported.", e); - vault.setState(VaultState.NEEDS_MIGRATION); + vault.setState(VaultState.NEEDS_MIGRATION, stamp); 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.setState(VaultState.NEEDS_MIGRATION, stamp); 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.setState(VaultState.NEEDS_MIGRATION, stamp); errorComponent.cause(e).window(window).returnToScene(startScene.get()).build().showErrorScene(); }).andFinally(() -> { + vault.unlockVaultState(stamp); passwordField.setDisable(false); progressSyncTask.cancel(true); }).runOnce(executor); diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java index c1b5fbd45..36d34c170 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java +++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java @@ -59,6 +59,8 @@ public class UnlockWorkflow extends Task { private final Lazy invalidMountPointScene; private final ErrorComponent.Builder errorComponent; + private volatile long stamp; + @Inject UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, AtomicReference password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional savedPassword, UserInteractionLock passwordEntryLock, KeychainManager keychain, @FxmlScene(FxmlFile.UNLOCK) Lazy unlockScene, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy invalidMountPointScene, ErrorComponent.Builder errorComponent) { this.window = window; @@ -87,6 +89,7 @@ public class UnlockWorkflow extends Task { @Override protected Boolean call() throws InterruptedException, IOException, VolumeException, InvalidMountPointException { try { + this.stamp = vault.lockVaultState(); if (attemptUnlock()) { handleSuccess(); return true; @@ -207,22 +210,25 @@ public class UnlockWorkflow extends Task { @Override protected void scheduled() { - vault.setState(VaultState.PROCESSING); + vault.setState(VaultState.PROCESSING, stamp); } @Override protected void succeeded() { - vault.setState(VaultState.UNLOCKED); + vault.setState(VaultState.UNLOCKED, stamp); + vault.unlockVaultState(stamp); } @Override protected void failed() { - vault.setState(VaultState.LOCKED); + vault.setState(VaultState.LOCKED, stamp); + vault.unlockVaultState(stamp); } @Override protected void cancelled() { - vault.setState(VaultState.LOCKED); + vault.setState(VaultState.LOCKED, stamp); + vault.unlockVaultState(stamp); } }