diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index adab47af0..6ba69a6ef 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -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; diff --git a/src/main/java/org/cryptomator/common/vaults/Vault.java b/src/main/java/org/cryptomator/common/vaults/Vault.java index 02fd03600..74ac7dc40 100644 --- a/src/main/java/org/cryptomator/common/vaults/Vault.java +++ b/src/main/java/org/cryptomator/common/vaults/Vault.java @@ -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); diff --git a/src/main/java/org/cryptomator/ui/common/FxmlFile.java b/src/main/java/org/cryptomator/ui/common/FxmlFile.java index b8d5bbff0..ea0c1ed38 100644 --- a/src/main/java/org/cryptomator/ui/common/FxmlFile.java +++ b/src/main/java/org/cryptomator/ui/common/FxmlFile.java @@ -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"), // diff --git a/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java b/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java index 6ec3cddb4..15b1718e1 100644 --- a/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java +++ b/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java @@ -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"), // diff --git a/src/main/java/org/cryptomator/ui/controls/FontAwesome5IconView.java b/src/main/java/org/cryptomator/ui/controls/FontAwesome5IconView.java index 3bfa70c46..4c89ca674 100644 --- a/src/main/java/org/cryptomator/ui/controls/FontAwesome5IconView.java +++ b/src/main/java/org/cryptomator/ui/controls/FontAwesome5IconView.java @@ -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 glyph = new SimpleObjectProperty<>(this, "glyph", DEFAULT_GLYPH); - private final DoubleProperty glyphSize = new SimpleDoubleProperty(this, "glyphSize", DEFAULT_GLYPH_SIZE); + protected final ObjectProperty glyph = new SimpleObjectProperty<>(this, "glyph", DEFAULT_GLYPH); + protected final DoubleProperty glyphSize = new SimpleDoubleProperty(this, "glyphSize", DEFAULT_GLYPH_SIZE); static { try { diff --git a/src/main/java/org/cryptomator/ui/health/BatchService.java b/src/main/java/org/cryptomator/ui/health/BatchService.java deleted file mode 100644 index 40f4e173f..000000000 --- a/src/main/java/org/cryptomator/ui/health/BatchService.java +++ /dev/null @@ -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 { - - private final Iterator remainingTasks; - - public BatchService(Iterable tasks) { - this.remainingTasks = tasks.iterator(); - } - - @Override - protected Task createTask() { - Preconditions.checkState(remainingTasks.hasNext(), "No remaining tasks"); - return remainingTasks.next(); - } - - @Override - protected void succeeded() { - if (remainingTasks.hasNext()) { - this.restart(); - } - } -} diff --git a/src/main/java/org/cryptomator/ui/health/Check.java b/src/main/java/org/cryptomator/ui/health/Check.java new file mode 100644 index 000000000..52bee578c --- /dev/null +++ b/src/main/java/org/cryptomator/ui/health/Check.java @@ -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 state = new SimpleObjectProperty<>(CheckState.RUNNABLE); + private final ObservableList results = FXCollections.observableArrayList(Result::observables); + private final ObjectProperty highestResultSeverity = new SimpleObjectProperty<>(null); + private final ObjectProperty 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 stateProperty() { + return state; + } + + CheckState getState() { + return state.get(); + } + + void setState(CheckState newState) { + state.set(newState); + } + + ObjectProperty errorProperty() { + return error; + } + + Throwable getError() { + return error.get(); + } + + void setError(Throwable t) { + error.set(t); + } + + ObjectProperty 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 getResults() { + return results; + } + + Observable[] observables() { + return new Observable[]{chosenForExecution, state, results, error}; + } +} diff --git a/src/main/java/org/cryptomator/ui/health/CheckDetailController.java b/src/main/java/org/cryptomator/ui/health/CheckDetailController.java index 7ccc9e09c..66f2e9bf5 100644 --- a/src/main/java/org/cryptomator/ui/health/CheckDetailController.java +++ b/src/main/java/org/cryptomator/ui/health/CheckDetailController.java @@ -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 results; - private final OptionalBinding taskState; - private final Binding taskName; - private final Binding taskDuration; - private final Binding taskRunning; - private final Binding taskScheduled; - private final Binding taskFinished; - private final Binding taskNotStarted; - private final Binding taskSucceeded; - private final Binding taskFailed; - private final Binding taskCancelled; + private final ObjectProperty check; + private final OptionalBinding checkState; + private final Binding checkName; + private final Binding checkRunning; + private final Binding checkScheduled; + private final Binding checkFinished; + private final Binding checkSkipped; + private final Binding checkSucceeded; + private final Binding checkFailed; + private final Binding checkCancelled; private final Binding countOfWarnSeverity; private final Binding countOfCritSeverity; + private final Binding warnOrCritsExist; private final ResultListCellFactory resultListCellFactory; - private final ResourceBundle resourceBundle; public ListView resultsListView; private Subscription resultSubscription; @Inject - public CheckDetailController(ObjectProperty selectedTask, ResultListCellFactory resultListCellFactory, ResourceBundle resourceBundle) { + public CheckDetailController(ObjectProperty 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 observable, HealthCheckTask oldValue, HealthCheckTask newValue) { + private void selectedTaskChanged(ObservableValue 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 taskNameProperty() { - return taskName; - } - - public String getTaskDuration() { - return taskDuration.getValue(); - } - - public Binding taskDurationProperty() { - return taskDuration; + public Binding 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 taskRunningProperty() { - return taskRunning; + public Binding checkRunningProperty() { + return checkRunning; } - public boolean isTaskFinished() { - return taskFinished.getValue(); + public boolean isCheckFinished() { + return checkFinished.getValue(); } - public Binding taskFinishedProperty() { - return taskFinished; + public Binding checkFinishedProperty() { + return checkFinished; } - public boolean isTaskScheduled() { - return taskScheduled.getValue(); + public boolean isCheckScheduled() { + return checkScheduled.getValue(); } - public Binding taskScheduledProperty() { - return taskScheduled; + public Binding checkScheduledProperty() { + return checkScheduled; } - public boolean isTaskNotStarted() { - return taskNotStarted.getValue(); + public boolean isCheckSkipped() { + return checkSkipped.getValue(); } - public Binding taskNotStartedProperty() { - return taskNotStarted; + public Binding checkSkippedProperty() { + return checkSkipped; } - public boolean isTaskSucceeded() { - return taskSucceeded.getValue(); + public boolean isCheckSucceeded() { + return checkSucceeded.getValue(); } - public Binding taskSucceededProperty() { - return taskSucceeded; + public Binding checkSucceededProperty() { + return checkSucceeded; } - public boolean isTaskFailed() { - return taskFailed.getValue(); + public boolean isCheckFailed() { + return checkFailed.getValue(); } - public Binding taskFailedProperty() { - return taskFailed; + public Binding checkFailedProperty() { + return checkFailed; } - public boolean isTaskCancelled() { - return taskCancelled.getValue(); + public boolean isCheckCancelled() { + return checkCancelled.getValue(); } - public Binding taskCancelledProperty() { - return taskCancelled; + public Binding 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 checkCancelledProperty() { + return checkCancelled; + } + + public ObjectProperty checkProperty() { + return check; + } + + public Check getCheck() { + return check.get(); + } } diff --git a/src/main/java/org/cryptomator/ui/health/CheckExecutor.java b/src/main/java/org/cryptomator/ui/health/CheckExecutor.java new file mode 100644 index 000000000..5b14bd17c --- /dev/null +++ b/src/main/java/org/cryptomator/ui/health/CheckExecutor.java @@ -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 tasksToExecute; + + + @Inject + public CheckExecutor(@HealthCheckWindow Vault vault, AtomicReference masterkeyRef, AtomicReference 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 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 { + + 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); + } + + } + +} \ No newline at end of file diff --git a/src/main/java/org/cryptomator/ui/health/CheckListCell.java b/src/main/java/org/cryptomator/ui/health/CheckListCell.java deleted file mode 100644 index 76a8f3c27..000000000 --- a/src/main/java/org/cryptomator/ui/health/CheckListCell.java +++ /dev/null @@ -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 { - - 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 isProblem = severity -> switch (severity) { - case WARN, CRITICAL -> true; - case INFO, GOOD -> false; - }; - return item.results().stream().map(Result::diagnosis).map(DiagnosticResult::getSeverity).anyMatch(isProblem); - } - -} diff --git a/src/main/java/org/cryptomator/ui/health/CheckListCellController.java b/src/main/java/org/cryptomator/ui/health/CheckListCellController.java new file mode 100644 index 000000000..799b73358 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/health/CheckListCellController.java @@ -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; + private final Binding checkName; + private final Binding checkRunnable; + private final List 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 checkProperty() { + return check; + } + + public Check getCheck() { + return check.get(); + } + + public void setCheck(Check c) { + check.set(c); + } + + public Binding checkNameProperty() { + return checkName; + } + + public String getCheckName() { + return checkName.getValue(); + } + + public Binding checkRunnableProperty() { + return checkRunnable; + } + + public boolean isCheckRunnable() { + return checkRunnable.getValue(); + } +} diff --git a/src/main/java/org/cryptomator/ui/health/CheckListCellFactory.java b/src/main/java/org/cryptomator/ui/health/CheckListCellFactory.java new file mode 100644 index 000000000..d8ccc8d48 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/health/CheckListCellFactory.java @@ -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, ListCell> { + + private final FxmlLoaderFactory fxmlLoaders; + + @Inject + CheckListCellFactory(@HealthCheckWindow FxmlLoaderFactory fxmlLoaders) { + this.fxmlLoaders = fxmlLoaders; + } + + @Override + public ListCell call(ListView 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 { + + 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); + } + } + } +} diff --git a/src/main/java/org/cryptomator/ui/health/CheckListController.java b/src/main/java/org/cryptomator/ui/health/CheckListController.java index 7710eb14d..75ecdef52 100644 --- a/src/main/java/org/cryptomator/ui/health/CheckListController.java +++ b/src/main/java/org/cryptomator/ui/health/CheckListController.java @@ -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 END_STATES = Set.of(Worker.State.FAILED, Worker.State.CANCELLED, Worker.State.SUCCEEDED); private final Stage window; - private final ObservableList tasks; - private final FilteredList chosenTasks; + private final ObservableList checks; + private final CheckExecutor checkExecutor; + private final FilteredList chosenChecks; private final ReportWriter reportWriter; - private final ExecutorService executorService; - private final ObjectProperty selectedTask; + private final ObjectProperty selectedCheck; + private final BooleanBinding mainRunStarted; //TODO: rerunning not considered for now + private final BooleanBinding somethingsRunning; private final Lazy errorComponentBuilder; - private final SimpleObjectProperty> runningTask; - private final Binding running; - private final Binding finished; private final IntegerBinding chosenTaskCount; private final BooleanBinding anyCheckSelected; - private final BooleanProperty showResultScreen; + private final CheckListCellFactory listCellFactory; /* FXML */ - public ListView checksListView; + public ListView checksListView; @Inject - public CheckListController(@HealthCheckWindow Stage window, Lazy> tasks, ReportWriter reportWriteTask, ObjectProperty selectedTask, ExecutorService executorService, Lazy errorComponentBuilder) { + public CheckListController(@HealthCheckWindow Stage window, List checks, CheckExecutor checkExecutor, ReportWriter reportWriteTask, ObjectProperty selectedCheck, Lazy 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 runningProperty() { - return running; - } - - public boolean isFinished() { - return finished.getValue(); - } - - public Binding 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() { diff --git a/src/main/java/org/cryptomator/ui/health/CheckStateIconView.java b/src/main/java/org/cryptomator/ui/health/CheckStateIconView.java new file mode 100644 index 000000000..4df4ec8ce --- /dev/null +++ b/src/main/java/org/cryptomator/ui/health/CheckStateIconView.java @@ -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 = new SimpleObjectProperty<>(); + private final ObservableObjectValue state; + private final ObservableObjectValue severity; + private final List 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 checkProperty() { + return check; + } + + public void setCheck(Check c) { + check.set(c); + } + + public Check getCheck() { + return check.get(); + } + +} diff --git a/src/main/java/org/cryptomator/ui/health/DummyHealthChecks.java b/src/main/java/org/cryptomator/ui/health/DummyHealthChecks.java deleted file mode 100644 index d2bccff37..000000000 --- a/src/main/java/org/cryptomator/ui/health/DummyHealthChecks.java +++ /dev/null @@ -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 consumer) { - // no-op - } - } - - public static class DummyCheck2 implements HealthCheck { - - @Override - public void check(Path path, VaultConfig vaultConfig, Masterkey masterkey, Cryptor cryptor, Consumer consumer) { - // no-op - } - } - - public static class DummyCheck3 implements HealthCheck { - - @Override - public void check(Path path, VaultConfig vaultConfig, Masterkey masterkey, Cryptor cryptor, Consumer consumer) { - // no-op - } - } - -} diff --git a/src/main/java/org/cryptomator/ui/health/HealthCheckComponent.java b/src/main/java/org/cryptomator/ui/health/HealthCheckComponent.java index 365ab63df..f78e815c6 100644 --- a/src/main/java/org/cryptomator/ui/health/HealthCheckComponent.java +++ b/src/main/java/org/cryptomator/ui/health/HealthCheckComponent.java @@ -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(); + Lazy startScene(); + + @FxmlScene(FxmlFile.HEALTH_START_FAIL) + Lazy 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) {} } diff --git a/src/main/java/org/cryptomator/ui/health/HealthCheckModule.java b/src/main/java/org/cryptomator/ui/health/HealthCheckModule.java index 78643f011..ad5ac6156 100644 --- a/src/main/java/org/cryptomator/ui/health/HealthCheckModule.java +++ b/src/main/java/org/cryptomator/ui/health/HealthCheckModule.java @@ -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 provideMasterkeyRef() { @@ -51,27 +63,20 @@ abstract class HealthCheckModule { @Provides @HealthCheckScoped - static Collection provideAvailableHealthChecks() { - return HealthCheck.allChecks(); - } - - @Provides - @HealthCheckScoped - static ObjectProperty provideSelectedHealthCheckTask() { + static ObjectProperty provideSelectedCheck() { return new SimpleObjectProperty<>(); } - /* Only inject with Lazy-Wrapper!*/ @Provides @HealthCheckScoped - static List provideAvailableHealthCheckTasks(Collection availableHealthChecks, @HealthCheckWindow Vault vault, AtomicReference masterkeyRef, AtomicReference vaultConfigRef, SecureRandom csprng, ResourceBundle resourceBundle) { - return availableHealthChecks.stream().map(check -> new HealthCheckTask(vault.getPath(), vaultConfigRef.get(), masterkeyRef.get(), csprng, check, resourceBundle)).toList(); + static List 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 showingListener) { + static Stage provideStage(StageFactory factory, @Named("healthCheckOwner") Stage owner, @HealthCheckWindow Vault vault, ChangeListener 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); } diff --git a/src/main/java/org/cryptomator/ui/health/HealthCheckTask.java b/src/main/java/org/cryptomator/ui/health/HealthCheckTask.java deleted file mode 100644 index 57cf6eb62..000000000 --- a/src/main/java/org/cryptomator/ui/health/HealthCheckTask.java +++ /dev/null @@ -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 { - - 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 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 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(); - } -} diff --git a/src/main/java/org/cryptomator/ui/health/ReportWriter.java b/src/main/java/org/cryptomator/ui/health/ReportWriter.java index 4366d14f0..18b785e91 100644 --- a/src/main/java/org/cryptomator/ui/health/ReportWriter.java +++ b/src/main/java/org/cryptomator/ui/health/ReportWriter.java @@ -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 tasks) throws IOException { + protected void writeReport(Collection 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()); diff --git a/src/main/java/org/cryptomator/ui/health/Result.java b/src/main/java/org/cryptomator/ui/health/Result.java index 23d812f50..8327a1130 100644 --- a/src/main/java/org/cryptomator/ui/health/Result.java +++ b/src/main/java/org/cryptomator/ui/health/Result.java @@ -17,10 +17,7 @@ record Result(DiagnosticResult diagnosis, ObjectProperty 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)); } diff --git a/src/main/java/org/cryptomator/ui/health/ResultListCellController.java b/src/main/java/org/cryptomator/ui/health/ResultListCellController.java index 683d0fb93..812fb97c5 100644 --- a/src/main/java/org/cryptomator/ui/health/ResultListCellController.java +++ b/src/main/java/org/cryptomator/ui/health/ResultListCellController.java @@ -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; + private final ObservableObjectValue severity; private final Binding description; private final ResultFixApplier fixApplier; - private final OptionalBinding fixState; - private final ObjectBinding glyph; + private final ObservableObjectValue fixState; + private final ObjectBinding severityGlyph; + private final ObjectBinding fixGlyph; private final BooleanBinding fixable; private final BooleanBinding fixing; private final BooleanBinding fixed; + private final BooleanBinding fixFailed; + private final BooleanBinding fixRunningOrDone; + private final List 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 descriptionProperty() { + return description; + } + public String getDescription() { return description.getValue(); } - public ObjectBinding glyphProperty() { - return glyph; + public ObjectBinding 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 descriptionProperty() { - return description; + public ObjectBinding 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(); + } + + } diff --git a/src/main/java/org/cryptomator/ui/health/StartController.java b/src/main/java/org/cryptomator/ui/health/StartController.java index 56368e46a..ebba001b5 100644 --- a/src/main/java/org/cryptomator/ui/health/StartController.java +++ b/src/main/java/org/cryptomator/ui/health/StartController.java @@ -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 unverifiedVaultConfig; + private final Stage unlockWindow; + private final ObjectProperty unverifiedVaultConfig; private final KeyLoadingStrategy keyLoadingStrategy; private final ExecutorService executor; private final AtomicReference masterkeyRef; private final AtomicReference vaultConfigRef; private final Lazy checkScene; private final Lazy errorComponent; - private final ObjectProperty 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 masterkeyRef, AtomicReference vaultConfigRef, @FxmlScene(FxmlFile.HEALTH_CHECK_LIST) Lazy checkScene, Lazy errorComponent) { - this.vault = vault; + public StartController(@HealthCheckWindow Stage window, HealthCheckComponent.LoadUnverifiedConfigResult configLoadResult, @HealthCheckWindow KeyLoadingStrategy keyLoadingStrategy, ExecutorService executor, AtomicReference masterkeyRef, AtomicReference vaultConfigRef, @FxmlScene(FxmlFile.HEALTH_CHECK_LIST) Lazy checkScene, Lazy 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); } diff --git a/src/main/java/org/cryptomator/ui/health/StartFailController.java b/src/main/java/org/cryptomator/ui/health/StartFailController.java new file mode 100644 index 000000000..826766026 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/health/StartFailController.java @@ -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 loadError; + private final ObjectProperty 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 observable, boolean wasExpanded, boolean willExpand) { + moreInfoIcon.set(willExpand ? FontAwesome5Icon.CARET_DOWN : FontAwesome5Icon.CARET_RIGHT); + } + + @FXML + public void close() { + window.close(); + } + + /* Getter & Setter */ + + public ObjectProperty 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(); + } + +} diff --git a/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingStrategy.java b/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingStrategy.java index eaef7480b..614247ebc 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingStrategy.java +++ b/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingStrategy.java @@ -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. *

@@ -60,4 +64,36 @@ public interface KeyLoadingStrategy extends MasterkeyLoader { }; } + /** + * Makes the given user 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 Optional exception type thrown by user + * @throws MasterkeyLoadingFailedException If a non-recoverable exception is thrown by user + * @throws E Exception thrown by user and rethrown by this method + */ + default void use(KeyLoadingStrategyUser 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 { + + void use(KeyLoadingStrategy strategy) throws MasterkeyLoadingFailedException, E; + + } + } diff --git a/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java b/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java index 36c3eacf9..073258d80 100644 --- a/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java +++ b/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java @@ -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 { } 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); } } diff --git a/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java b/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java index 0ccea096e..a35b108d9 100644 --- a/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java +++ b/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java @@ -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(); + } } diff --git a/src/main/java/org/cryptomator/ui/vaultoptions/HealthVaultOptionsController.java b/src/main/java/org/cryptomator/ui/vaultoptions/HealthVaultOptionsController.java deleted file mode 100644 index 7b0842e97..000000000 --- a/src/main/java/org/cryptomator/ui/vaultoptions/HealthVaultOptionsController.java +++ /dev/null @@ -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(); - } -} diff --git a/src/main/java/org/cryptomator/ui/vaultoptions/SelectedVaultOptionsTab.java b/src/main/java/org/cryptomator/ui/vaultoptions/SelectedVaultOptionsTab.java index bfaff147a..9212b21dd 100644 --- a/src/main/java/org/cryptomator/ui/vaultoptions/SelectedVaultOptionsTab.java +++ b/src/main/java/org/cryptomator/ui/vaultoptions/SelectedVaultOptionsTab.java @@ -25,9 +25,4 @@ public enum SelectedVaultOptionsTab { * Show Auto-Lock tab */ AUTOLOCK, - - /** - * Show health tab - */ - HEALTH; } diff --git a/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsController.java b/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsController.java index 662232a49..20dac7594 100644 --- a/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsController.java +++ b/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsController.java @@ -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 selectedTabProperty) { @@ -50,7 +49,6 @@ public class VaultOptionsController implements FxController { case MOUNT -> mountTab; case KEY -> keyTab; case AUTOLOCK -> autoLockTab; - case HEALTH -> healthTab; }; } diff --git a/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsModule.java b/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsModule.java index 2de9421a0..cb6c109b9 100644 --- a/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsModule.java +++ b/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsModule.java @@ -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); } diff --git a/src/main/resources/META-INF/services/org.cryptomator.cryptofs.health.api.HealthCheck b/src/main/resources/META-INF/services/org.cryptomator.cryptofs.health.api.HealthCheck deleted file mode 100644 index e78e709b4..000000000 --- a/src/main/resources/META-INF/services/org.cryptomator.cryptofs.health.api.HealthCheck +++ /dev/null @@ -1,3 +0,0 @@ -org.cryptomator.ui.health.DummyHealthChecks$DummyCheck1 -org.cryptomator.ui.health.DummyHealthChecks$DummyCheck2 -org.cryptomator.ui.health.DummyHealthChecks$DummyCheck3 \ No newline at end of file diff --git a/src/main/resources/css/dark_theme.css b/src/main/resources/css/dark_theme.css index 8d20ad9be..87e5ee15e 100644 --- a/src/main/resources/css/dark_theme.css +++ b/src/main/resources/css/dark_theme.css @@ -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; } diff --git a/src/main/resources/css/light_theme.css b/src/main/resources/css/light_theme.css index b0ba8ac8c..37d5f666e 100644 --- a/src/main/resources/css/light_theme.css +++ b/src/main/resources/css/light_theme.css @@ -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; } diff --git a/src/main/resources/fxml/health_check_details.fxml b/src/main/resources/fxml/health_check_details.fxml index 51d9b22ae..65c6a409a 100644 --- a/src/main/resources/fxml/health_check_details.fxml +++ b/src/main/resources/fxml/health_check_details.fxml @@ -1,24 +1,29 @@ - + + - + - \ No newline at end of file diff --git a/src/main/resources/fxml/health_check_list.fxml b/src/main/resources/fxml/health_check_list.fxml index 91cd02975..1d8054ab6 100644 --- a/src/main/resources/fxml/health_check_list.fxml +++ b/src/main/resources/fxml/health_check_list.fxml @@ -24,23 +24,26 @@ - -