Merge pull request #1416 from cryptomator/feature/#1228-forcedUnmountDialog

Feature/#1228 forced unmount dialog
This commit is contained in:
Armin Schrenk
2020-12-09 15:04:30 +01:00
committed by GitHub
16 changed files with 465 additions and 6 deletions

View File

@@ -11,6 +11,8 @@ public enum FxmlFile {
CHANGEPASSWORD("/fxml/changepassword.fxml"), //
ERROR("/fxml/error.fxml"), //
FORGET_PASSWORD("/fxml/forget_password.fxml"), //
LOCK_FORCED("/fxml/lock_forced.fxml"), //
LOCK_FAILED("/fxml/lock_failed.fxml"), //
MAIN_WINDOW("/fxml/main_window.fxml"), //
MIGRATION_CAPABILITY_ERROR("/fxml/migration_capability_error.fxml"), //
MIGRATION_IMPOSSIBLE("/fxml/migration_impossible.fxml"),

View File

@@ -65,6 +65,7 @@ public class VaultService {
public Task<Vault> createLockTask(Vault vault, boolean forced) {
Task<Vault> task = new LockVaultTask(vault, forced);
task.setOnSucceeded(evt -> LOG.info("Locked {}", vault.getDisplayName()));
task.setOnFailed(evt -> LOG.info("Failed to lock {}.", vault.getDisplayName(), evt.getSource().getException()));
return task;
}

View File

@@ -11,6 +11,7 @@ import org.cryptomator.integrations.uiappearance.UiAppearanceException;
import org.cryptomator.integrations.uiappearance.UiAppearanceListener;
import org.cryptomator.integrations.uiappearance.UiAppearanceProvider;
import org.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.lock.LockComponent;
import org.cryptomator.ui.mainwindow.MainWindowComponent;
import org.cryptomator.ui.preferences.PreferencesComponent;
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
@@ -42,6 +43,7 @@ public class FxApplication extends Application {
private final Lazy<PreferencesComponent> preferencesWindow;
private final Lazy<QuitComponent> quitWindow;
private final Provider<UnlockComponent.Builder> unlockWindowBuilderProvider;
private final Provider<LockComponent.Builder> lockWindowBuilderProvider;
private final Optional<TrayIntegrationProvider> trayIntegration;
private final Optional<UiAppearanceProvider> appearanceProvider;
private final VaultService vaultService;
@@ -51,11 +53,12 @@ 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, 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> unlockWindowBuilderProvider, Provider<LockComponent.Builder> lockWindowBuilderProvider, Lazy<QuitComponent> quitWindow, 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.quitWindow = quitWindow;
this.trayIntegration = trayIntegration;
this.appearanceProvider = appearanceProvider;
@@ -110,6 +113,13 @@ public class FxApplication extends Application {
});
}
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());
});
}
public void showQuitWindow(QuitResponse response) {
Platform.runLater(() -> {
quitWindow.get().showQuitWindow(response);

View File

@@ -11,6 +11,7 @@ import dagger.Provides;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.lock.LockComponent;
import org.cryptomator.ui.mainwindow.MainWindowComponent;
import org.cryptomator.ui.preferences.PreferencesComponent;
import org.cryptomator.ui.quit.QuitComponent;
@@ -27,7 +28,7 @@ import java.io.UncheckedIOException;
import java.util.Collections;
import java.util.List;
@Module(includes = {UpdateCheckerModule.class}, subcomponents = {MainWindowComponent.class, PreferencesComponent.class, UnlockComponent.class, QuitComponent.class, ErrorComponent.class})
@Module(includes = {UpdateCheckerModule.class}, subcomponents = {MainWindowComponent.class, PreferencesComponent.class, UnlockComponent.class, LockComponent.class, QuitComponent.class, ErrorComponent.class})
abstract class FxApplicationModule {
@Provides

View File

@@ -0,0 +1,39 @@
package org.cryptomator.ui.lock;
import dagger.BindsInstance;
import dagger.Subcomponent;
import org.cryptomator.common.vaults.Vault;
import javax.inject.Named;
import javafx.stage.Stage;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
@LockScoped
@Subcomponent(modules = {LockModule.class})
public interface LockComponent {
ExecutorService defaultExecutorService();
LockWorkflow lockWorkflow();
default Future<Void> startLockWorkflow() {
LockWorkflow workflow = lockWorkflow();
defaultExecutorService().submit(workflow);
return workflow;
}
@Subcomponent.Builder
interface Builder {
@BindsInstance
LockComponent.Builder vault(@LockWindow Vault vault);
@BindsInstance
LockComponent.Builder owner(@Named("lockWindowOwner") Optional<Stage> owner);
LockComponent build();
}
}

View File

@@ -0,0 +1,31 @@
package org.cryptomator.ui.lock;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import javax.inject.Inject;
import javafx.fxml.FXML;
import javafx.stage.Stage;
@LockScoped
public class LockFailedController implements FxController {
private final Stage window;
private final Vault vault;
@Inject
public LockFailedController(@LockWindow Stage window, @LockWindow Vault vault) {
this.window = window;
this.vault = vault;
}
@FXML
public void close() {
window.close();
}
// ----- Getter & Setter -----
public String getVaultName() {
return vault.getDisplayName();
}
}

View File

@@ -0,0 +1,57 @@
package org.cryptomator.ui.lock;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.UserInteractionLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.fxml.FXML;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
@LockScoped
public class LockForcedController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(LockForcedController.class);
private final Stage window;
private final Vault vault;
private final UserInteractionLock<LockModule.ForceLockDecision> forceLockDecisionLock;
@Inject
public LockForcedController(@LockWindow Stage window, @LockWindow Vault vault, UserInteractionLock<LockModule.ForceLockDecision> forceLockDecisionLock) {
this.window = window;
this.vault = vault;
this.forceLockDecisionLock = forceLockDecisionLock;
this.window.setOnHiding(this::windowClosed);
}
@FXML
public void cancel() {
forceLockDecisionLock.interacted(LockModule.ForceLockDecision.CANCEL);
window.close();
}
@FXML
public void confirmForcedLock() {
forceLockDecisionLock.interacted(LockModule.ForceLockDecision.FORCE);
window.close();
}
private void windowClosed(WindowEvent windowEvent) {
// if not already interacted, set the decision to CANCEL
if (forceLockDecisionLock.awaitingInteraction().get()) {
LOG.debug("Lock canceled in force-lock-phase by user.");
forceLockDecisionLock.interacted(LockModule.ForceLockDecision.CANCEL);
}
}
// ----- Getter & Setter -----
public String getVaultName() {
return vault.getDisplayName();
}
}

View File

@@ -0,0 +1,89 @@
package org.cryptomator.ui.lock;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FXMLLoaderFactory;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.common.UserInteractionLock;
import javax.inject.Named;
import javax.inject.Provider;
import javafx.scene.Scene;
import javafx.stage.Modality;
import javafx.stage.Stage;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
@Module
abstract class LockModule {
enum ForceLockDecision {
CANCEL,
FORCE;
}
@Provides
@LockScoped
static UserInteractionLock<LockModule.ForceLockDecision> provideForceLockDecisionLock() {
return new UserInteractionLock<>(null);
}
@Provides
@LockWindow
@LockScoped
static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
return new FXMLLoaderFactory(factories, sceneFactory, resourceBundle);
}
@Provides
@LockWindow
@LockScoped
static Stage provideWindow(StageFactory factory, @LockWindow Vault vault, @Named("lockWindowOwner") Optional<Stage> owner) {
Stage stage = factory.create();
stage.setTitle(vault.getDisplayName());
stage.setResizable(false);
if (owner.isPresent()) {
stage.initOwner(owner.get());
stage.initModality(Modality.WINDOW_MODAL);
} else {
stage.initModality(Modality.APPLICATION_MODAL);
}
return stage;
}
@Provides
@FxmlScene(FxmlFile.LOCK_FORCED)
@LockScoped
static Scene provideForceLockScene(@LockWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/lock_forced.fxml");
}
@Provides
@FxmlScene(FxmlFile.LOCK_FAILED)
@LockScoped
static Scene provideLockFailedScene(@LockWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/lock_failed.fxml");
}
// ------------------
@Binds
@IntoMap
@FxControllerKey(LockForcedController.class)
abstract FxController bindLockForcedController(LockForcedController controller);
@Binds
@IntoMap
@FxControllerKey(LockFailedController.class)
abstract FxController bindLockFailedController(LockFailedController controller);
}

View File

@@ -0,0 +1,13 @@
package org.cryptomator.ui.lock;
import javax.inject.Scope;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Scope
@Documented
@Retention(RetentionPolicy.RUNTIME)
@interface LockScoped {
}

View File

@@ -0,0 +1,14 @@
package org.cryptomator.ui.lock;
import javax.inject.Qualifier;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Documented
@Retention(RUNTIME)
@interface LockWindow {
}

View File

@@ -0,0 +1,105 @@
package org.cryptomator.ui.lock;
import dagger.Lazy;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.common.vaults.Volume;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.UserInteractionLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.stage.Window;
/**
* The sequence of actions performed and checked during lock of a vault.
* <p>
* This class implements the Task interface, sucht that it can run in the background with some possible forground operations/requests to the ui, without blocking the main app.
* If the task state is
* <li>succeeded, the vault was successfully locked;</li>
* <li>canceled, the lock was canceled;</li>
* <li>failed, the lock failed due to an exception.</li>
*/
public class LockWorkflow extends Task<Void> {
private static final Logger LOG = LoggerFactory.getLogger(LockWorkflow.class);
private final Stage lockWindow;
private final Vault vault;
private final UserInteractionLock<LockModule.ForceLockDecision> forceLockDecisionLock;
private final Lazy<Scene> lockForcedScene;
private final Lazy<Scene> lockFailedScene;
@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) {
this.lockWindow = lockWindow;
this.vault = vault;
this.forceLockDecisionLock = forceLockDecisionLock;
this.lockForcedScene = lockForcedScene;
this.lockFailedScene = lockFailedScene;
}
@Override
protected Void call() throws Volume.VolumeException, InterruptedException {
try {
vault.lock(false);
} catch (Volume.VolumeException e) {
LOG.debug("Regular lock of {} failed.", vault.getDisplayName(), e);
var decision = askUserForAction();
switch (decision) {
case FORCE -> vault.lock(true);
case CANCEL -> cancel(false);
}
}
return null;
}
private LockModule.ForceLockDecision askUserForAction() throws InterruptedException {
// show forcedLock dialogue ...
Platform.runLater(() -> {
lockWindow.setScene(lockForcedScene.get());
lockWindow.show();
Window owner = lockWindow.getOwner();
if (owner != null) {
lockWindow.setX(owner.getX() + (owner.getWidth() - lockWindow.getWidth()) / 2);
lockWindow.setY(owner.getY() + (owner.getHeight() - lockWindow.getHeight()) / 2);
} else {
lockWindow.centerOnScreen();
}
});
// ... and wait for answer
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);
}
@Override
protected void failed() {
LOG.warn("Failed to lock {}.", vault.getDisplayName());
vault.setState(VaultState.UNLOCKED);
lockWindow.setScene(lockFailedScene.get());
lockWindow.show();
}
@Override
protected void cancelled() {
LOG.debug("Lock of {} canceled.", vault.getDisplayName());
vault.setState(VaultState.UNLOCKED);
}
}

View File

@@ -6,25 +6,32 @@ import com.google.common.cache.LoadingCache;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.fxapp.FxApplication;
import org.cryptomator.ui.stats.VaultStatisticsComponent;
import javax.inject.Inject;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.fxml.FXML;
import javafx.stage.Stage;
import java.util.Optional;
@MainWindowScoped
public class VaultDetailUnlockedController implements FxController {
private final ReadOnlyObjectProperty<Vault> vault;
private final FxApplication application;
private final VaultService vaultService;
private final Stage mainWindow;
private final LoadingCache<Vault, VaultStatisticsComponent> vaultStats;
private final VaultStatisticsComponent.Builder vaultStatsBuilder;
@Inject
public VaultDetailUnlockedController(ObjectProperty<Vault> vault, VaultService vaultService, VaultStatisticsComponent.Builder vaultStatsBuilder) {
public VaultDetailUnlockedController(ObjectProperty<Vault> vault, FxApplication application, VaultService vaultService, VaultStatisticsComponent.Builder vaultStatsBuilder, @MainWindow Stage mainWindow) {
this.vault = vault;
this.application = application;
this.vaultService = vaultService;
this.mainWindow = mainWindow;
this.vaultStats = CacheBuilder.newBuilder().weakValues().build(CacheLoader.from(this::buildVaultStats));
this.vaultStatsBuilder = vaultStatsBuilder;
}
@@ -40,8 +47,7 @@ public class VaultDetailUnlockedController implements FxController {
@FXML
public void lock() {
vaultService.lock(vault.get(), false);
// TODO count lock attempts, and allow forced lock
application.startLockWorkflow(vault.get(), Optional.of(mainWindow));
}
@FXML

View File

@@ -108,7 +108,7 @@ class TrayMenuController {
}
private void lockVault(Vault vault) {
fxApplicationStarter.get(true).thenAccept(app -> app.getVaultService().lock(vault, false));
fxApplicationStarter.get(true).thenAccept(app -> app.startLockWorkflow(vault, Optional.empty()));
}
private void lockAllVaults(ActionEvent actionEvent) {

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import org.cryptomator.ui.controls.FormattedLabel?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Circle?>
<VBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.lock.LockFailedController"
minWidth="400"
maxWidth="400"
minHeight="145"
spacing="12">
<padding>
<Insets topRightBottomLeft="12"/>
</padding>
<children>
<HBox spacing="12" alignment="CENTER_LEFT" VBox.vgrow="ALWAYS">
<StackPane alignment="CENTER" HBox.hgrow="NEVER">
<Circle styleClass="glyph-icon-red" radius="24"/>
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="TIMES" glyphSize="24"/>
</StackPane>
<VBox spacing="6">
<Label styleClass="label-large" text="%lock.fail.heading"/>
<FormattedLabel format="%lock.fail.message" arg1="${controller.vaultName}" wrapText="true"/>
</VBox>
</HBox>
<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
<Button text="OK" defaultButton="false" VBox.vgrow="ALWAYS" cancelButton="true" onAction="#close"/>
</VBox>
</children>
</VBox>

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import org.cryptomator.ui.controls.FormattedLabel?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Circle?>
<VBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.lock.LockForcedController"
minWidth="400"
maxWidth="400"
minHeight="145"
spacing="12">
<padding>
<Insets topRightBottomLeft="12"/>
</padding>
<children>
<HBox spacing="12" alignment="CENTER_LEFT" VBox.vgrow="ALWAYS">
<StackPane alignment="CENTER" HBox.hgrow="NEVER">
<Circle styleClass="glyph-icon-orange" radius="24"/>
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="EXCLAMATION" glyphSize="24"/>
</StackPane>
<VBox spacing="6">
<Label styleClass="label-large" text="%lock.forced.heading"/>
<FormattedLabel format="%lock.forced.message" arg1="${controller.vaultName}" wrapText="true"/>
</VBox>
</HBox>
<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
<ButtonBar buttonMinWidth="120" buttonOrder="+CI">
<buttons>
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" defaultButton="true" cancelButton="true" onAction="#cancel"/>
<!-- TODO: third button with retry? -->
<Button text="%lock.forced.confirmBtn" ButtonBar.buttonData="FINISH" onAction="#confirmForcedLock"/>
</buttons>
</ButtonBar>
</VBox>
</children>
</VBox>

View File

@@ -108,6 +108,15 @@ unlock.error.heading=Unable to unlock vault
unlock.error.invalidMountPoint.notExisting=Mount point "%s" is not a directory, not empty or does not exist.
unlock.error.invalidMountPoint.existing=Mount point "%s" already exists or parent folder is missing.
# Lock
## Force
lock.forced.heading=Forcefully lock vault?
lock.forced.message=The vault "%s" cannot be locked due to open files or pending operations. You can enforce locking, but unsaved data will be lost and pending read/write operations aborted.
lock.forced.confirmBtn=Force locking
## Failure
lock.fail.heading=Locking vault failed.
lock.fail.message=Vault "%s" could not be locked. Ensure unsaved work is saved elsewhere and important Read/Write operations are finished. In order to close the vault, kill the Cryptomator process.
# Migration
migration.title=Upgrade Vault
## Start