implemented cancellable health checks

This commit is contained in:
Sebastian Stenzel
2021-04-14 10:20:34 +02:00
parent 1fab246fcd
commit f3a03c71ec
7 changed files with 138 additions and 86 deletions

View File

@@ -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 ReadOnlyObjectProperty<HealthCheck> selectedCheckProperty() {
return selectedCheck;
public BooleanExpression runningProperty() {
return running;
}
public boolean isReady() {
return ready.get();
}
public BooleanExpression readyProperty() {
return ready;
}
private ObservableList<DiagnosticResult> getSelectedResults() {
if (selectedTask.get() == null) {
return FXCollections.emptyObservableList();
} else {
return selectedTask.get().results();
}
}
}

View File

@@ -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;
};
}
}

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

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

View File

@@ -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>