Check the vault state right away when retrying an unlock instead of blocking the UI for up to 5 seconds

This commit is contained in:
Tobias Hagemann
2026-10-15 00:51:47 +02:00
parent fe4cdd2374
commit cc0e76cc51
3 changed files with 25 additions and 48 deletions
@@ -8,7 +8,6 @@ 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;
@@ -98,31 +97,6 @@ public class VaultState extends ObservableValueBase<VaultState.Value> implements
return success;
}
/**
* 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
*/
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();
}
}
/**
* Waits, without a timeout, until the state differs from the specified one.
*
@@ -5,7 +5,6 @@ import org.cryptomator.common.mount.ConflictingMountServiceException;
import org.cryptomator.common.mount.FSKitExtensionSwitchedOffException;
import org.cryptomator.common.mount.IllegalMountPointException;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.integrations.mount.MountFailedException;
import org.cryptomator.ui.common.FxmlFile;
@@ -30,7 +29,6 @@ import javafx.stage.Screen;
import javafx.stage.Stage;
import java.io.IOException;
import java.nio.file.ReadOnlyFileSystemException;
import java.util.concurrent.TimeUnit;
/**
* A multi-step task that consists of background activities as well as user interaction.
@@ -191,17 +189,11 @@ public class UnlockWorkflow extends Task<Void> {
}
private void restartUnlock() {
try {
var isLocked = vault.stateProperty().awaitState(VaultState.Value.LOCKED, 5, TimeUnit.SECONDS);
if (!isLocked) {
LOG.error("Vault did not changed to LOCKED state within 5 seconds. Aborting unlock retry.");
} else {
appWindows.startUnlockWorkflow(vault, owner);
}
} catch (InterruptedException e) {
LOG.error("Waiting for LOCKED vault state was interrupted. Aborting unlock retry.", e);
Thread.currentThread().interrupt();
if (!vault.isLocked()) {
LOG.warn("Vault '{}' is {} instead of LOCKED. Aborting unlock retry.", vault.getDisplayName(), vault.getState());
return;
}
appWindows.startUnlockWorkflow(vault, owner);
}
@Override
@@ -5,7 +5,6 @@ import org.cryptomator.common.mount.FSKitExtensionSwitchedOffException;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.common.settings.WhenUnlocked;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.integrations.mount.MountFailedException;
import org.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.dialogs.Dialogs;
@@ -25,7 +24,6 @@ import javafx.beans.property.SimpleObjectProperty;
import javafx.stage.Stage;
import java.io.IOException;
import java.nio.file.ReadOnlyFileSystemException;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
class UnlockWorkflowTest {
@@ -132,19 +130,34 @@ class UnlockWorkflowTest {
private void assertExtensionSwitchedOffRetryRestartsUnlockWith(Stage unlockOwner) throws Exception {
workflow = createWorkflow(unlockOwner);
Mockito.when(dialogs.prepareFSKitExtensionSwitchedOffDialog(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(dialogBuilder);
runFailingWith(new FSKitExtensionSwitchedOffException(new MountFailedException("switched off")));
ArgumentCaptor<Consumer<Stage>> okAction = ArgumentCaptor.captor();
Mockito.verify(dialogs).prepareFSKitExtensionSwitchedOffDialog(Mockito.any(), Mockito.any(), okAction.capture());
var okAction = failWithExtensionSwitchedOffAndCaptureRetry();
var dialogStage = Mockito.mock(Stage.class);
okAction.getValue().accept(dialogStage);
okAction.accept(dialogStage);
Mockito.verify(dialogStage).close();
Mockito.verify(appWindows).startUnlockWorkflow(vault, unlockOwner);
Assertions.assertFalse(vaultSettings.usesReadOnlyMode.get());
}
@Test
public void testExtensionSwitchedOffRetryAbortsWhenVaultIsNotLocked() throws Exception {
var okAction = failWithExtensionSwitchedOffAndCaptureRetry();
Mockito.when(vault.isLocked()).thenReturn(false); // e.g. an unlock from the tray menu started while the dialog was open
okAction.accept(Mockito.mock(Stage.class));
Mockito.verify(appWindows, Mockito.never()).startUnlockWorkflow(Mockito.any(), Mockito.any());
}
private Consumer<Stage> failWithExtensionSwitchedOffAndCaptureRetry() throws Exception {
Mockito.when(dialogs.prepareFSKitExtensionSwitchedOffDialog(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(dialogBuilder);
runFailingWith(new FSKitExtensionSwitchedOffException(new MountFailedException("switched off")));
ArgumentCaptor<Consumer<Stage>> okAction = ArgumentCaptor.captor();
Mockito.verify(dialogs).prepareFSKitExtensionSwitchedOffDialog(Mockito.any(), Mockito.any(), okAction.capture());
return okAction.getValue();
}
@Test
public void testReadOnlyRetryRestartsUnlockWithOwner() throws Exception {
Mockito.when(dialogs.prepareRetryIfReadonlyDialog(Mockito.any(), Mockito.any())).thenReturn(dialogBuilder);
@@ -166,9 +179,7 @@ class UnlockWorkflowTest {
invocation.getArgument(0, Runnable.class).run();
return null;
});
var vaultState = Mockito.mock(VaultState.class);
Mockito.when(vaultState.awaitState(VaultState.Value.LOCKED, 5, TimeUnit.SECONDS)).thenReturn(true);
Mockito.when(vault.stateProperty()).thenReturn(vaultState);
Mockito.when(vault.isLocked()).thenReturn(true);
Mockito.doThrow(exception).when(keyLoadingStrategy).use(Mockito.any());
workflow.run();