Merge pull request #1712 from cryptomator/feature/improve-health-check

Refactor health check to improve UX
This commit is contained in:
Armin Schrenk
2021-07-26 18:33:40 +02:00
committed by GitHub
43 changed files with 1106 additions and 778 deletions

View File

@@ -44,6 +44,7 @@ module org.cryptomator.desktop {
opens org.cryptomator.ui.controls to javafx.fxml;
opens org.cryptomator.ui.forgetPassword to javafx.fxml;
opens org.cryptomator.ui.fxapp to javafx.fxml;
opens org.cryptomator.ui.health to javafx.fxml;
opens org.cryptomator.ui.keyloading.masterkeyfile to javafx.fxml;
opens org.cryptomator.ui.mainwindow to javafx.fxml;
opens org.cryptomator.ui.migration to javafx.fxml;

View File

@@ -19,6 +19,7 @@ import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags;
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
import org.cryptomator.cryptofs.VaultConfig;
import org.cryptomator.cryptofs.VaultConfig.UnverifiedVaultConfig;
import org.cryptomator.cryptofs.VaultConfigLoadException;
import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.cryptolib.api.MasterkeyLoader;
@@ -327,6 +328,14 @@ public class Vault {
return stats;
}
/**
* Attempts to read the vault config file and parse it without verifying its integrity.
*
* @return an unverified vault config
* @throws VaultConfigLoadException if the read file cannot be properly parsed
* @throws IOException if reading the file fails
*
*/
public UnverifiedVaultConfig getUnverifiedVaultConfig() throws IOException {
Path configPath = getPath().resolve(org.cryptomator.common.Constants.VAULTCONFIG_FILENAME);
String token = Files.readString(configPath, StandardCharsets.US_ASCII);

View File

@@ -12,6 +12,7 @@ public enum FxmlFile {
ERROR("/fxml/error.fxml"), //
FORGET_PASSWORD("/fxml/forget_password.fxml"), //
HEALTH_START("/fxml/health_start.fxml"), //
HEALTH_START_FAIL("/fxml/health_start_fail.fxml"), //
HEALTH_CHECK_LIST("/fxml/health_check_list.fxml"), //
LOCK_FORCED("/fxml/lock_forced.fxml"), //
LOCK_FAILED("/fxml/lock_failed.fxml"), //

View File

@@ -8,6 +8,8 @@ public enum FontAwesome5Icon {
ARROW_UP("\uF062"), //
BAN("\uF05E"), //
BUG("\uF188"), //
CARET_DOWN("\uF0D7"), //
CARET_RIGHT("\uF0Da"), //
CHECK("\uF00C"), //
CLOCK("\uF017"), //
COG("\uF013"), //
@@ -20,6 +22,7 @@ public enum FontAwesome5Icon {
EXCLAMATION_TRIANGLE("\uF071"), //
EYE("\uF06E"), //
EYE_SLASH("\uF070"), //
FAST_FORWARD("\uF050"), //
FILE("\uF15B"), //
FILE_IMPORT("\uF56F"), //
FOLDER_OPEN("\uF07C"), //

View File

@@ -21,8 +21,8 @@ public class FontAwesome5IconView extends Text {
private static final String FONT_PATH = "/css/fontawesome5-free-solid.otf";
private static final Font FONT;
private final ObjectProperty<FontAwesome5Icon> glyph = new SimpleObjectProperty<>(this, "glyph", DEFAULT_GLYPH);
private final DoubleProperty glyphSize = new SimpleDoubleProperty(this, "glyphSize", DEFAULT_GLYPH_SIZE);
protected final ObjectProperty<FontAwesome5Icon> glyph = new SimpleObjectProperty<>(this, "glyph", DEFAULT_GLYPH);
protected final DoubleProperty glyphSize = new SimpleDoubleProperty(this, "glyphSize", DEFAULT_GLYPH_SIZE);
static {
try {

View File

@@ -1,35 +0,0 @@
package org.cryptomator.ui.health;
import com.google.common.base.Preconditions;
import com.google.common.base.Suppliers;
import dagger.Lazy;
import javax.inject.Inject;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import java.util.Collection;
import java.util.Iterator;
import java.util.concurrent.ExecutorService;
import java.util.function.Supplier;
public class BatchService extends Service<Void> {
private final Iterator<HealthCheckTask> remainingTasks;
public BatchService(Iterable<HealthCheckTask> tasks) {
this.remainingTasks = tasks.iterator();
}
@Override
protected Task<Void> createTask() {
Preconditions.checkState(remainingTasks.hasNext(), "No remaining tasks");
return remainingTasks.next();
}
@Override
protected void succeeded() {
if (remainingTasks.hasNext()) {
this.restart();
}
}
}

View File

@@ -0,0 +1,103 @@
package org.cryptomator.ui.health;
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
import org.cryptomator.cryptofs.health.api.HealthCheck;
import javafx.beans.Observable;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
public class Check {
private final HealthCheck check;
private final BooleanProperty chosenForExecution = new SimpleBooleanProperty(false);
private final ObjectProperty<CheckState> state = new SimpleObjectProperty<>(CheckState.RUNNABLE);
private final ObservableList<Result> results = FXCollections.observableArrayList(Result::observables);
private final ObjectProperty<DiagnosticResult.Severity> highestResultSeverity = new SimpleObjectProperty<>(null);
private final ObjectProperty<Throwable> error = new SimpleObjectProperty<>(null);
private final BooleanBinding isInReRunState = state.isNotEqualTo(CheckState.RUNNING).or(state.isNotEqualTo(CheckState.SCHEDULED));
Check(HealthCheck check) {
this.check = check;
}
String getName() {
return check.name();
}
HealthCheck getHealthCheck() {
return check;
}
BooleanProperty chosenForExecutionProperty() {
return chosenForExecution;
}
boolean isChosenForExecution() {
return chosenForExecution.get();
}
ObjectProperty<CheckState> stateProperty() {
return state;
}
CheckState getState() {
return state.get();
}
void setState(CheckState newState) {
state.set(newState);
}
ObjectProperty<Throwable> errorProperty() {
return error;
}
Throwable getError() {
return error.get();
}
void setError(Throwable t) {
error.set(t);
}
ObjectProperty<DiagnosticResult.Severity> highestResultSeverityProperty() {
return highestResultSeverity;
}
DiagnosticResult.Severity getHighestResultSeverity() {
return highestResultSeverity.get();
}
void setHighestResultSeverity(DiagnosticResult.Severity severity) {
highestResultSeverity.set(severity);
}
boolean isInReRunState() {
return isInReRunState.get();
}
enum CheckState {
RUNNABLE,
SCHEDULED,
RUNNING,
SUCCEEDED,
SKIPPED,
ERROR,
CANCELLED;
}
ObservableList<Result> getResults() {
return results;
}
Observable[] observables() {
return new Observable[]{chosenForExecution, state, results, error};
}
}

View File

@@ -12,11 +12,8 @@ import javafx.beans.binding.Binding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.concurrent.Worker;
import javafx.fxml.FXML;
import javafx.scene.control.ListView;
import java.time.Duration;
import java.util.ResourceBundle;
import java.util.function.Function;
import java.util.stream.Stream;
@@ -24,50 +21,50 @@ import java.util.stream.Stream;
public class CheckDetailController implements FxController {
private final EasyObservableList<Result> results;
private final OptionalBinding<Worker.State> taskState;
private final Binding<String> taskName;
private final Binding<String> taskDuration;
private final Binding<Boolean> taskRunning;
private final Binding<Boolean> taskScheduled;
private final Binding<Boolean> taskFinished;
private final Binding<Boolean> taskNotStarted;
private final Binding<Boolean> taskSucceeded;
private final Binding<Boolean> taskFailed;
private final Binding<Boolean> taskCancelled;
private final ObjectProperty<Check> check;
private final OptionalBinding<Check.CheckState> checkState;
private final Binding<String> checkName;
private final Binding<Boolean> checkRunning;
private final Binding<Boolean> checkScheduled;
private final Binding<Boolean> checkFinished;
private final Binding<Boolean> checkSkipped;
private final Binding<Boolean> checkSucceeded;
private final Binding<Boolean> checkFailed;
private final Binding<Boolean> checkCancelled;
private final Binding<Number> countOfWarnSeverity;
private final Binding<Number> countOfCritSeverity;
private final Binding<Boolean> warnOrCritsExist;
private final ResultListCellFactory resultListCellFactory;
private final ResourceBundle resourceBundle;
public ListView<Result> resultsListView;
private Subscription resultSubscription;
@Inject
public CheckDetailController(ObjectProperty<HealthCheckTask> selectedTask, ResultListCellFactory resultListCellFactory, ResourceBundle resourceBundle) {
public CheckDetailController(ObjectProperty<Check> selectedTask, ResultListCellFactory resultListCellFactory) {
this.resultListCellFactory = resultListCellFactory;
this.resourceBundle = resourceBundle;
this.results = EasyBind.wrapList(FXCollections.observableArrayList());
this.taskState = EasyBind.wrapNullable(selectedTask).mapObservable(HealthCheckTask::stateProperty);
this.taskName = EasyBind.wrapNullable(selectedTask).map(HealthCheckTask::getTitle).orElse("");
this.taskDuration = EasyBind.wrapNullable(selectedTask).mapObservable(HealthCheckTask::durationInMillisProperty).orElse(-1L).map(this::millisToReadAbleDuration);
this.taskRunning = EasyBind.wrapNullable(selectedTask).mapObservable(HealthCheckTask::runningProperty).orElse(false); //TODO: DOES NOT WORK
this.taskScheduled = taskState.map(Worker.State.SCHEDULED::equals).orElse(false);
this.taskNotStarted = taskState.map(Worker.State.READY::equals).orElse(false);
this.taskSucceeded = taskState.map(Worker.State.SUCCEEDED::equals).orElse(false);
this.taskFailed = taskState.map(Worker.State.FAILED::equals).orElse(false);
this.taskCancelled = taskState.map(Worker.State.CANCELLED::equals).orElse(false);
this.taskFinished = EasyBind.combine(taskSucceeded, taskFailed, taskCancelled, (a, b, c) -> a || b || c);
this.check = selectedTask;
this.checkState = EasyBind.wrapNullable(selectedTask).mapObservable(Check::stateProperty);
this.checkName = EasyBind.wrapNullable(selectedTask).map(Check::getName).orElse("");
this.checkRunning = checkState.map(Check.CheckState.RUNNING::equals).orElse(false);
this.checkScheduled = checkState.map(Check.CheckState.SCHEDULED::equals).orElse(false);
this.checkSkipped = checkState.map(Check.CheckState.SKIPPED::equals).orElse(false);
this.checkSucceeded = checkState.map(Check.CheckState.SUCCEEDED::equals).orElse(false);
this.checkFailed = checkState.map(Check.CheckState.ERROR::equals).orElse(false);
this.checkCancelled = checkState.map(Check.CheckState.CANCELLED::equals).orElse(false);
this.checkFinished = EasyBind.combine(checkSucceeded, checkFailed, checkCancelled, (a, b, c) -> a || b || c);
this.countOfWarnSeverity = results.reduce(countSeverity(DiagnosticResult.Severity.WARN));
this.countOfCritSeverity = results.reduce(countSeverity(DiagnosticResult.Severity.CRITICAL));
this.warnOrCritsExist = EasyBind.combine(checkSucceeded, countOfWarnSeverity, countOfCritSeverity, (suceeded, warns, crits) -> suceeded && (warns.longValue() > 0 || crits.longValue() > 0) );
selectedTask.addListener(this::selectedTaskChanged);
}
private void selectedTaskChanged(ObservableValue<? extends HealthCheckTask> observable, HealthCheckTask oldValue, HealthCheckTask newValue) {
private void selectedTaskChanged(ObservableValue<? extends Check> observable, Check oldValue, Check newValue) {
if (resultSubscription != null) {
resultSubscription.unsubscribe();
}
if (newValue != null) {
resultSubscription = EasyBind.bindContent(results, newValue.results());
resultSubscription = EasyBind.bindContent(results, newValue.getResults());
}
}
@@ -83,20 +80,12 @@ public class CheckDetailController implements FxController {
/* Getter/Setter */
public String getTaskName() {
return taskName.getValue();
public String getCheckName() {
return checkName.getValue();
}
public Binding<String> taskNameProperty() {
return taskName;
}
public String getTaskDuration() {
return taskDuration.getValue();
}
public Binding<String> taskDurationProperty() {
return taskDuration;
public Binding<String> checkNameProperty() {
return checkName;
}
public long getCountOfWarnSeverity() {
@@ -115,77 +104,75 @@ public class CheckDetailController implements FxController {
return countOfCritSeverity;
}
public boolean isTaskRunning() {
return taskRunning.getValue();
public boolean isCheckRunning() {
return checkRunning.getValue();
}
public Binding<Boolean> taskRunningProperty() {
return taskRunning;
public Binding<Boolean> checkRunningProperty() {
return checkRunning;
}
public boolean isTaskFinished() {
return taskFinished.getValue();
public boolean isCheckFinished() {
return checkFinished.getValue();
}
public Binding<Boolean> taskFinishedProperty() {
return taskFinished;
public Binding<Boolean> checkFinishedProperty() {
return checkFinished;
}
public boolean isTaskScheduled() {
return taskScheduled.getValue();
public boolean isCheckScheduled() {
return checkScheduled.getValue();
}
public Binding<Boolean> taskScheduledProperty() {
return taskScheduled;
public Binding<Boolean> checkScheduledProperty() {
return checkScheduled;
}
public boolean isTaskNotStarted() {
return taskNotStarted.getValue();
public boolean isCheckSkipped() {
return checkSkipped.getValue();
}
public Binding<Boolean> taskNotStartedProperty() {
return taskNotStarted;
public Binding<Boolean> checkSkippedProperty() {
return checkSkipped;
}
public boolean isTaskSucceeded() {
return taskSucceeded.getValue();
public boolean isCheckSucceeded() {
return checkSucceeded.getValue();
}
public Binding<Boolean> taskSucceededProperty() {
return taskSucceeded;
public Binding<Boolean> checkSucceededProperty() {
return checkSucceeded;
}
public boolean isTaskFailed() {
return taskFailed.getValue();
public boolean isCheckFailed() {
return checkFailed.getValue();
}
public Binding<Boolean> taskFailedProperty() {
return taskFailed;
public Binding<Boolean> checkFailedProperty() {
return checkFailed;
}
public boolean isTaskCancelled() {
return taskCancelled.getValue();
public boolean isCheckCancelled() {
return checkCancelled.getValue();
}
public Binding<Boolean> taskCancelledProperty() {
return taskCancelled;
public Binding<Boolean> warnOrCritsExistProperty() {
return warnOrCritsExist;
}
private String millisToReadAbleDuration(Number millis) {
Duration tmp = Duration.ofMillis(millis.longValue());
long hours = tmp.toHoursPart();
long minutes = tmp.toMinutesPart();
long seconds = tmp.toSecondsPart();
if (hours != 0) {
String hms_format = resourceBundle.getString("health.check.detail.hmsFormat");
return String.format(hms_format, hours, minutes, seconds);
} else if (minutes != 0) {
String ms_format = resourceBundle.getString("health.check.detail.msFormat");
return String.format(ms_format, minutes, seconds);
} else {
String s_format = resourceBundle.getString("health.check.detail.sFormat");
return String.format(s_format, seconds);
}
public boolean isWarnOrCritsExist() {
return warnOrCritsExist.getValue();
}
public Binding<Boolean> checkCancelledProperty() {
return checkCancelled;
}
public ObjectProperty<Check> checkProperty() {
return check;
}
public Check getCheck() {
return check.get();
}
}

View File

@@ -0,0 +1,109 @@
package org.cryptomator.ui.health;
import com.google.common.collect.Comparators;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.VaultConfig;
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
import org.cryptomator.cryptolib.api.CryptorProvider;
import org.cryptomator.cryptolib.api.Masterkey;
import javax.inject.Inject;
import javafx.application.Platform;
import javafx.concurrent.Task;
import java.nio.file.Path;
import java.security.SecureRandom;
import java.util.List;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.atomic.AtomicReference;
@HealthCheckScoped
public class CheckExecutor {
private final Path vaultPath;
private final SecureRandom csprng;
private final Masterkey masterkey;
private final VaultConfig vaultConfig;
private final ExecutorService sequentialExecutor;
private final BlockingDeque<CheckTask> tasksToExecute;
@Inject
public CheckExecutor(@HealthCheckWindow Vault vault, AtomicReference<Masterkey> masterkeyRef, AtomicReference<VaultConfig> vaultConfigRef, SecureRandom csprng) {
this.vaultPath = vault.getPath();
this.masterkey = masterkeyRef.get();
this.vaultConfig = vaultConfigRef.get();
this.csprng = csprng;
this.tasksToExecute = new LinkedBlockingDeque<>();
this.sequentialExecutor = Executors.newSingleThreadExecutor();
}
public synchronized void executeBatch(List<Check> checks) {
checks.stream().map(c -> {
c.setState(Check.CheckState.SCHEDULED);
var task = new CheckTask(c);
tasksToExecute.addLast(task);
return task;
}).forEach(sequentialExecutor::submit);
}
public synchronized void cancel() {
CheckTask task;
while ((task = tasksToExecute.pollLast()) != null) {
task.cancel(true);
}
}
private class CheckTask extends Task<Void> {
private final Check c;
private DiagnosticResult.Severity highestResultSeverity = DiagnosticResult.Severity.GOOD;
CheckTask(Check c) {
this.c = c;
}
@Override
protected Void call() throws Exception {
try (var masterkeyClone = masterkey.clone(); //
var cryptor = CryptorProvider.forScheme(vaultConfig.getCipherCombo()).provide(masterkeyClone, csprng)) {
c.getHealthCheck().check(vaultPath, vaultConfig, masterkeyClone, cryptor, diagnosis -> {
Platform.runLater(() -> c.getResults().add(Result.create(diagnosis)));
highestResultSeverity = Comparators.max(highestResultSeverity, diagnosis.getSeverity());
});
}
return null;
}
@Override
protected void running() {
c.setState(Check.CheckState.RUNNING);
}
@Override
protected void cancelled() {
c.setState(Check.CheckState.CANCELLED);
}
@Override
protected void succeeded() {
c.setState(Check.CheckState.SUCCEEDED);
c.setHighestResultSeverity(highestResultSeverity);
}
@Override
protected void failed() {
c.setState(Check.CheckState.ERROR);
c.setError(this.getException());
}
@Override
protected void done() {
tasksToExecute.remove(this);
}
}
}

View File

@@ -1,71 +0,0 @@
package org.cryptomator.ui.health;
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
import org.cryptomator.ui.controls.FontAwesome5Icon;
import org.cryptomator.ui.controls.FontAwesome5IconView;
import javafx.beans.binding.Bindings;
import javafx.concurrent.Worker;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.ListCell;
import java.util.function.Predicate;
class CheckListCell extends ListCell<HealthCheckTask> {
private final FontAwesome5IconView stateIcon = new FontAwesome5IconView();
private CheckBox checkBox = new CheckBox();
CheckListCell() {
setPadding(new Insets(6));
setAlignment(Pos.CENTER_LEFT);
setContentDisplay(ContentDisplay.LEFT);
getStyleClass().add("label");
}
@Override
protected void updateItem(HealthCheckTask item, boolean empty) {
super.updateItem(item, empty);
if (item != null) {
setText(item.getTitle());
graphicProperty().bind(Bindings.createObjectBinding(() -> graphicForState(item.getState()), item.stateProperty()));
stateIcon.glyphProperty().bind(Bindings.createObjectBinding(() -> glyphForState(item), item.stateProperty()));
checkBox.selectedProperty().bindBidirectional(item.chosenForExecutionProperty());
} else {
graphicProperty().unbind();
setGraphic(null);
setText(null);
checkBox.selectedProperty().unbind();
}
}
private Node graphicForState(Worker.State state) {
return switch (state) {
case READY -> checkBox;
case SCHEDULED, RUNNING, FAILED, CANCELLED, SUCCEEDED -> stateIcon;
};
}
private FontAwesome5Icon glyphForState(HealthCheckTask item) {
return switch (item.getState()) {
case READY -> FontAwesome5Icon.COG; //just a placeholder
case SCHEDULED -> FontAwesome5Icon.CLOCK;
case RUNNING -> FontAwesome5Icon.SPINNER;
case FAILED -> FontAwesome5Icon.EXCLAMATION_TRIANGLE;
case CANCELLED -> FontAwesome5Icon.BAN;
case SUCCEEDED -> checkFoundProblems(item) ? FontAwesome5Icon.EXCLAMATION_TRIANGLE : FontAwesome5Icon.CHECK;
};
}
private boolean checkFoundProblems(HealthCheckTask item) {
Predicate<DiagnosticResult.Severity> isProblem = severity -> switch (severity) {
case WARN, CRITICAL -> true;
case INFO, GOOD -> false;
};
return item.results().stream().map(Result::diagnosis).map(DiagnosticResult::getSeverity).anyMatch(isProblem);
}
}

View File

@@ -0,0 +1,70 @@
package org.cryptomator.ui.health;
import com.tobiasdiez.easybind.EasyBind;
import com.tobiasdiez.easybind.Subscription;
import org.cryptomator.ui.common.FxController;
import javax.inject.Inject;
import javafx.beans.binding.Binding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.CheckBox;
import java.util.ArrayList;
import java.util.List;
public class CheckListCellController implements FxController {
private final ObjectProperty<Check> check;
private final Binding<String> checkName;
private final Binding<Boolean> checkRunnable;
private final List<Subscription> subscriptions;
/* FXML */
public CheckBox forRunSelectedCheckBox;
@Inject
public CheckListCellController() {
check = new SimpleObjectProperty<>();
checkRunnable = EasyBind.wrapNullable(check).mapObservable(Check::stateProperty).map(Check.CheckState.RUNNABLE::equals).orElse(false);
checkName = EasyBind.wrapNullable(check).map(Check::getName).orElse("");
subscriptions = new ArrayList<>();
}
public void initialize() {
subscriptions.add(EasyBind.subscribe(check, c -> {
forRunSelectedCheckBox.selectedProperty().unbind();
if (c != null) {
forRunSelectedCheckBox.selectedProperty().bindBidirectional(c.chosenForExecutionProperty());
}
}));
}
public ObjectProperty<Check> checkProperty() {
return check;
}
public Check getCheck() {
return check.get();
}
public void setCheck(Check c) {
check.set(c);
}
public Binding<String> checkNameProperty() {
return checkName;
}
public String getCheckName() {
return checkName.getValue();
}
public Binding<Boolean> checkRunnableProperty() {
return checkRunnable;
}
public boolean isCheckRunnable() {
return checkRunnable.getValue();
}
}

View File

@@ -0,0 +1,58 @@
package org.cryptomator.ui.health;
import org.cryptomator.ui.common.FxmlLoaderFactory;
import javax.inject.Inject;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.util.Callback;
import java.io.IOException;
import java.io.UncheckedIOException;
// unscoped because each cell needs its own controller
public class CheckListCellFactory implements Callback<ListView<Check>, ListCell<Check>> {
private final FxmlLoaderFactory fxmlLoaders;
@Inject
CheckListCellFactory(@HealthCheckWindow FxmlLoaderFactory fxmlLoaders) {
this.fxmlLoaders = fxmlLoaders;
}
@Override
public ListCell<Check> call(ListView<Check> param) {
try {
FXMLLoader fxmlLoader = fxmlLoaders.load("/fxml/health_check_listcell.fxml");
return new CheckListCellFactory.Cell(fxmlLoader.getRoot(), fxmlLoader.getController());
} catch (IOException e) {
throw new UncheckedIOException("Failed to load /fxml/health_check_listcell.fxml.", e);
}
}
private static class Cell extends ListCell<Check> {
private final Parent node;
private final CheckListCellController controller;
public Cell(Parent node, CheckListCellController controller) {
this.node = node;
this.controller = controller;
}
@Override
protected void updateItem(Check item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setText(null);
setGraphic(null);
} else {
controller.setCheck(item);
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
setGraphic(node);
}
}
}
}

View File

@@ -1,8 +1,6 @@
package org.cryptomator.ui.health;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicates;
import com.tobiasdiez.easybind.EasyBind;
import dagger.Lazy;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxController;
@@ -10,110 +8,100 @@ 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.IntegerBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.concurrent.Worker;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ListView;
import javafx.scene.control.SelectionMode;
import javafx.stage.Stage;
import java.io.IOException;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
@HealthCheckScoped
public class CheckListController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(CheckListController.class);
private static final Set<Worker.State> END_STATES = Set.of(Worker.State.FAILED, Worker.State.CANCELLED, Worker.State.SUCCEEDED);
private final Stage window;
private final ObservableList<HealthCheckTask> tasks;
private final FilteredList<HealthCheckTask> chosenTasks;
private final ObservableList<Check> checks;
private final CheckExecutor checkExecutor;
private final FilteredList<Check> chosenChecks;
private final ReportWriter reportWriter;
private final ExecutorService executorService;
private final ObjectProperty<HealthCheckTask> selectedTask;
private final ObjectProperty<Check> selectedCheck;
private final BooleanBinding mainRunStarted; //TODO: rerunning not considered for now
private final BooleanBinding somethingsRunning;
private final Lazy<ErrorComponent.Builder> errorComponentBuilder;
private final SimpleObjectProperty<Worker<?>> runningTask;
private final Binding<Boolean> running;
private final Binding<Boolean> finished;
private final IntegerBinding chosenTaskCount;
private final BooleanBinding anyCheckSelected;
private final BooleanProperty showResultScreen;
private final CheckListCellFactory listCellFactory;
/* FXML */
public ListView<HealthCheckTask> checksListView;
public ListView<Check> checksListView;
@Inject
public CheckListController(@HealthCheckWindow Stage window, Lazy<List<HealthCheckTask>> tasks, ReportWriter reportWriteTask, ObjectProperty<HealthCheckTask> selectedTask, ExecutorService executorService, Lazy<ErrorComponent.Builder> errorComponentBuilder) {
public CheckListController(@HealthCheckWindow Stage window, List<Check> checks, CheckExecutor checkExecutor, ReportWriter reportWriteTask, ObjectProperty<Check> selectedCheck, Lazy<ErrorComponent.Builder> errorComponentBuilder, CheckListCellFactory listCellFactory) {
this.window = window;
this.tasks = FXCollections.observableList(tasks.get(), HealthCheckTask::observables);
this.chosenTasks = this.tasks.filtered(HealthCheckTask::isChosenForExecution);
this.checks = FXCollections.observableList(checks, Check::observables);
this.checkExecutor = checkExecutor;
this.listCellFactory = listCellFactory;
this.chosenChecks = this.checks.filtered(Check::isChosenForExecution);
this.reportWriter = reportWriteTask;
this.executorService = executorService;
this.selectedTask = selectedTask;
this.selectedCheck = selectedCheck;
this.errorComponentBuilder = errorComponentBuilder;
this.runningTask = new SimpleObjectProperty<>();
this.running = EasyBind.wrapNullable(runningTask).mapObservable(Worker::runningProperty).orElse(false);
this.finished = EasyBind.wrapNullable(runningTask).mapObservable(Worker::stateProperty).map(END_STATES::contains).orElse(false);
this.chosenTaskCount = Bindings.size(this.chosenTasks);
this.anyCheckSelected = selectedTask.isNotNull();
this.showResultScreen = new SimpleBooleanProperty(false);
this.chosenTaskCount = Bindings.size(this.chosenChecks);
this.mainRunStarted = Bindings.isEmpty(this.checks.filtered(c -> c.getState() == Check.CheckState.RUNNABLE));
this.somethingsRunning = Bindings.isNotEmpty(this.checks.filtered(c -> c.getState() == Check.CheckState.SCHEDULED || c.getState() == Check.CheckState.RUNNING));
this.anyCheckSelected = selectedCheck.isNotNull();
}
@FXML
public void initialize() {
checksListView.setItems(tasks);
checksListView.setCellFactory(view -> new CheckListCell());
selectedTask.bind(checksListView.getSelectionModel().selectedItemProperty());
checksListView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
checksListView.setItems(checks);
checksListView.setCellFactory(listCellFactory);
selectedCheck.bind(checksListView.getSelectionModel().selectedItemProperty());
}
@FXML
public void toggleSelectAll(ActionEvent event) {
if (event.getSource() instanceof CheckBox c) {
tasks.forEach(t -> t.chosenForExecutionProperty().set(c.isSelected()));
}
public void selectAllChecks() {
checks.forEach(t -> t.chosenForExecutionProperty().set(true));
}
@FXML
public void deselectAllChecks() {
checks.forEach(t -> t.chosenForExecutionProperty().set(false));
}
@FXML
public void runSelectedChecks() {
Preconditions.checkState(runningTask.get() == null);
Preconditions.checkState(!mainRunStarted.get());
Preconditions.checkState(!somethingsRunning.get());
Preconditions.checkState(!chosenChecks.isEmpty());
// prevent further interaction by cancelling non-chosen tasks:
tasks.filtered(Predicates.not(chosenTasks::contains)).forEach(HealthCheckTask::cancel);
// run chosen tasks:
var batchService = new BatchService(chosenTasks);
batchService.setExecutor(executorService);
batchService.start();
runningTask.set(batchService);
showResultScreen.set(true);
checksListView.getSelectionModel().select(chosenTasks.get(0));
checks.filtered(c -> !c.isChosenForExecution()).forEach(c -> c.setState(Check.CheckState.SKIPPED));
checkExecutor.executeBatch(chosenChecks);
checksListView.getSelectionModel().select(chosenChecks.get(0));
checksListView.refresh();
window.sizeToScene();
}
@FXML
public synchronized void cancelCheck() {
Preconditions.checkState(runningTask.get() != null);
runningTask.get().cancel();
public synchronized void cancelRun() {
Preconditions.checkState(somethingsRunning.get());
checkExecutor.cancel();
}
@FXML
public void exportResults() {
try {
reportWriter.writeReport(tasks);
reportWriter.writeReport(chosenChecks);
} catch (IOException e) {
LOG.error("Failed to write health check report.", e);
errorComponentBuilder.get().cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
@@ -122,19 +110,11 @@ public class CheckListController implements FxController {
/* Getter/Setter */
public boolean isRunning() {
return running.getValue();
return somethingsRunning.getValue();
}
public Binding<Boolean> runningProperty() {
return running;
}
public boolean isFinished() {
return finished.getValue();
}
public Binding<Boolean> finishedProperty() {
return finished;
public BooleanBinding runningProperty() {
return somethingsRunning;
}
public boolean isAnyCheckSelected() {
@@ -145,12 +125,12 @@ public class CheckListController implements FxController {
return anyCheckSelected;
}
public boolean getShowResultScreen() {
return showResultScreen.get();
public boolean isMainRunStarted() {
return mainRunStarted.get();
}
public BooleanProperty showResultScreenProperty() {
return showResultScreen;
public BooleanBinding mainRunStartedProperty() {
return mainRunStarted;
}
public int getChosenTaskCount() {

View File

@@ -0,0 +1,75 @@
package org.cryptomator.ui.health;
import com.tobiasdiez.easybind.EasyBind;
import com.tobiasdiez.easybind.Subscription;
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
import org.cryptomator.ui.controls.FontAwesome5Icon;
import org.cryptomator.ui.controls.FontAwesome5IconView;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableObjectValue;
import java.util.List;
/**
* A {@link FontAwesome5IconView} that automatically sets the glyph depending on
* the {@link Check#stateProperty() state} and {@link Check#highestResultSeverityProperty() severity} of a HealthCheck.
*/
public class CheckStateIconView extends FontAwesome5IconView {
private final ObjectProperty<Check> check = new SimpleObjectProperty<>();
private final ObservableObjectValue<Check.CheckState> state;
private final ObservableObjectValue<DiagnosticResult.Severity> severity;
private final List<Subscription> subscriptions;
public CheckStateIconView() {
this.state = EasyBind.wrapNullable(check).mapObservable(Check::stateProperty).asOrdinary();
this.severity = EasyBind.wrapNullable(check).mapObservable(Check::highestResultSeverityProperty).asOrdinary();
this.glyph.bind(Bindings.createObjectBinding(this::glyphForState, state, severity));
this.subscriptions = List.of( //
EasyBind.includeWhen(getStyleClass(), "glyph-icon-muted", Bindings.equal(state, Check.CheckState.SKIPPED).or(Bindings.equal(state, Check.CheckState.CANCELLED))), //
EasyBind.includeWhen(getStyleClass(), "glyph-icon-primary", Bindings.equal(severity, DiagnosticResult.Severity.GOOD)), //
EasyBind.includeWhen(getStyleClass(), "glyph-icon-orange", Bindings.equal(severity, DiagnosticResult.Severity.WARN).or(Bindings.equal(severity, DiagnosticResult.Severity.CRITICAL))), //
EasyBind.includeWhen(getStyleClass(), "glyph-icon-red", Bindings.equal(state, Check.CheckState.ERROR)) //
);
}
private FontAwesome5Icon glyphForState() {
if (state.getValue() == null) {
return null;
}
return switch (state.getValue()) {
case RUNNABLE -> null;
case SKIPPED -> FontAwesome5Icon.FAST_FORWARD;
case SCHEDULED -> FontAwesome5Icon.CLOCK;
case RUNNING -> FontAwesome5Icon.SPINNER;
case ERROR -> FontAwesome5Icon.TIMES;
case CANCELLED -> FontAwesome5Icon.BAN;
case SUCCEEDED -> glyphIconForSeverity();
};
}
private FontAwesome5Icon glyphIconForSeverity() {
if (severity.getValue() == null) {
return null;
}
return switch (severity.getValue()) {
case GOOD, INFO -> FontAwesome5Icon.CHECK;
case WARN, CRITICAL -> FontAwesome5Icon.EXCLAMATION_TRIANGLE;
};
}
public ObjectProperty<Check> checkProperty() {
return check;
}
public void setCheck(Check c) {
check.set(c);
}
public Check getCheck() {
return check.get();
}
}

View File

@@ -1,41 +0,0 @@
package org.cryptomator.ui.health;
import org.cryptomator.cryptofs.VaultConfig;
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
import org.cryptomator.cryptofs.health.api.HealthCheck;
import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.cryptolib.api.Masterkey;
import java.nio.file.Path;
import java.util.function.Consumer;
/**
* FIXME: Remove in production release
*/
public class DummyHealthChecks {
public static class DummyCheck1 implements HealthCheck {
@Override
public void check(Path path, VaultConfig vaultConfig, Masterkey masterkey, Cryptor cryptor, Consumer<DiagnosticResult> consumer) {
// no-op
}
}
public static class DummyCheck2 implements HealthCheck {
@Override
public void check(Path path, VaultConfig vaultConfig, Masterkey masterkey, Cryptor cryptor, Consumer<DiagnosticResult> consumer) {
// no-op
}
}
public static class DummyCheck3 implements HealthCheck {
@Override
public void check(Path path, VaultConfig vaultConfig, Masterkey masterkey, Cryptor cryptor, Consumer<DiagnosticResult> consumer) {
// no-op
}
}
}

View File

@@ -4,6 +4,7 @@ import dagger.BindsInstance;
import dagger.Lazy;
import dagger.Subcomponent;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.VaultConfig;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
@@ -15,20 +16,27 @@ import javafx.stage.Stage;
@Subcomponent(modules = {HealthCheckModule.class})
public interface HealthCheckComponent {
LoadUnverifiedConfigResult loadConfig();
@HealthCheckWindow
Stage window();
@Named("windowToClose")
Stage windowToClose();
@FxmlScene(FxmlFile.HEALTH_START)
Lazy<Scene> scene();
Lazy<Scene> startScene();
@FxmlScene(FxmlFile.HEALTH_START_FAIL)
Lazy<Scene> failScene();
default Stage showHealthCheckWindow() {
Stage stage = window();
stage.setScene(scene().get());
// TODO reevaluate config loading, as soon as we have the new generic error screen
var unverifiedConf = loadConfig();
if (unverifiedConf.config() != null) {
stage.setScene(startScene().get());
} else {
stage.setScene(failScene().get());
}
stage.show();
windowToClose().close();
return stage;
}
@@ -39,9 +47,10 @@ public interface HealthCheckComponent {
Builder vault(@HealthCheckWindow Vault vault);
@BindsInstance
Builder windowToClose(@Named("windowToClose") Stage window);
Builder owner(@Named("healthCheckOwner") Stage owner);
HealthCheckComponent build();
}
record LoadUnverifiedConfigResult(VaultConfig.UnverifiedVaultConfig config, Throwable error) {}
}

View File

@@ -17,8 +17,8 @@ import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.keyloading.KeyLoadingComponent;
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
import org.cryptomator.ui.mainwindow.MainWindow;
import javax.inject.Named;
import javax.inject.Provider;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
@@ -26,8 +26,8 @@ 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.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -37,6 +37,18 @@ import java.util.concurrent.atomic.AtomicReference;
@Module(subcomponents = {KeyLoadingComponent.class})
abstract class HealthCheckModule {
// TODO reevaluate config loading, as soon as we have the new generic error screen
@Provides
@HealthCheckScoped
static HealthCheckComponent.LoadUnverifiedConfigResult provideLoadConfigResult(@HealthCheckWindow Vault vault) {
try {
return new HealthCheckComponent.LoadUnverifiedConfigResult(vault.getUnverifiedVaultConfig(), null);
} catch (IOException e) {
return new HealthCheckComponent.LoadUnverifiedConfigResult(null, e);
}
}
@Provides
@HealthCheckScoped
static AtomicReference<Masterkey> provideMasterkeyRef() {
@@ -51,27 +63,20 @@ abstract class HealthCheckModule {
@Provides
@HealthCheckScoped
static Collection<HealthCheck> provideAvailableHealthChecks() {
return HealthCheck.allChecks();
}
@Provides
@HealthCheckScoped
static ObjectProperty<HealthCheckTask> provideSelectedHealthCheckTask() {
static ObjectProperty<Check> provideSelectedCheck() {
return new SimpleObjectProperty<>();
}
/* Only inject with Lazy-Wrapper!*/
@Provides
@HealthCheckScoped
static List<HealthCheckTask> provideAvailableHealthCheckTasks(Collection<HealthCheck> availableHealthChecks, @HealthCheckWindow Vault vault, AtomicReference<Masterkey> masterkeyRef, AtomicReference<VaultConfig> vaultConfigRef, SecureRandom csprng, ResourceBundle resourceBundle) {
return availableHealthChecks.stream().map(check -> new HealthCheckTask(vault.getPath(), vaultConfigRef.get(), masterkeyRef.get(), csprng, check, resourceBundle)).toList();
static List<Check> provideAvailableChecks() {
return HealthCheck.allChecks().stream().map(Check::new).toList();
}
@Provides
@HealthCheckWindow
@HealthCheckScoped
static KeyLoadingStrategy provideKeyLoadingStrategy(KeyLoadingComponent.Builder compBuilder, @HealthCheckWindow Vault vault, @HealthCheckWindow Stage window) {
static KeyLoadingStrategy provideKeyLoadingStrategy(KeyLoadingComponent.Builder compBuilder, @HealthCheckWindow Vault vault, @Named("unlockWindow") Stage window ) {
return compBuilder.vault(vault).window(window).build().keyloadingStrategy();
}
@@ -82,14 +87,26 @@ abstract class HealthCheckModule {
return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle);
}
@Provides
@Named("unlockWindow")
@HealthCheckScoped
static Stage provideUnlockWindow (@HealthCheckWindow Stage window, @HealthCheckWindow Vault vault, StageFactory factory, ResourceBundle resourceBundle) {
Stage stage = factory.create();
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(window);
stage.setTitle(String.format(resourceBundle.getString("unlock.title"), vault.getDisplayName()));
stage.setResizable(false);
return stage;
}
@Provides
@HealthCheckWindow
@HealthCheckScoped
static Stage provideStage(StageFactory factory, @MainWindow Stage owner, ResourceBundle resourceBundle, ChangeListener<Boolean> showingListener) {
static Stage provideStage(StageFactory factory, @Named("healthCheckOwner") Stage owner, @HealthCheckWindow Vault vault, ChangeListener<Boolean> showingListener, ResourceBundle resourceBundle) {
Stage stage = factory.create();
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner);
stage.setTitle(resourceBundle.getString("health.title"));
stage.setTitle(String.format(resourceBundle.getString("health.title"), vault.getDisplayName()));
stage.setResizable(true);
stage.showingProperty().addListener(showingListener); // bind masterkey lifecycle to window
return stage;
@@ -112,6 +129,13 @@ abstract class HealthCheckModule {
return fxmlLoaders.createScene(FxmlFile.HEALTH_START);
}
@Provides
@FxmlScene(FxmlFile.HEALTH_START_FAIL)
@HealthCheckScoped
static Scene provideHealthStartFailScene(@HealthCheckWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.HEALTH_START_FAIL);
}
@Provides
@FxmlScene(FxmlFile.HEALTH_CHECK_LIST)
@HealthCheckScoped
@@ -124,6 +148,11 @@ abstract class HealthCheckModule {
@FxControllerKey(StartController.class)
abstract FxController bindStartController(StartController controller);
@Binds
@IntoMap
@FxControllerKey(StartFailController.class)
abstract FxController bindStartFailController(StartFailController controller);
@Binds
@IntoMap
@FxControllerKey(CheckListController.class)
@@ -139,4 +168,8 @@ abstract class HealthCheckModule {
@FxControllerKey(ResultListCellController.class)
abstract FxController bindResultListCellController(ResultListCellController controller);
@Binds
@IntoMap
@FxControllerKey(CheckListCellController.class)
abstract FxController bindCheckListCellController(CheckListCellController controller);
}

View File

@@ -1,114 +0,0 @@
package org.cryptomator.ui.health;
import org.cryptomator.cryptofs.VaultConfig;
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
import org.cryptomator.cryptofs.health.api.HealthCheck;
import org.cryptomator.cryptolib.api.CryptorProvider;
import org.cryptomator.cryptolib.api.Masterkey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.LongProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import java.nio.file.Path;
import java.security.SecureRandom;
import java.time.Duration;
import java.time.Instant;
import java.util.MissingResourceException;
import java.util.Objects;
import java.util.ResourceBundle;
import java.util.concurrent.CancellationException;
class HealthCheckTask extends Task<Void> {
private static final Logger LOG = LoggerFactory.getLogger(HealthCheckTask.class);
private final Path vaultPath;
private final VaultConfig vaultConfig;
private final Masterkey masterkey;
private final SecureRandom csprng;
private final HealthCheck check;
private final ObservableList<Result> results;
private final LongProperty durationInMillis;
private final BooleanProperty chosenForExecution;
public HealthCheckTask(Path vaultPath, VaultConfig vaultConfig, Masterkey masterkey, SecureRandom csprng, HealthCheck check, ResourceBundle resourceBundle) {
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.results = FXCollections.observableArrayList(Result::observables);
try {
updateTitle(resourceBundle.getString("health." + check.name()));
} catch (MissingResourceException e) {
LOG.warn("Missing proper name for health check {}, falling back to default.", check.name());
updateTitle(check.name());
}
this.durationInMillis = new SimpleLongProperty(-1);
this.chosenForExecution = new SimpleBooleanProperty();
}
@Override
protected Void call() {
Instant start = Instant.now();
try (var masterkeyClone = masterkey.clone(); //
var cryptor = CryptorProvider.forScheme(vaultConfig.getCipherCombo()).provide(masterkeyClone, csprng)) {
check.check(vaultPath, vaultConfig, masterkeyClone, cryptor, diagnosis -> {
if (isCancelled()) {
throw new CancellationException();
}
Platform.runLater(() -> results.add(Result.create(diagnosis)));
});
}
Platform.runLater(() -> durationInMillis.set(Duration.between(start, Instant.now()).toMillis()));
return null;
}
@Override
protected void scheduled() {
LOG.info("starting {}", check.name());
}
@Override
protected void done() {
LOG.info("finished {}", check.name());
}
/* Getter */
Observable[] observables() {
return new Observable[]{results, chosenForExecution};
}
public ObservableList<Result> results() {
return results;
}
public HealthCheck getCheck() {
return check;
}
public LongProperty durationInMillisProperty() {
return durationInMillis;
}
public long getDurationInMillis() {
return durationInMillis.get();
}
public BooleanProperty chosenForExecutionProperty() {
return chosenForExecution;
}
public boolean isChosenForExecution() {
return chosenForExecution.get();
}
}

View File

@@ -1,15 +1,12 @@
package org.cryptomator.ui.health;
import org.apache.commons.lang3.exception.ExceptionUtils;
import com.google.common.base.Throwables;
import org.cryptomator.common.Environment;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.VaultConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.application.Application;
import javafx.concurrent.Worker;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
@@ -28,11 +25,10 @@ import java.util.stream.Collectors;
@HealthCheckScoped
public class ReportWriter {
private static final Logger LOG = LoggerFactory.getLogger(ReportWriter.class);
private static final String REPORT_HEADER = """
**************************************
* Cryptomator Vault Health Report *
**************************************
*******************************************
* Cryptomator Vault Health Report *
*******************************************
Analyzed vault: %s (Current name "%s")
Vault storage path: %s
""";
@@ -58,38 +54,35 @@ public class ReportWriter {
this.exportDestination = env.getLogDir().orElse(Path.of(System.getProperty("user.home"))).resolve("healthReport_" + vault.getDisplayName() + "_" + TIME_STAMP.format(Instant.now()) + ".log");
}
protected void writeReport(Collection<HealthCheckTask> tasks) throws IOException {
protected void writeReport(Collection<Check> performedChecks) throws IOException {
try (var out = Files.newOutputStream(exportDestination, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); //
var writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8))) {
writer.write(REPORT_HEADER.formatted(vaultConfig.getId(), vault.getDisplayName(), vault.getPath()));
for (var task : tasks) {
if (task.getState() == Worker.State.READY) {
LOG.debug("Skipping not performed check {}.", task.getCheck().name());
continue;
}
writer.write(REPORT_CHECK_HEADER.formatted(task.getCheck().name()));
switch (task.getState()) {
for (var check : performedChecks) {
writer.write(REPORT_CHECK_HEADER.formatted(check.getHealthCheck().name()));
switch (check.getState()) {
case SUCCEEDED -> {
writer.write("STATUS: SUCCESS\nRESULTS:\n");
for (var result : task.results()) {
for (var result : check.getResults()) {
writer.write(REPORT_CHECK_RESULT.formatted(result.diagnosis().getSeverity(), result.getDescription()));
}
}
case CANCELLED -> writer.write("STATUS: CANCELED\n");
case FAILED -> {
writer.write("STATUS: FAILED\nREASON:\n" + task.getCheck().name());
writer.write(prepareFailureMsg(task));
case ERROR -> {
writer.write("STATUS: FAILED\nREASON:\n");
writer.write(prepareFailureMsg(check));
}
case RUNNING, SCHEDULED -> throw new IllegalStateException("Checks are still running.");
case RUNNABLE, RUNNING, SCHEDULED -> throw new IllegalStateException("Checks are still running.");
case SKIPPED -> {} //noop
}
}
}
reveal();
}
private String prepareFailureMsg(HealthCheckTask task) {
if (task.getException() != null) {
return ExceptionUtils.getStackTrace(task.getException()) //
private String prepareFailureMsg(Check check) {
if (check.getError() != null) {
return Throwables.getStackTraceAsString(check.getError()) //
.lines() //
.map(line -> "\t\t" + line + "\n") //
.collect(Collectors.joining());

View File

@@ -17,10 +17,7 @@ record Result(DiagnosticResult diagnosis, ObjectProperty<FixState> fixState) {
}
public static Result create(DiagnosticResult diagnosis) {
FixState initialState = switch (diagnosis.getSeverity()) {
case WARN -> FixState.FIXABLE;
default -> FixState.NOT_FIXABLE;
};
FixState initialState = diagnosis.getSeverity() == DiagnosticResult.Severity.WARN ? FixState.FIXABLE : FixState.NOT_FIXABLE;
return new Result(diagnosis, new SimpleObjectProperty<>(initialState));
}

View File

@@ -1,7 +1,8 @@
package org.cryptomator.ui.health;
import com.tobiasdiez.easybind.EasyBind;
import com.tobiasdiez.easybind.optional.OptionalBinding;
import com.tobiasdiez.easybind.Subscription;
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.controls.FontAwesome5Icon;
import org.cryptomator.ui.controls.FontAwesome5IconView;
@@ -16,45 +17,74 @@ import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableObjectValue;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Tooltip;
import javafx.util.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.ResourceBundle;
// unscoped because each cell needs its own controller
public class ResultListCellController implements FxController {
private static final FontAwesome5Icon INFO_ICON = FontAwesome5Icon.INFO_CIRCLE;
private static final FontAwesome5Icon GOOD_ICON = FontAwesome5Icon.CHECK;
private static final FontAwesome5Icon WARN_ICON = FontAwesome5Icon.EXCLAMATION_TRIANGLE;
private static final FontAwesome5Icon CRIT_ICON = FontAwesome5Icon.TIMES;
private final Logger LOG = LoggerFactory.getLogger(ResultListCellController.class);
private final ObjectProperty<Result> result;
private final ObservableObjectValue<DiagnosticResult.Severity> severity;
private final Binding<String> description;
private final ResultFixApplier fixApplier;
private final OptionalBinding<Result.FixState> fixState;
private final ObjectBinding<FontAwesome5Icon> glyph;
private final ObservableObjectValue<Result.FixState> fixState;
private final ObjectBinding<FontAwesome5Icon> severityGlyph;
private final ObjectBinding<FontAwesome5Icon> fixGlyph;
private final BooleanBinding fixable;
private final BooleanBinding fixing;
private final BooleanBinding fixed;
private final BooleanBinding fixFailed;
private final BooleanBinding fixRunningOrDone;
private final List<Subscription> subscriptions;
private final Tooltip fixSuccess;
private final Tooltip fixFail;
public FontAwesome5IconView iconView;
public Button fixButton;
/* FXML */
public FontAwesome5IconView severityView;
public FontAwesome5IconView fixView;
@Inject
public ResultListCellController(ResultFixApplier fixApplier) {
public ResultListCellController(ResultFixApplier fixApplier, ResourceBundle resourceBundle) {
this.result = new SimpleObjectProperty<>(null);
this.severity = EasyBind.wrapNullable(result).map(r -> r.diagnosis().getSeverity()).asOrdinary();
this.description = EasyBind.wrapNullable(result).map(Result::getDescription).orElse("");
this.fixApplier = fixApplier;
this.fixState = EasyBind.wrapNullable(result).mapObservable(Result::fixState);
this.glyph = Bindings.createObjectBinding(this::getGlyph, result);
this.fixState = EasyBind.wrapNullable(result).mapObservable(Result::fixState).asOrdinary();
this.severityGlyph = Bindings.createObjectBinding(this::getSeverityGlyph, result);
this.fixGlyph = Bindings.createObjectBinding(this::getFixGlyph, fixState);
this.fixable = Bindings.createBooleanBinding(this::isFixable, fixState);
this.fixing = Bindings.createBooleanBinding(this::isFixing, fixState);
this.fixed = Bindings.createBooleanBinding(this::isFixed, fixState);
this.fixFailed = Bindings.createBooleanBinding(this::isFixFailed, fixState);
this.fixRunningOrDone = fixing.or(fixed).or(fixFailed);
this.subscriptions = new ArrayList<>();
this.fixSuccess = new Tooltip(resourceBundle.getString("health.fix.successTip"));
this.fixFail = new Tooltip(resourceBundle.getString("health.fix.failTip"));
fixSuccess.setShowDelay(Duration.millis(100));
fixFail.setShowDelay(Duration.millis(100));
}
@FXML
public void initialize() {
// see getGlyph() for relevant glyphs:
EasyBind.includeWhen(iconView.getStyleClass(), "glyph-icon-muted", iconView.glyphProperty().isEqualTo(FontAwesome5Icon.INFO_CIRCLE));
EasyBind.includeWhen(iconView.getStyleClass(), "glyph-icon-primary", iconView.glyphProperty().isEqualTo(FontAwesome5Icon.CHECK));
EasyBind.includeWhen(iconView.getStyleClass(), "glyph-icon-orange", iconView.glyphProperty().isEqualTo(FontAwesome5Icon.EXCLAMATION_TRIANGLE));
EasyBind.includeWhen(iconView.getStyleClass(), "glyph-icon-red", iconView.glyphProperty().isEqualTo(FontAwesome5Icon.TIMES));
subscriptions.addAll(List.of(EasyBind.includeWhen(severityView.getStyleClass(), "glyph-icon-muted", Bindings.equal(severity, DiagnosticResult.Severity.INFO)), //
EasyBind.includeWhen(severityView.getStyleClass(), "glyph-icon-primary", Bindings.equal(severity, DiagnosticResult.Severity.GOOD)), //
EasyBind.includeWhen(severityView.getStyleClass(), "glyph-icon-orange", Bindings.equal(severity, DiagnosticResult.Severity.WARN)), //
EasyBind.includeWhen(severityView.getStyleClass(), "glyph-icon-red", Bindings.equal(severity, DiagnosticResult.Severity.CRITICAL)) //
// EasyBind.includeWhen(fixView.getStyleClass(), "glyph-icon-muted", fixView.glyphProperty().isNotNull())) // TODO not really needed, right?
));
}
@FXML
@@ -68,7 +98,9 @@ public class ResultListCellController implements FxController {
private void fixFinished(Void unused, Throwable exception) {
if (exception != null) {
LOG.error("Failed to apply fix", exception);
// TODO ...
Tooltip.install(fixView, fixFail);
} else {
Tooltip.install(fixView, fixSuccess);
}
}
@@ -87,29 +119,45 @@ public class ResultListCellController implements FxController {
return result;
}
public Binding<String> descriptionProperty() {
return description;
}
public String getDescription() {
return description.getValue();
}
public ObjectBinding<FontAwesome5Icon> glyphProperty() {
return glyph;
public ObjectBinding<FontAwesome5Icon> severityGlyphProperty() {
return severityGlyph;
}
public FontAwesome5Icon getGlyph() {
public FontAwesome5Icon getSeverityGlyph() {
var r = result.get();
if (r == null) {
return null;
}
return switch (r.diagnosis().getSeverity()) {
case INFO -> FontAwesome5Icon.INFO_CIRCLE;
case GOOD -> FontAwesome5Icon.CHECK;
case WARN -> FontAwesome5Icon.EXCLAMATION_TRIANGLE;
case CRITICAL -> FontAwesome5Icon.TIMES;
case INFO -> INFO_ICON;
case GOOD -> GOOD_ICON;
case WARN -> WARN_ICON;
case CRITICAL -> CRIT_ICON;
};
}
public Binding<String> descriptionProperty() {
return description;
public ObjectBinding<FontAwesome5Icon> fixGlyphProperty() {
return fixGlyph;
}
public FontAwesome5Icon getFixGlyph() {
if (fixState.getValue() == null) {
return null;
}
return switch (fixState.getValue()) {
case NOT_FIXABLE, FIXABLE -> null;
case FIXING -> FontAwesome5Icon.SPINNER;
case FIXED -> FontAwesome5Icon.CHECK;
case FIX_FAILED -> FontAwesome5Icon.TIMES;
};
}
public BooleanBinding fixableProperty() {
@@ -117,7 +165,7 @@ public class ResultListCellController implements FxController {
}
public boolean isFixable() {
return fixState.get().map(Result.FixState.FIXABLE::equals).orElse(false);
return Result.FixState.FIXABLE.equals(fixState.get());
}
public BooleanBinding fixingProperty() {
@@ -125,7 +173,7 @@ public class ResultListCellController implements FxController {
}
public boolean isFixing() {
return fixState.get().map(Result.FixState.FIXING::equals).orElse(false);
return Result.FixState.FIXING.equals(fixState.get());
}
public BooleanBinding fixedProperty() {
@@ -133,7 +181,24 @@ public class ResultListCellController implements FxController {
}
public boolean isFixed() {
return fixState.get().map(Result.FixState.FIXED::equals).orElse(false);
return Result.FixState.FIXED.equals(fixState.get());
}
public BooleanBinding fixFailedProperty() {
return fixFailed;
}
public Boolean isFixFailed() {
return Result.FixState.FIX_FAILED.equals(fixState.get());
}
public BooleanBinding fixRunningOrDoneProperty() {
return fixRunningOrDone;
}
public boolean isFixRunningOrDone() {
return fixRunningOrDone.get();
}
}

View File

@@ -1,12 +1,11 @@
package org.cryptomator.ui.health;
import com.google.common.base.Preconditions;
import dagger.Lazy;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.VaultConfig;
import org.cryptomator.cryptofs.VaultConfigLoadException;
import org.cryptomator.cryptofs.VaultKeyInvalidException;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
@@ -17,15 +16,13 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javafx.application.Platform;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutorService;
@@ -36,40 +33,28 @@ public class StartController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(StartController.class);
private final Vault vault;
private final Stage window;
private final CompletableFuture<VaultConfig.UnverifiedVaultConfig> unverifiedVaultConfig;
private final Stage unlockWindow;
private final ObjectProperty<VaultConfig.UnverifiedVaultConfig> unverifiedVaultConfig;
private final KeyLoadingStrategy keyLoadingStrategy;
private final ExecutorService executor;
private final AtomicReference<Masterkey> masterkeyRef;
private final AtomicReference<VaultConfig> vaultConfigRef;
private final Lazy<Scene> checkScene;
private final Lazy<ErrorComponent.Builder> errorComponent;
private final ObjectProperty<State> state = new SimpleObjectProperty<>(State.LOADING);
private final BooleanBinding loading = state.isEqualTo(State.LOADING);
private final BooleanBinding failed = state.isEqualTo(State.FAILED);
private final BooleanBinding loaded = state.isEqualTo(State.LOADED);
public enum State {
LOADING,
FAILED,
LOADED
}
/* FXML */
@Inject
public StartController(@HealthCheckWindow Vault vault, @HealthCheckWindow Stage window, @HealthCheckWindow KeyLoadingStrategy keyLoadingStrategy, ExecutorService executor, AtomicReference<Masterkey> masterkeyRef, AtomicReference<VaultConfig> vaultConfigRef, @FxmlScene(FxmlFile.HEALTH_CHECK_LIST) Lazy<Scene> checkScene, Lazy<ErrorComponent.Builder> errorComponent) {
this.vault = vault;
public StartController(@HealthCheckWindow Stage window, HealthCheckComponent.LoadUnverifiedConfigResult configLoadResult, @HealthCheckWindow KeyLoadingStrategy keyLoadingStrategy, ExecutorService executor, AtomicReference<Masterkey> masterkeyRef, AtomicReference<VaultConfig> vaultConfigRef, @FxmlScene(FxmlFile.HEALTH_CHECK_LIST) Lazy<Scene> checkScene, Lazy<ErrorComponent.Builder> errorComponent, @Named("unlockWindow") Stage unlockWindow) {
Preconditions.checkNotNull(configLoadResult.config());
this.window = window;
this.unverifiedVaultConfig = CompletableFuture.supplyAsync(this::loadConfig, executor);
this.unlockWindow = unlockWindow;
this.unverifiedVaultConfig = new SimpleObjectProperty<>(configLoadResult.config());
this.keyLoadingStrategy = keyLoadingStrategy;
this.executor = executor;
this.masterkeyRef = masterkeyRef;
this.vaultConfigRef = vaultConfigRef;
this.checkScene = checkScene;
this.errorComponent = errorComponent;
this.unverifiedVaultConfig.whenCompleteAsync(this::loadedConfig, Platform::runLater);
}
@FXML
@@ -84,29 +69,18 @@ public class StartController implements FxController {
CompletableFuture.runAsync(this::loadKey, executor).whenCompleteAsync(this::loadedKey, Platform::runLater);
}
private VaultConfig.UnverifiedVaultConfig loadConfig() {
assert !Platform.isFxApplicationThread();
try {
return this.vault.getUnverifiedVaultConfig();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private void loadedConfig(VaultConfig.UnverifiedVaultConfig cfg, Throwable exception) {
assert Platform.isFxApplicationThread();
if (exception != null) {
state.set(State.FAILED);
} else {
assert cfg != null;
state.set(State.LOADED);
}
}
private void loadKey() {
assert !Platform.isFxApplicationThread();
assert unverifiedVaultConfig.isDone();
var unverifiedCfg = unverifiedVaultConfig.join();
assert unverifiedVaultConfig.get() != null;
try {
keyLoadingStrategy.use(this::verifyVaultConfig);
} catch (VaultConfigLoadException | UnlockCancelledException e) {
throw new LoadingFailedException(e);
}
}
private void verifyVaultConfig(KeyLoadingStrategy keyLoadingStrategy) throws VaultConfigLoadException {
var unverifiedCfg = unverifiedVaultConfig.get();
try (var masterkey = keyLoadingStrategy.loadKey(unverifiedCfg.getKeyId())) {
var verifiedCfg = unverifiedCfg.verify(masterkey.getEncoded(), unverifiedCfg.allegedVaultVersion());
vaultConfigRef.set(verifiedCfg);
@@ -114,15 +88,6 @@ public class StartController implements FxController {
if (old != null) {
old.destroy();
}
} catch (MasterkeyLoadingFailedException e) {
if (keyLoadingStrategy.recoverFromException(e)) {
// retry
loadKey();
} else {
throw new LoadingFailedException(e);
}
} catch (VaultConfigLoadException e) {
throw new LoadingFailedException(e);
}
}
@@ -134,6 +99,7 @@ public class StartController implements FxController {
loadingKeyFailed(exception);
} else {
LOG.debug("Loaded valid key");
unlockWindow.close();
window.setScene(checkScene.get());
}
}
@@ -150,35 +116,10 @@ public class StartController implements FxController {
}
}
/* Getter */
public BooleanBinding loadingProperty() {
return loading;
}
public boolean isLoading() {
return loading.get();
}
public BooleanBinding failedProperty() {
return failed;
}
public boolean isFailed() {
return failed.get();
}
public BooleanBinding loadedProperty() {
return loaded;
}
public boolean isLoaded() {
return loaded.get();
}
/* internal types */
private static class LoadingFailedException extends CompletionException {
LoadingFailedException(Throwable cause) {
super(cause);
}

View File

@@ -0,0 +1,79 @@
package org.cryptomator.ui.health;
import com.google.common.base.Preconditions;
import org.cryptomator.cryptofs.VaultConfigLoadException;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.controls.FontAwesome5Icon;
import javax.inject.Inject;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.scene.control.TitledPane;
import javafx.stage.Stage;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
// TODO reevaluate config loading, as soon as we have the new generic error screen
@HealthCheckScoped
public class StartFailController implements FxController {
private final Stage window;
private final ObjectProperty<Throwable> loadError;
private final ObjectProperty<FontAwesome5Icon> moreInfoIcon;
/* FXML */
public TitledPane moreInfoPane;
@Inject
public StartFailController(@HealthCheckWindow Stage window, HealthCheckComponent.LoadUnverifiedConfigResult configLoadResult) {
Preconditions.checkNotNull(configLoadResult.error());
this.window = window;
this.loadError = new SimpleObjectProperty<>(configLoadResult.error());
this.moreInfoIcon = new SimpleObjectProperty<>(FontAwesome5Icon.CARET_RIGHT);
}
public void initialize() {
moreInfoPane.expandedProperty().addListener(this::setMoreInfoIcon);
}
private void setMoreInfoIcon(ObservableValue<? extends Boolean> observable, boolean wasExpanded, boolean willExpand) {
moreInfoIcon.set(willExpand ? FontAwesome5Icon.CARET_DOWN : FontAwesome5Icon.CARET_RIGHT);
}
@FXML
public void close() {
window.close();
}
/* Getter & Setter */
public ObjectProperty<FontAwesome5Icon> moreInfoIconProperty() {
return moreInfoIcon;
}
public FontAwesome5Icon getMoreInfoIcon() {
return moreInfoIcon.getValue();
}
public String getStackTrace() {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
loadError.get().printStackTrace(new PrintStream(baos));
return baos.toString(StandardCharsets.UTF_8);
}
public String getLocalizedErrorMessage() {
return loadError.get().getLocalizedMessage();
}
public boolean isParseException() {
return loadError.get() instanceof VaultConfigLoadException;
}
public boolean isIoException() {
return !isParseException();
}
}

View File

@@ -3,6 +3,8 @@ package org.cryptomator.ui.keyloading;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.api.MasterkeyLoader;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URI;
@@ -12,6 +14,8 @@ import java.net.URI;
@FunctionalInterface
public interface KeyLoadingStrategy extends MasterkeyLoader {
Logger LOG = LoggerFactory.getLogger(KeyLoadingStrategy.class);
/**
* Loads a master key. This might be a long-running operation, as it may require user input or expensive computations.
* <p>
@@ -60,4 +64,36 @@ public interface KeyLoadingStrategy extends MasterkeyLoader {
};
}
/**
* Makes the given <code>user</code> apply this key loading strategy. If the user fails with a {@link MasterkeyLoadingFailedException},
* an attempt is made to {@link #recoverFromException(MasterkeyLoadingFailedException) recover} from it. Any other exception will be rethrown.
*
* @param user Some method using this strategy. May be invoked multiple times in case of recoverable {@link MasterkeyLoadingFailedException}s
* @param <E> Optional exception type thrown by <code>user</code>
* @throws MasterkeyLoadingFailedException If a non-recoverable exception is thrown by <code>user</code>
* @throws E Exception thrown by <code>user</code> and rethrown by this method
*/
default <E extends Exception> void use(KeyLoadingStrategyUser<E> user) throws MasterkeyLoadingFailedException, E {
boolean success = false;
try {
user.use(this);
} catch (MasterkeyLoadingFailedException e) {
if (recoverFromException(e)) {
LOG.info("Unlock attempt threw {}. Reattempting...", e.getClass().getSimpleName());
use(user);
} else {
throw e;
}
} finally {
cleanup(success);
}
}
@FunctionalInterface
interface KeyLoadingStrategyUser<E extends Exception> {
void use(KeyLoadingStrategy strategy) throws MasterkeyLoadingFailedException, E;
}
}

View File

@@ -1,5 +1,6 @@
package org.cryptomator.ui.unlock;
import com.google.common.base.Throwables;
import dagger.Lazy;
import org.cryptomator.common.mountpoint.InvalidMountPointException;
import org.cryptomator.common.vaults.MountPointRequirement;
@@ -7,12 +8,10 @@ import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.common.vaults.Volume.VolumeException;
import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.keyloading.KeyLoadingComponent;
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -68,19 +67,14 @@ public class UnlockWorkflow extends Task<Boolean> {
}
private void attemptUnlock() throws IOException, VolumeException, InvalidMountPointException, CryptoException {
boolean success = false;
try {
vault.unlock(keyLoadingStrategy);
success = true;
} catch (MasterkeyLoadingFailedException e) {
if (keyLoadingStrategy.recoverFromException(e)) {
LOG.info("Unlock attempt threw {}. Reattempting...", e.getClass().getSimpleName());
attemptUnlock();
} else {
throw e;
}
} finally {
keyLoadingStrategy.cleanup(success);
keyLoadingStrategy.use(vault::unlock);
} catch (Exception e) {
Throwables.propagateIfPossible(e, IOException.class);
Throwables.propagateIfPossible(e, VolumeException.class);
Throwables.propagateIfPossible(e, InvalidMountPointException.class);
Throwables.propagateIfPossible(e, CryptoException.class);
throw new IllegalStateException("unexpected exception type", e);
}
}

View File

@@ -4,10 +4,12 @@ import org.cryptomator.common.settings.WhenUnlocked;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.controls.NumericTextField;
import org.cryptomator.ui.health.HealthCheckComponent;
import javax.inject.Inject;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ChoiceBox;
@@ -24,6 +26,7 @@ public class GeneralVaultOptionsController implements FxController {
private final Stage window;
private final Vault vault;
private final HealthCheckComponent.Builder healthCheckWindow;
private final ResourceBundle resourceBundle;
public TextField vaultName;
@@ -33,9 +36,10 @@ public class GeneralVaultOptionsController implements FxController {
public NumericTextField lockTimeInMinutesTextField;
@Inject
GeneralVaultOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, ResourceBundle resourceBundle) {
GeneralVaultOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, HealthCheckComponent.Builder healthCheckWindow, ResourceBundle resourceBundle) {
this.window = window;
this.vault = vault;
this.healthCheckWindow = healthCheckWindow;
this.resourceBundle = resourceBundle;
}
@@ -104,4 +108,8 @@ public class GeneralVaultOptionsController implements FxController {
}
}
}
public void startHealthCheck() {
healthCheckWindow.vault(vault).owner(window).build().showHealthCheckWindow();
}
}

View File

@@ -1,30 +0,0 @@
package org.cryptomator.ui.vaultoptions;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.health.HealthCheckComponent;
import javax.inject.Inject;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.stage.Stage;
@VaultOptionsScoped
public class HealthVaultOptionsController implements FxController {
private final Stage window;
private final Vault vault;
private final HealthCheckComponent.Builder healthCheckWindow;
@Inject
public HealthVaultOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, HealthCheckComponent.Builder healthCheckWindow) {
this.window = window;
this.vault = vault;
this.healthCheckWindow = healthCheckWindow;
}
@FXML
public void startHealthCheck(ActionEvent event) {
healthCheckWindow.vault(vault).windowToClose(window).build().showHealthCheckWindow();
}
}

View File

@@ -25,9 +25,4 @@ public enum SelectedVaultOptionsTab {
* Show Auto-Lock tab
*/
AUTOLOCK,
/**
* Show health tab
*/
HEALTH;
}

View File

@@ -24,7 +24,6 @@ public class VaultOptionsController implements FxController {
public Tab mountTab;
public Tab keyTab;
public Tab autoLockTab;
public Tab healthTab;
@Inject
VaultOptionsController(@VaultOptionsWindow Stage window, ObjectProperty<SelectedVaultOptionsTab> selectedTabProperty) {
@@ -50,7 +49,6 @@ public class VaultOptionsController implements FxController {
case MOUNT -> mountTab;
case KEY -> keyTab;
case AUTOLOCK -> autoLockTab;
case HEALTH -> healthTab;
};
}

View File

@@ -83,9 +83,4 @@ abstract class VaultOptionsModule {
@IntoMap
@FxControllerKey(MasterkeyOptionsController.class)
abstract FxController bindMasterkeyOptionsController(MasterkeyOptionsController controller);
@Binds
@IntoMap
@FxControllerKey(HealthVaultOptionsController.class)
abstract FxController bindHealthOptionsController(HealthVaultOptionsController controller);
}

View File

@@ -1,3 +0,0 @@
org.cryptomator.ui.health.DummyHealthChecks$DummyCheck1
org.cryptomator.ui.health.DummyHealthChecks$DummyCheck2
org.cryptomator.ui.health.DummyHealthChecks$DummyCheck3

View File

@@ -127,23 +127,38 @@
-fx-fill: TEXT_FILL;
}
.glyph-icon-primary {
.glyph-icon-primary,
.glyph-icon.glyph-icon-primary,
.list-cell .glyph-icon.glyph-icon-primary,
.list-cell:selected .glyph-icon.glyph-icon-primary {
-fx-fill: PRIMARY;
}
.glyph-icon-muted {
.glyph-icon-muted,
.glyph-icon.glyph-icon-muted,
.list-cell .glyph-icon.glyph-icon-muted,
.list-cell:selected .glyph-icon.glyph-icon-muted {
-fx-fill: TEXT_FILL_MUTED;
}
.glyph-icon-white {
.glyph-icon-white,
.glyph-icon.glyph-icon-white,
.list-cell .glyph-icon.glyph-icon-white,
.list-cell:selected .glyph-icon.glyph-icon-white {
-fx-fill: white;
}
.glyph-icon-red {
.glyph-icon-red,
.glyph-icon.glyph-icon-red,
.list-cell .glyph-icon.glyph-icon-red,
.list-cell:selected .glyph-icon.glyph-icon-red {
-fx-fill: RED_5;
}
.glyph-icon-orange {
.glyph-icon-orange,
.glyph-icon.glyph-icon-orange,
.list-cell .glyph-icon.glyph-icon-orange,
.list-cell:selected .glyph-icon.glyph-icon-orange {
-fx-fill: ORANGE_5;
}

View File

@@ -127,23 +127,38 @@
-fx-fill: TEXT_FILL;
}
.glyph-icon-primary {
.glyph-icon-primary,
.glyph-icon.glyph-icon-primary,
.list-cell .glyph-icon.glyph-icon-primary,
.list-cell:selected .glyph-icon.glyph-icon-primary {
-fx-fill: PRIMARY;
}
.glyph-icon-muted {
.glyph-icon-muted,
.glyph-icon.glyph-icon-muted,
.list-cell .glyph-icon.glyph-icon-muted,
.list-cell:selected .glyph-icon.glyph-icon-muted {
-fx-fill: TEXT_FILL_MUTED;
}
.glyph-icon-white {
.glyph-icon-white,
.glyph-icon.glyph-icon-white,
.list-cell .glyph-icon.glyph-icon-white,
.list-cell:selected .glyph-icon.glyph-icon-white {
-fx-fill: white;
}
.glyph-icon-red {
.glyph-icon-red,
.glyph-icon.glyph-icon-red,
.list-cell .glyph-icon.glyph-icon-red,
.list-cell:selected .glyph-icon.glyph-icon-red {
-fx-fill: RED_5;
}
.glyph-icon-orange {
.glyph-icon-orange,
.glyph-icon.glyph-icon-orange,
.list-cell .glyph-icon.glyph-icon-orange,
.list-cell:selected .glyph-icon.glyph-icon-orange {
-fx-fill: ORANGE_5;
}

View File

@@ -1,24 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import org.cryptomator.ui.controls.FormattedLabel?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.layout.VBox?>
<?import org.cryptomator.ui.health.CheckStateIconView?>
<?import javafx.scene.layout.HBox?>
<VBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.health.CheckDetailController"
prefWidth="500"
spacing="6">
<FormattedLabel fx:id="checkTitle" styleClass="label-large" format="%health.check.detail.header" arg1="${controller.taskName}"/>
<Label fx:id="detailHeader" styleClass="label-large" text="${controller.checkName}" contentDisplay="LEFT">
<graphic>
<HBox alignment="CENTER" minWidth="25" maxWidth="25">
<CheckStateIconView fx:id="checkStateIconView" check="${controller.check}" glyphSize="20"/>
</HBox>
</graphic>
</Label>
<Label text="%health.check.detail.taskNotStarted" visible="${controller.taskNotStarted}" managed="${controller.taskNotStarted}"/>
<Label text="%health.check.detail.taskRunning" visible="${controller.taskRunning}" managed="${controller.taskRunning}"/>
<Label text="%health.check.detail.taskScheduled" visible="${controller.taskScheduled}" managed="${controller.taskScheduled}"/>
<Label text="%health.check.detail.taskCancelled" visible="${controller.taskCancelled}" managed="${controller.taskCancelled}"/>
<Label text="%health.check.detail.taskFailed" visible="${controller.taskFailed}" managed="${controller.taskFailed}"/>
<FormattedLabel styleClass="label" format="%health.check.detail.taskSucceeded" arg1="${controller.taskDuration}" visible="${controller.taskSucceeded}" managed="${controller.taskSucceeded}"/>
<Label text="%health.check.detail.checkRunning" visible="${controller.checkRunning}" managed="${controller.checkRunning}"/>
<Label text="%health.check.detail.checkScheduled" visible="${controller.checkScheduled}" managed="${controller.checkScheduled}"/>
<Label text="%health.check.detail.checkSkipped" visible="${controller.checkSkipped}" managed="${controller.checkSkipped}"/>
<Label text="%health.check.detail.checkCancelled" visible="${controller.checkCancelled}" managed="${controller.checkCancelled}"/>
<Label text="%health.check.detail.checkFailed" visible="${controller.checkFailed}" managed="${controller.checkFailed}"/>
<Label text="%health.check.detail.checkFinished" visible="${controller.checkSucceeded &amp;&amp; !controller.warnOrCritsExist}" managed="${controller.checkSucceeded &amp;&amp; !controller.warnOrCritsExist}"/>
<Label text="%health.check.detail.checkFinishedAndFound" visible="${controller.checkSucceeded &amp;&amp; controller.warnOrCritsExist}" managed="${controller.checkSucceeded &amp;&amp; controller.warnOrCritsExist}"/>
<FormattedLabel styleClass="label" format="%health.check.detail.problemCount" arg1="${controller.countOfWarnSeverity}" arg2="${controller.countOfCritSeverity}" visible="${!controller.taskNotStarted}"
managed="${!controller.taskNotStarted}" />
<ListView fx:id="resultsListView" VBox.vgrow="ALWAYS"/>
<ListView fx:id="resultsListView" VBox.vgrow="ALWAYS" visible="${!controller.checkSkipped}" fixedCellSize="25"/>
</VBox>

View File

@@ -24,23 +24,26 @@
</fx:define>
<children>
<HBox spacing="12" VBox.vgrow="ALWAYS">
<VBox minWidth="80" maxWidth="200" spacing="6" HBox.hgrow="ALWAYS" >
<Label fx:id="listHeading" text="%health.checkList.header"/>
<CheckBox onAction="#toggleSelectAll" text="%health.checkList.selectAllBox" visible="${!controller.showResultScreen}" managed="${!controller.showResultScreen}" />
<ListView fx:id="checksListView" VBox.vgrow="ALWAYS"/>
<ListView fx:id="checksListView" VBox.vgrow="ALWAYS" minWidth="175" maxWidth="175"/>
<VBox alignment="CENTER" visible="${!controller.mainRunStarted}" managed="${!controller.mainRunStarted}" HBox.hgrow="ALWAYS" spacing="12">
<Label text="%health.checkList.description" wrapText="true"/>
<HBox alignment="CENTER">
<Button onAction="#selectAllChecks" text="%health.checkList.selectAllButton" />
<Button onAction="#deselectAllChecks" text="%health.checkList.deselectAllButton" />
</HBox>
</VBox>
<StackPane visible="${controller.showResultScreen}" HBox.hgrow="ALWAYS" >
<StackPane visible="${controller.mainRunStarted}" managed="${controller.mainRunStarted}" HBox.hgrow="ALWAYS">
<VBox minWidth="300" alignment="CENTER" visible="${!controller.anyCheckSelected}" managed="${!controller.anyCheckSelected}" >
<Label text="%health.check.detail.noSelectedCheck" wrapText="true" alignment="CENTER" />
</VBox>
<fx:include source="health_check_details.fxml" visible="${controller.anyCheckSelected}" managed="${controller.anyCheckSelected}" />
<fx:include source="health_check_details.fxml" visible="${controller.anyCheckSelected}" managed="${controller.anyCheckSelected}" HBox.hgrow="ALWAYS"/>
</StackPane>
</HBox>
<ButtonBar buttonMinWidth="120" buttonOrder="+CX">
<buttons>
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" onAction="#cancelCheck" disable="${!controller.running}" visible="${controller.showResultScreen}" managed="${controller.showResultScreen}" />
<Button text="%health.check.exportBtn" ButtonBar.buttonData="NEXT_FORWARD" defaultButton="true" disable="${!controller.finished}" visible="${controller.showResultScreen}" managed="${controller.showResultScreen}" onAction="#exportResults"/>
<Button text="%health.check.runBatchBtn" ButtonBar.buttonData="NEXT_FORWARD" defaultButton="true" onAction="#runSelectedChecks" disable="${controller.chosenTaskCount == ZERO}" visible="${!controller.showResultScreen}" managed="${!controller.showResultScreen}"/>
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" onAction="#cancelRun" visible="${controller.running}" managed="${controller.running}" />
<Button text="%health.check.exportBtn" ButtonBar.buttonData="NEXT_FORWARD" defaultButton="true" disable="${controller.running}" visible="${controller.mainRunStarted}" managed="${controller.mainRunStarted}" onAction="#exportResults"/>
<Button text="%health.check.runBatchBtn" ButtonBar.buttonData="NEXT_FORWARD" defaultButton="true" onAction="#runSelectedChecks" disable="${controller.chosenTaskCount == ZERO}" visible="${!controller.mainRunStarted}" managed="${!controller.mainRunStarted}"/>
</buttons>
</ButtonBar>
</children>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import org.cryptomator.ui.health.CheckStateIconView?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.control.Label?>
<HBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.cryptomator.ui.health.CheckListCellController"
prefHeight="30.0" prefWidth="150.0"
alignment="CENTER_LEFT" spacing="6">
<padding>
<Insets topRightBottomLeft="6"/>
</padding>
<StackPane minWidth="20" minHeight="20" alignment="CENTER">
<CheckBox fx:id="forRunSelectedCheckBox" visible="${controller.checkRunnable}" />
<CheckStateIconView check="${controller.check}" glyphSize="20" visible="${!controller.checkRunnable}"/>
</StackPane>
<Label text="${controller.checkName}"/>
</HBox>

View File

@@ -6,9 +6,6 @@
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.control.ProgressIndicator?>
<?import javafx.scene.text.Text?>
<?import javafx.scene.layout.Pane?>
<?import javafx.scene.layout.StackPane?>
<HBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
@@ -22,18 +19,14 @@
<Insets topRightBottomLeft="6"/>
</padding>
<children>
<FontAwesome5IconView fx:id="iconView" HBox.hgrow="NEVER" glyphSize="16" glyph="${controller.glyph}"/>
<StackPane minWidth="25" minHeight="25">
<FontAwesome5IconView fx:id="severityView" HBox.hgrow="NEVER" glyphSize="16" glyph="${controller.severityGlyph}"/>
</StackPane>
<Label text="${controller.description}"/>
<Region HBox.hgrow="ALWAYS"/>
<!-- TODO: setting the minWidth of the button is just a workaround.
What we actually want to do is to prevent shrinking the button more than the text
-> own subclass of HBox is needed -->
<StackPane HBox.hgrow="NEVER">
<children>
<Button fx:id="fixButton" text="%health.check.fixBtn" visible="${controller.fixable}" managed="${controller.fixable}" onAction="#fix" alignment="CENTER" minWidth="-Infinity"/>
<ProgressIndicator progress="-1" prefWidth="12" prefHeight="12" visible="${controller.fixing}" managed="${controller.fixing}"/>
<FontAwesome5IconView glyph="CHECK" glyphSize="16" visible="${controller.fixed}" managed="${controller.fixed}"/>
</children>
<Button fx:id="fixButton" text="%health.fix.fixBtn" visible="${controller.fixable}" onAction="#fix" alignment="CENTER" minWidth="-Infinity"/>
<FontAwesome5IconView fx:id="fixView" styleClass="glyph-icon-muted" glyph="${controller.fixGlyph}" glyphSize="16" visible="${controller.fixRunningOrDone}" managed="${controller.fixRunningOrDone}"/>
</StackPane>
</children>
</HBox>

View File

@@ -1,41 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.layout.VBox?>
<VBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.health.StartController"
minWidth="400"
maxWidth="400"
minHeight="145"
prefWidth="600"
prefHeight="400"
spacing="12">
<padding>
<Insets topRightBottomLeft="12"/>
</padding>
<children>
<Label text="TODO loading config..." visible="${controller.loading}" managed="${controller.loading}" wrapText="true" contentDisplay="LEFT">
<graphic>
<FontAwesome5IconView glyph="SPINNER"/>
</graphic>
</Label>
<Label text="%health.start.configInvalid" visible="${controller.failed}" managed="${controller.failed}" wrapText="true" contentDisplay="LEFT">
<graphic>
<FontAwesome5IconView glyph="TIMES" styleClass="glyph-icon-red"/>
</graphic>
</Label>
<Label text="%health.start.configValid" visible="${controller.loaded}" managed="${controller.loaded}" wrapText="true" contentDisplay="LEFT">
<graphic>
<FontAwesome5IconView glyph="CHECK" styleClass="glyph-icon-primary"/>
</graphic>
</Label>
<HBox VBox.vgrow="ALWAYS">
<VBox alignment="CENTER" minWidth="175" maxWidth="175">
<ImageView VBox.vgrow="ALWAYS" fitHeight="128" preserveRatio="true" smooth="true" cache="true">
<Image url="@../img/bot/bot.png"/>
</ImageView>
</VBox>
<VBox HBox.hgrow="ALWAYS" alignment="CENTER">
<padding>
<Insets topRightBottomLeft="12"/>
</padding>
<Label text="%health.intro.header" styleClass="label-large"/>
<Region minHeight="15"/>
<VBox>
<Label text="%health.intro.text" wrapText="true"/>
<GridPane alignment="CENTER_LEFT" >
<padding>
<Insets left="6"/>
</padding>
<columnConstraints>
<ColumnConstraints minWidth="20" halignment="LEFT"/>
<ColumnConstraints fillWidth="true"/>
</columnConstraints>
<rowConstraints>
<RowConstraints valignment="TOP"/>
<RowConstraints valignment="TOP"/>
<RowConstraints valignment="TOP"/>
</rowConstraints>
<Label text="1." GridPane.rowIndex="0" GridPane.columnIndex="0" />
<Label text="%health.intro.remarkSync" wrapText="true" GridPane.rowIndex="0" GridPane.columnIndex="1" />
<Label text="2." GridPane.rowIndex="1" GridPane.columnIndex="0" />
<Label text="%health.intro.remarkFix" wrapText="true" GridPane.rowIndex="1" GridPane.columnIndex="1" />
<Label text="3." GridPane.rowIndex="2" GridPane.columnIndex="0" />
<Label text="%health.intro.remarkBackup" wrapText="true" GridPane.rowIndex="2" GridPane.columnIndex="1" />
</GridPane>
<Region minHeight="15"/>
<CheckBox text="%health.intro.affirmation" fx:id="affirmationBox"/>
</VBox>
</VBox>
</HBox>
<ButtonBar buttonMinWidth="120" buttonOrder="+CX">
<buttons>
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#close"/>
<Button text="%generic.button.next" ButtonBar.buttonData="NEXT_FORWARD" disable="${!controller.loaded}" defaultButton="true" onAction="#next"/>
<Button text="%generic.button.next" ButtonBar.buttonData="NEXT_FORWARD" disable="${!affirmationBox.selected}" defaultButton="true" onAction="#next"/>
</buttons>
</ButtonBar>
</children>

View File

@@ -0,0 +1,44 @@
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.TextArea?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.TitledPane?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.text.TextFlow?>
<?import javafx.scene.text.Text?>
<VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.cryptomator.ui.health.StartFailController"
prefWidth="600"
prefHeight="400"
spacing="12">
<padding>
<Insets topRightBottomLeft="12"/>
</padding>
<Label text="%health.fail.header" styleClass="label-large" />
<TextFlow fx:id="ioErrorLabel" visible="${controller.ioException}" managed="${controller.ioException}">
<Text text="%health.fail.ioError" />
<Text text="${controller.localizedErrorMessage}"/>
</TextFlow>
<Label fx:id="parseErrorLabel" text="%health.fail.parseError" visible="${controller.parseException}" managed="${controller.parseException}"/>
<TitledPane fx:id="moreInfoPane" text="%health.fail.moreInfo" expanded="false">
<graphic>
<HBox alignment="CENTER" minWidth="8">
<FontAwesome5IconView glyph="${controller.moreInfoIcon}"/>
</HBox>
</graphic>
<content>
<TextArea VBox.vgrow="ALWAYS" text="${controller.stackTrace}" prefRowCount="20" editable="false" />
</content>
</TitledPane>
<Region VBox.vgrow="ALWAYS"/>
<ButtonBar buttonMinWidth="120" buttonOrder="+CX">
<buttons>
<Button text="%generic.button.close" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#close"/>
</buttons>
</ButtonBar>
</VBox>

View File

@@ -36,13 +36,5 @@
<fx:include source="vault_options_masterkey.fxml"/>
</content>
</Tab>
<Tab fx:id="healthTab" id="HEALTH" text="%vaultOptions.health">
<graphic>
<FontAwesome5IconView glyph="STETHOSCOPE"/>
</graphic>
<content>
<fx:include source="vault_options_health.fxml"/>
</content>
</Tab>
</tabs>
</TabPane>

View File

@@ -12,6 +12,7 @@
<?import javafx.scene.text.Text?>
<?import org.cryptomator.ui.controls.NumericTextField?>
<?import org.cryptomator.ui.controls.FormattedLabel?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<VBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.vaultoptions.GeneralVaultOptionsController"
@@ -39,5 +40,11 @@
<Label text="%vaultOptions.general.actionAfterUnlock"/>
<ChoiceBox fx:id="actionAfterUnlockChoiceBox"/>
</HBox>
<Button fx:id="healthCheckButton" text="%vaultOptions.general.startHealthCheckBtn" onAction="#startHealthCheck">
<graphic>
<FontAwesome5IconView glyph="STETHOSCOPE"/>
</graphic>
</Button>
</children>
</VBox>

View File

@@ -1,49 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.shape.Box?>
<?import javafx.scene.Group?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.collections.ObservableList?>
<?import javafx.collections.FXCollections?>
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.control.CheckBox?>
<VBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.vaultoptions.HealthVaultOptionsController"
spacing="6">
<padding>
<Insets topRightBottomLeft="12"/>
</padding>
<children>
<Label text="%vaultOptions.health.introduction" wrapText="true"/>
<Label text="%vaultOptions.health.remarks" wrapText="true"/>
<GridPane >
<padding>
<Insets left="6"/>
</padding>
<columnConstraints>
<ColumnConstraints minWidth="20" halignment="LEFT"/>
<ColumnConstraints fillWidth="true"/>
</columnConstraints>
<rowConstraints>
<RowConstraints valignment="TOP"/>
<RowConstraints valignment="TOP"/>
<RowConstraints valignment="TOP"/>
</rowConstraints>
<Label text="1." GridPane.rowIndex="0" GridPane.columnIndex="0" />
<Label text="%vaultOptions.health.remarkSync" wrapText="true" GridPane.rowIndex="0" GridPane.columnIndex="1" />
<Label text="2." GridPane.rowIndex="1" GridPane.columnIndex="0" />
<Label text="%vaultOptions.health.remarkFix" wrapText="true" GridPane.rowIndex="1" GridPane.columnIndex="1" />
<Label text="3." GridPane.rowIndex="2" GridPane.columnIndex="0" />
<Label text="%vaultOptions.health.remarkBackup" wrapText="true" GridPane.rowIndex="2" GridPane.columnIndex="1" />
</GridPane>
<CheckBox text="%vaultOptions.health.affirmation" fx:id="affirmationBox"/>
<Button text="%vaultOptions.health.startBtn" disable="${!affirmationBox.selected}" onAction="#startHealthCheck"/>
</children>
</VBox>

View File

@@ -96,7 +96,7 @@ forgetPassword.information=This will delete the saved password of this vault fro
forgetPassword.confirmBtn=Forget Password
# Unlock
unlock.title=Unlock Vault
unlock.title=Unlock "%s"
unlock.passwordPrompt=Enter password for "%s":
unlock.savePassword=Remember Password
unlock.unlockBtn=Unlock
@@ -147,29 +147,38 @@ migration.impossible.reason=The vault cannot be automatically migrated because i
migration.impossible.moreInfo=The vault can still be opened with an older version. For instructions on how to manually migrate a vault, visit
# Health Check
health.title=Vault Health Check
health.start.configValid=Reading and parsing vault configuration file was successful. Proceed to select checks.
health.start.configInvalid=Error while reading and parsing the vault configuration file.
health.checkList.header=Available Health Checks
health.checkList.selectAllBox=Select All
## Start
health.title=Health Check of "%s"
health.intro.header=Health Check
health.intro.text=Health Check is a collection of checks to detect and possibly fix problems in the internal structure of your vault. Please keep in mind:
health.intro.remarkSync=Ensure all devices are completely synced, this resolves most problems.
health.intro.remarkFix=Not all problems can be fixed.
health.intro.remarkBackup=If data is corrupted, only a backup can help.
health.intro.affirmation=I have read and understood the above information
## Start Failure
health.fail.header=Error on loading Vault Configuration
health.fail.ioError=An error happened while accessing and reading the config file.
health.fail.parseError=An error happened while parsing the vault config.
health.fail.moreInfo=More Info
## Check Selection
health.checkList.description=Select checks in the left list or use the buttons below.
health.checkList.selectAllButton=Select All Checks
health.checkList.deselectAllButton=Deselect All Checks
health.check.runBatchBtn=Run Selected Checks
## Detail view
health.check.detail.noSelectedCheck=For results select a finished health check in the left list.
health.check.detail.header=Results of %s
health.check.detail.taskNotStarted=The check was not selected to run.
health.check.detail.taskScheduled=The check is scheduled.
health.check.detail.taskRunning=The check is currently running…
health.check.detail.taskSucceeded=The check finished successfully after %s.
health.check.detail.taskFailed=The check exited due to an error.
health.check.detail.taskCancelled=The check was cancelled.
health.check.detail.problemCount=Found %d problems and %d unfixable errors.
health.check.detail.checkScheduled=The check is scheduled.
health.check.detail.checkRunning=The check is currently running…
health.check.detail.checkSkipped=The check was not selected to run.
health.check.detail.checkFinished=The check finished successfully.
health.check.detail.checkFinishedAndFound=The check finished running. Please review the results.
health.check.detail.checkFailed=The check exited due to an error.
health.check.detail.checkCancelled=The check was cancelled.
health.check.exportBtn=Export Report
health.check.fixBtn=Fix
health.check.detail.hmsFormat= %d hours, %2d minutes and %2d seconds
health.check.detail.msFormat= %d minutes and %2d seconds
health.check.detail.sFormat= %d seconds
## Checks
health.org.cryptomator.cryptofs.health.dirid.DirIdCheck=Directory Check
## Fix Application
health.fix.fixBtn=Fix
health.fix.successTip=Fix successful
health.fix.failTip=Fix failed, see log for details
# Preferences
preferences.title=Preferences
@@ -310,6 +319,8 @@ vaultOptions.general.actionAfterUnlock=After successful unlock
vaultOptions.general.actionAfterUnlock.ignore=Do nothing
vaultOptions.general.actionAfterUnlock.reveal=Reveal Drive
vaultOptions.general.actionAfterUnlock.ask=Ask
vaultOptions.general.startHealthCheckBtn=Start Health Check
## Mount
vaultOptions.mount=Mounting
vaultOptions.mount.readonly=Read-Only
@@ -328,15 +339,6 @@ vaultOptions.masterkey.forgetSavedPasswordBtn=Forget Saved Password
vaultOptions.masterkey.recoveryKeyExplanation=A recovery key is your only means to restore access to a vault if you lose your password.
vaultOptions.masterkey.showRecoveryKeyBtn=Display Recovery Key
vaultOptions.masterkey.recoverPasswordBtn=Recover Password
## Health
vaultOptions.health=Health Check
vaultOptions.health.startBtn=Start Health Check
vaultOptions.health.introduction=Health Check is a collection of checks to detect and possibly fix problems in the internal structure of your vault.
vaultOptions.health.remarks=Please keep in mind:
vaultOptions.health.remarkSync=Incomplete synchronisation causes most problems. Ensure that every device is completely synced.
vaultOptions.health.remarkFix=Not all problems can be fixed.
vaultOptions.health.remarkBackup=If data is corrupted, only a backup can help.
vaultOptions.health.affirmation=I have read and understood the above information.
# Recovery Key