Browse Source

implemented cancellable health checks

Sebastian Stenzel 4 years ago
parent
commit
f3a03c71ec

+ 59 - 34
main/ui/src/main/java/org/cryptomator/ui/health/CheckController.java

@@ -1,23 +1,29 @@
 package org.cryptomator.ui.health;
 
-import org.cryptomator.common.vaults.Vault;
+import com.tobiasdiez.easybind.EasyBind;
+import com.tobiasdiez.easybind.EasyBinding;
+import com.tobiasdiez.easybind.optional.OptionalBinding;
+import dagger.Lazy;
 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.cryptomator.ui.common.FxController;
-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.binding.BooleanExpression;
+import javafx.beans.binding.ObjectBinding;
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.ReadOnlyObjectProperty;
 import javafx.collections.FXCollections;
 import javafx.collections.ObservableList;
+import javafx.concurrent.Worker;
 import javafx.fxml.FXML;
 import javafx.scene.control.ListView;
 import javafx.stage.Stage;
-import java.security.SecureRandom;
+import java.util.Collection;
 import java.util.Objects;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.atomic.AtomicReference;
@@ -27,59 +33,78 @@ public class CheckController implements FxController {
 
 	private final Stage window;
 	private final VaultConfig vaultConfig;
-	private final HealthCheckTaskFactory healthCheckTaskFactory;
 	private final ExecutorService executor;
-	private final ObjectProperty<HealthCheck> selectedCheck;
-	private final ObservableList<HealthCheck> checks;
-	private final ObservableList<DiagnosticResult> results;
+	private final ObjectProperty<HealthCheckTask> selectedTask;
+	private final ObservableList<HealthCheckTask> tasks;
+	private final ObjectBinding<ObservableList<DiagnosticResult>> selectedResults;
+	private final OptionalBinding<Worker.State> selectedTaskState;
+	private final BooleanExpression ready;
+	private final BooleanExpression running;
 
-	public ListView<HealthCheck> checksListView;
+	public ListView<HealthCheckTask> checksListView;
 	public ListView<DiagnosticResult> resultsListView;
 
+
 	@Inject
-	public CheckController(@HealthCheckWindow Stage window, AtomicReference<VaultConfig> vaultConfigRef, HealthCheckTaskFactory healthCheckTaskFactory, ExecutorService executor, ObjectProperty<HealthCheck> selectedCheck) {
+	public CheckController(@HealthCheckWindow Stage window, AtomicReference<VaultConfig> vaultConfigRef, Lazy<Collection<HealthCheckTask>> tasks, ExecutorService executor, ObjectProperty<HealthCheckTask> selectedTask) {
 		this.window = window;
 		this.vaultConfig = Objects.requireNonNull(vaultConfigRef.get());
-		this.healthCheckTaskFactory = healthCheckTaskFactory;
 		this.executor = executor;
-		this.selectedCheck = selectedCheck;
-		this.checks = FXCollections.observableArrayList(HealthCheck.allChecks());
-		this.results = FXCollections.observableArrayList();
+		this.selectedTask = selectedTask;
+		this.selectedResults = Bindings.createObjectBinding(this::getSelectedResults, selectedTask);
+		this.selectedTaskState = EasyBind.wrapNullable(selectedTask).mapObservable(HealthCheckTask::stateProperty);
+		this.ready = BooleanExpression.booleanExpression(selectedTaskState.map(Worker.State.READY::equals).orElse(false));
+		this.running = BooleanExpression.booleanExpression(selectedTaskState.map(Worker.State.RUNNING::equals).orElse(false));
+		this.tasks = FXCollections.observableArrayList(tasks.get());
 	}
 
 	@FXML
 	public void initialize() {
-		checksListView.setItems(checks);
-		checksListView.setCellFactory(this::createCheckListCell);
-		resultsListView.setItems(results);
-		resultsListView.setCellFactory(this::createResultListCell);
-		selectedCheck.bind(checksListView.getSelectionModel().selectedItemProperty());
-	}
-
-	private CheckListCell createCheckListCell(ListView<HealthCheck> list) {
-		return new CheckListCell();
-	}
-
-	private ResultListCell createResultListCell(ListView<DiagnosticResult> list) {
-		return new ResultListCell();
+		checksListView.setItems(tasks);
+		checksListView.setCellFactory(ignored -> new CheckListCell());
+		resultsListView.itemsProperty().bind(selectedResults);
+		resultsListView.setCellFactory(ignored -> new ResultListCell());
+		selectedTask.bind(checksListView.getSelectionModel().selectedItemProperty());
 	}
 
 	@FXML
 	public void runCheck() {
-		executor.execute(healthCheckTaskFactory.newTask(selectedCheck.get(), results::add));
+		assert selectedTask.get() != null;
+		executor.execute(selectedTask.get());
+	}
+	@FXML
+	public void cancelCheck() {
+		assert selectedTask.get() != null;
+		assert selectedTask.get().isRunning();
+		selectedTask.get().cancel();
 	}
-
 	/* Getter/Setter */
 
 	public VaultConfig getVaultConfig() {
 		return vaultConfig;
 	}
 
-	public HealthCheck getSelectedCheck() {
-		return selectedCheck.get();
+	public boolean isRunning() {
+		return running.get();
+	}
+
+	public BooleanExpression runningProperty() {
+		return running;
+	}
+
+	public boolean isReady() {
+		return ready.get();
+	}
+
+	public BooleanExpression readyProperty() {
+		return ready;
 	}
 
-	public ReadOnlyObjectProperty<HealthCheck> selectedCheckProperty() {
-		return selectedCheck;
+	private ObservableList<DiagnosticResult> getSelectedResults() {
+		if (selectedTask.get() == null) {
+			return FXCollections.emptyObservableList();
+		} else {
+			return selectedTask.get().results();
+		}
 	}
 }

+ 31 - 4
main/ui/src/main/java/org/cryptomator/ui/health/CheckListCell.java

@@ -1,16 +1,43 @@
 package org.cryptomator.ui.health;
 
-import org.cryptomator.cryptofs.health.api.HealthCheck;
+import org.cryptomator.ui.controls.FontAwesome5Icon;
+import org.cryptomator.ui.controls.FontAwesome5IconView;
 
+import javafx.beans.value.ObservableValue;
+import javafx.concurrent.Worker;
+import javafx.scene.control.ContentDisplay;
 import javafx.scene.control.ListCell;
 
-class CheckListCell extends ListCell<HealthCheck> {
+class CheckListCell extends ListCell<HealthCheckTask> {
+
+	private final FontAwesome5IconView stateIcon = new FontAwesome5IconView();
 
 	@Override
-	protected void updateItem(HealthCheck item, boolean empty) {
+	protected void updateItem(HealthCheckTask item, boolean empty) {
 		super.updateItem(item, empty);
 		if (item != null) {
-			setText(item.identifier());
+			setText(item.getCheck().identifier());
+			item.stateProperty().addListener(this::stateChanged);
+			setGraphic(stateIcon);
+			stateIcon.setGlyph(glyphForState(item.getState()));
+			setContentDisplay(ContentDisplay.LEFT);
+		} else {
+			setText(null);
+			setContentDisplay(ContentDisplay.TEXT_ONLY);
 		}
 	}
+
+	private void stateChanged(ObservableValue<? extends Worker.State> observable, Worker.State oldState, Worker.State newState) {
+		stateIcon.setGlyph(glyphForState(newState));
+	}
+
+	private FontAwesome5Icon glyphForState(Worker.State state) {
+		// TODO choose appropriate glyphs
+		return switch (state) {
+			case READY, SCHEDULED -> FontAwesome5Icon.ANCHOR;
+			case RUNNING -> FontAwesome5Icon.SPINNER;
+			case FAILED, CANCELLED -> FontAwesome5Icon.EXCLAMATION_TRIANGLE;
+			case SUCCEEDED -> FontAwesome5Icon.CHECK;
+		};
+	}
 }

+ 13 - 3
main/ui/src/main/java/org/cryptomator/ui/health/HealthCheckModule.java

@@ -3,6 +3,7 @@ package org.cryptomator.ui.health;
 import dagger.Binds;
 import dagger.Module;
 import dagger.Provides;
+import dagger.multibindings.ElementsIntoSet;
 import dagger.multibindings.IntoMap;
 import org.cryptomator.common.vaults.Vault;
 import org.cryptomator.cryptofs.VaultConfig;
@@ -26,9 +27,12 @@ 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.util.Map;
 import java.util.Optional;
 import java.util.ResourceBundle;
+import java.util.Set;
 import java.util.concurrent.atomic.AtomicReference;
 
 @Module(subcomponents = {KeyLoadingComponent.class})
@@ -36,8 +40,8 @@ abstract class HealthCheckModule {
 
 	@Provides
 	@HealthCheckScoped
-	static ObjectProperty<HealthCheck> selectedHealthCheck() {
-		return new SimpleObjectProperty<HealthCheck>();
+	static ObjectProperty<HealthCheckTask> selectedHealthCheckTask() {
+		return new SimpleObjectProperty<>();
 	}
 
 	@Provides
@@ -59,6 +63,12 @@ abstract class HealthCheckModule {
 		return compBuilder.vault(vault).window(window).build().keyloadingStrategy();
 	}
 
+	@Provides
+	@HealthCheckScoped
+	static Collection<HealthCheckTask> provideHealthCheckTasks(@HealthCheckWindow Vault vault, AtomicReference<Masterkey> masterkeyRef, AtomicReference<VaultConfig> vaultConfigRef, SecureRandom csprng) {
+		return HealthCheck.allChecks().stream().map(check -> new HealthCheckTask(vault.getPath(), vaultConfigRef.get(), masterkeyRef.get(), csprng, check)).toList();
+	}
+
 	@Provides
 	@HealthCheckWindow
 	@HealthCheckScoped
@@ -72,7 +82,7 @@ abstract class HealthCheckModule {
 	static Stage provideStage(StageFactory factory, @MainWindow Stage owner, ResourceBundle resourceBundle, ChangeListener<Boolean> showingListener) {
 		Stage stage = factory.create();
 		stage.setTitle(resourceBundle.getString("health.title"));
-		stage.setResizable(false);
+		stage.setResizable(true);
 		stage.initModality(Modality.WINDOW_MODAL);
 		stage.initOwner(owner);
 		stage.showingProperty().addListener(showingListener);

+ 30 - 8
main/ui/src/main/java/org/cryptomator/ui/health/HealthCheckTask.java

@@ -8,7 +8,12 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import javafx.application.Platform;
+import javafx.beans.binding.BooleanBinding;
+import javafx.beans.property.ReadOnlyBooleanProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
 import javafx.concurrent.Task;
+import javafx.concurrent.Worker;
 import java.nio.file.Path;
 import java.security.SecureRandom;
 import java.util.Objects;
@@ -24,15 +29,15 @@ class HealthCheckTask extends Task<Void> {
 	private final Masterkey masterkey;
 	private final SecureRandom csprng;
 	private final HealthCheck check;
-	private final Consumer<DiagnosticResult> resultConsumer;
+	private final ObservableList<DiagnosticResult> results;
 
-	public HealthCheckTask(Path vaultPath, VaultConfig vaultConfig, Masterkey masterkey, SecureRandom csprng, HealthCheck check, Consumer<DiagnosticResult> resultConsumer) {
-		this.vaultPath = vaultPath;
-		this.vaultConfig = vaultConfig;
-		this.masterkey = masterkey;
-		this.csprng = csprng;
+	public HealthCheckTask(Path vaultPath, VaultConfig vaultConfig, Masterkey masterkey, SecureRandom csprng, HealthCheck check) {
+		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.resultConsumer = resultConsumer;
+		this.results = FXCollections.observableArrayList();
 	}
 
 	@Override
@@ -42,7 +47,13 @@ class HealthCheckTask extends Task<Void> {
 				if (isCancelled()) {
 					throw new CancellationException();
 				}
-				Platform.runLater(() -> resultConsumer.accept(result));
+				// FIXME: slowdown for demonstration purposes only:
+				try {
+					Thread.sleep(200);
+				} catch (InterruptedException e) {
+					e.printStackTrace();
+				}
+				Platform.runLater(() -> results.add(result));
 			});
 		}
 		return null;
@@ -57,4 +68,15 @@ class HealthCheckTask extends Task<Void> {
 	protected void done() {
 		LOG.info("finished {}", check.identifier());
 	}
+
+	/* Getter */
+
+	public ObservableList<DiagnosticResult> results() {
+		return results;
+	}
+
+	public HealthCheck getCheck() {
+		return check;
+	}
+
 }

+ 0 - 35
main/ui/src/main/java/org/cryptomator/ui/health/HealthCheckTaskFactory.java

@@ -1,35 +0,0 @@
-package org.cryptomator.ui.health;
-
-import org.cryptomator.common.vaults.Vault;
-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 javax.inject.Inject;
-import java.security.SecureRandom;
-import java.util.Objects;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Consumer;
-
-@HealthCheckScoped
-class HealthCheckTaskFactory {
-
-	private final Vault vault;
-	private final Masterkey masterkey;
-	private final VaultConfig vaultConfig;
-	private final SecureRandom csprng;
-
-	@Inject
-	public HealthCheckTaskFactory(@HealthCheckWindow Vault vault, AtomicReference<Masterkey> masterkeyRef, AtomicReference<VaultConfig> vaultConfigRef, SecureRandom csprng) {
-		this.vault = vault;
-		this.masterkey = Objects.requireNonNull(masterkeyRef.get());
-		this.vaultConfig = Objects.requireNonNull(vaultConfigRef.get());
-		this.csprng = csprng;
-	}
-
-	public HealthCheckTask newTask(HealthCheck healthCheck, Consumer<DiagnosticResult> resultConsumer) {
-		return new HealthCheckTask(vault.getPath(), vaultConfig, masterkey, csprng, healthCheck, resultConsumer);
-	}
-
-}

+ 2 - 0
main/ui/src/main/java/org/cryptomator/ui/health/ResultListCell.java

@@ -12,6 +12,8 @@ class ResultListCell extends ListCell<DiagnosticResult> {
 		super.updateItem(item, empty);
 		if (item != null) {
 			setText(item.toString());
+		} else {
+			setText(null);
 		}
 	}
 }

+ 3 - 2
main/ui/src/main/resources/fxml/health_check.fxml

@@ -24,9 +24,10 @@
 
 			<ListView fx:id="resultsListView"/>
 
-			<ButtonBar buttonMinWidth="120" buttonOrder="+X">
+			<ButtonBar buttonMinWidth="120" buttonOrder="+CA">
 				<buttons>
-					<Button text="TODO run check" ButtonBar.buttonData="NEXT_FORWARD" defaultButton="true" onAction="#runCheck" disable="${selectedCheck.isNull}"/>
+					<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" onAction="#cancelCheck" disable="${!controller.running}"/>
+					<Button text="TODO run check" ButtonBar.buttonData="APPLY" defaultButton="true" onAction="#runCheck" disable="${!controller.ready}"/>
 				</buttons>
 			</ButtonBar>
 		</VBox>