diff --git a/.idea/runConfigurations/Cryptomator_macOS.xml b/.idea/runConfigurations/Cryptomator_macOS.xml
index c777434a2..bdd57a54a 100644
--- a/.idea/runConfigurations/Cryptomator_macOS.xml
+++ b/.idea/runConfigurations/Cryptomator_macOS.xml
@@ -5,7 +5,7 @@
-
+
diff --git a/dist/mac/dmg/build.sh b/dist/mac/dmg/build.sh
index 26673589b..391ee7bb5 100755
--- a/dist/mac/dmg/build.sh
+++ b/dist/mac/dmg/build.sh
@@ -123,6 +123,7 @@ ${JAVA_HOME}/bin/jpackage \
--java-options "-Dcryptomator.integrationsMac.keychainServiceName=\"${APP_NAME}\"" \
--java-options "-Dcryptomator.mountPointsDir=\"@{userhome}/Library/Application Support${APP_NAME}/mnt\"" \
--java-options "-Dcryptomator.showTrayIcon=true" \
+ --java-options "-Dcryptomator.updateMechanism=org.cryptomator.updater.MacOsDmgUpdateMechanism" \
--java-options "-Dcryptomator.buildNumber=\"dmg-${REVISION_NO}\"" \
--mac-package-identifier ${PACKAGE_IDENTIFIER} \
--resource-dir ../resources
diff --git a/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java b/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java
index f33b474f1..70839b92f 100644
--- a/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java
+++ b/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java
@@ -3,9 +3,11 @@ package org.cryptomator.ui.preferences;
import org.cryptomator.common.Environment;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.integrations.update.UpdateMechanism;
-import org.cryptomator.integrations.update.UpdateProcess;
+import org.cryptomator.integrations.update.UpdateStep;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.fxapp.UpdateChecker;
+import org.cryptomator.updater.FallbackUpdateMechanism;
+import org.cryptomator.updater.UpdateService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -21,11 +23,13 @@ import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableValue;
-import javafx.concurrent.Task;
+import javafx.concurrent.Service;
+import javafx.concurrent.Worker;
+import javafx.concurrent.WorkerStateEvent;
+import javafx.event.EventType;
import javafx.fxml.FXML;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ContentDisplay;
-import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
@@ -36,8 +40,6 @@ import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Locale;
import java.util.ResourceBundle;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
@PreferencesScoped
@@ -68,14 +70,15 @@ public class UpdatesPreferencesController implements FxController {
private final BooleanBinding upToDate;
private final String downloadsUri;
private final UpdateMechanism updateMechanism;
- public final Task updatePreparationTask;
+ private final Service updateService;
private final StringBinding updateButtonTitle;
+ private final BooleanBinding updateReady;
/* FXML */
public CheckBox checkForUpdatesCheckbox;
@Inject
- UpdatesPreferencesController(Application application, Environment environment, ResourceBundle resourceBundle, Settings settings, UpdateChecker updateChecker, Environment env) {
+ UpdatesPreferencesController(Application application, Environment environment, ResourceBundle resourceBundle, Settings settings, UpdateChecker updateChecker, Environment env, FallbackUpdateMechanism fallbackUpdateMechanism) {
this.application = application;
this.environment = environment;
this.resourceBundle = resourceBundle;
@@ -93,18 +96,13 @@ public class UpdatesPreferencesController implements FxController {
this.checkFailed = updateChecker.checkFailedProperty();
this.lastUpdateCheckMessage = Bindings.createStringBinding(this::getLastUpdateCheckMessage, lastSuccessfulUpdateCheck);
this.downloadsUri = DOWNLOADS_URI_TEMPLATE.formatted(URLEncoder.encode(currentVersion, StandardCharsets.US_ASCII));
- this.updateMechanism = UpdateMechanism.get();
- this.updatePreparationTask = new Task<>() { // TODO custom class?
- @Override
- protected UpdateProcess call() throws IOException, InterruptedException {
- var updateProcess = updateMechanism.prepareUpdate();
- do {
- updateProgress(updateProcess.preparationProgress(), 1.0);
- } while (!updateProcess.await(100, TimeUnit.MILLISECONDS));
- return updateProcess;
- }
- };
- this.updateButtonTitle = Bindings.createStringBinding(this::getUpdateButtonTitle, updatePreparationTask.stateProperty());
+ this.updateMechanism = UpdateMechanism.get().orElse(fallbackUpdateMechanism);
+ this.updateService = new UpdateService(updateMechanism);
+ this.updateButtonTitle = Bindings.createStringBinding(this::getUpdateButtonTitle, updateService.stateProperty(), updateService.messageProperty());
+ this.updateReady = updateService.stateProperty().isEqualTo(Worker.State.READY);
+
+ updateService.setOnSucceeded(this::updateSucceeded);
+ updateService.setOnFailed(this::updateFailed);
}
public void initialize() {
@@ -136,28 +134,27 @@ public class UpdatesPreferencesController implements FxController {
@FXML
public void doUpdate() {
- if (updatePreparationTask.isDone()) {
- try {
- // TODO: check if all vaults closed?
- var restartProcess = updatePreparationTask.get().applyUpdate();
- if (restartProcess.isAlive()) {
- Platform.exit();
- } else {
- LOG.error("Update process terminated prematurely: {}", restartProcess.info().commandLine());
- }
- Platform.exit(); // TODO: prompt?
- } catch (IOException | InterruptedException | ExecutionException e) {
- LOG.error("Oh no", e); // TODO: Show error dialog
- }
- } else if (updatePreparationTask.isRunning()) {
- throw new IllegalStateException("Update already in progress");
- } else if (updatePreparationTask.isCancelled()) {
- throw new IllegalStateException("Update preparation task was cancelled");
+ updateService.start();
+ }
+
+ private void updateSucceeded(WorkerStateEvent workerStateEvent) {
+ assert workerStateEvent.getSource() == updateService;
+ var lastStep = updateService.getValue();
+ if (lastStep == UpdateStep.EXIT) {
+ LOG.info("Exiting app to update...");
+ Platform.exit();
+ } else if (lastStep == UpdateStep.RETRY) {
+ updateService.reset();
} else {
- Thread.startVirtualThread(updatePreparationTask);
+ LOG.info("Update succeeded.");
}
}
+ private void updateFailed(WorkerStateEvent workerStateEvent) {
+ assert workerStateEvent.getSource() == updateService;
+ LOG.error("Update failed.", updateService.getException());
+ }
+
/* Observable Properties */
public ObjectBinding checkForUpdatesButtonStateProperty() {
@@ -236,8 +233,8 @@ public class UpdatesPreferencesController implements FxController {
return checkFailed.getValue();
}
- public Task getUpdatePreparationTask() {
- return updatePreparationTask;
+ public Service getUpdateService() {
+ return updateService;
}
public StringBinding updateButtonTitleProperty() {
@@ -245,11 +242,20 @@ public class UpdatesPreferencesController implements FxController {
}
public String getUpdateButtonTitle() {
- return switch (updatePreparationTask.getState()) {
+ return switch (updateService.getState()) {
case READY -> updateMechanism.getName();
- case SCHEDULED, RUNNING -> "Preparing Update..."; // TODO: resourceBundle.getString("preferences.updates.preparingUpdate")...
+ case SCHEDULED, RUNNING -> updateService.getMessage(); // "Preparing Update..."; // TODO: resourceBundle.getString("preferences.updates.preparingUpdate")...
case SUCCEEDED -> "Restart to Update"; // TODO: resourceBundle.getString("preferences.updates.readyToRestart")...
case FAILED, CANCELLED -> "failed";
};
}
+
+ public boolean isUpdateReady() {
+ return updateReady.get();
+ }
+
+ public BooleanBinding updateReadyProperty() {
+ return updateReady;
+ }
+
}
diff --git a/src/main/java/org/cryptomator/updater/DownloadUpdateMechanism.java b/src/main/java/org/cryptomator/updater/DownloadUpdateMechanism.java
index 8434a9964..eac2ba617 100644
--- a/src/main/java/org/cryptomator/updater/DownloadUpdateMechanism.java
+++ b/src/main/java/org/cryptomator/updater/DownloadUpdateMechanism.java
@@ -17,8 +17,14 @@ public abstract class DownloadUpdateMechanism implements UpdateMechanism {
private static final ObjectMapper MAPPER = new ObjectMapper();
+ private final String assetSuffix;
+
+ protected DownloadUpdateMechanism(String assetSuffix) {
+ this.assetSuffix = assetSuffix;
+ }
+
@Override
- public boolean isUpdateAvailable() {
+ public boolean isUpdateAvailable(String currentVersion) {
try (var client = HttpClient.newHttpClient()) {
// TODO: check different source
HttpRequest request = HttpRequest.newBuilder().uri(URI.create("https://api.github.com/repos/cryptomator/cryptomator/releases/latest")).header("Accept", "application/vnd.github+json").build();
@@ -31,7 +37,8 @@ public abstract class DownloadUpdateMechanism implements UpdateMechanism {
var release = MAPPER.readValue(response.body(), GitHubRelease.class);
- return release.assets.stream().anyMatch(a -> a.name.endsWith("arm64.dmg"));
+ return release.assets.stream().anyMatch(a -> a.name.endsWith(assetSuffix))
+ && UpdateMechanism.isUpdateAvailable(release.tagName, currentVersion);
} catch (IOException | InterruptedException e) {
return false;
}
diff --git a/src/main/java/org/cryptomator/updater/FallbackUpdateMechanism.java b/src/main/java/org/cryptomator/updater/FallbackUpdateMechanism.java
new file mode 100644
index 000000000..ec0328bd2
--- /dev/null
+++ b/src/main/java/org/cryptomator/updater/FallbackUpdateMechanism.java
@@ -0,0 +1,57 @@
+package org.cryptomator.updater;
+
+import org.cryptomator.common.Environment;
+import org.cryptomator.integrations.common.DisplayName;
+import org.cryptomator.integrations.common.Priority;
+import org.cryptomator.integrations.update.UpdateMechanism;
+import org.cryptomator.integrations.update.UpdateStep;
+import org.cryptomator.integrations.update.UpdateStepAdapter;
+import org.cryptomator.ui.fxapp.FxApplicationScoped;
+import org.jetbrains.annotations.Nullable;
+
+import javax.inject.Inject;
+import javafx.application.Application;
+import javafx.application.Platform;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.TimeUnit;
+
+@FxApplicationScoped
+@Priority(Priority.FALLBACK)
+@DisplayName("Show Download Page") // TODO localize
+public class FallbackUpdateMechanism implements UpdateMechanism {
+
+ private static final String DOWNLOADS_URI_TEMPLATE = "https://cryptomator.org/downloads/" //
+ + "?utm_source=cryptomator-desktop" //
+ + "&utm_medium=update-notification&" //
+ + "utm_campaign=app-update-%s";
+
+ private final Application app;
+ private final Environment env;
+
+ @Inject
+ public FallbackUpdateMechanism(Application app, Environment env) {
+ this.app = app;
+ this.env = env;
+ }
+
+ @Override
+ public boolean isUpdateAvailable(String currentVersion) {
+ // FIXME: what source shall we use? self-hosted JSON?
+ return true;
+ }
+
+ @Override
+ public UpdateStep firstStep() {
+ return UpdateStep.of("Go to download page", this::openDownloadPage); // TODO localize
+ }
+
+ private UpdateStep openDownloadPage() {
+ var downloadUrl = DOWNLOADS_URI_TEMPLATE.formatted(URLEncoder.encode(env.getAppVersion(), StandardCharsets.US_ASCII));
+ Platform.runLater(() -> {
+ app.getHostServices().showDocument(downloadUrl);
+ });
+ return UpdateStep.RETRY; // allow running this "update mechanism" as many times as the user wants
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/updater/MacOsDmgUpdateMechanism.java b/src/main/java/org/cryptomator/updater/MacOsDmgUpdateMechanism.java
index 6d288bbae..c08a4bf12 100644
--- a/src/main/java/org/cryptomator/updater/MacOsDmgUpdateMechanism.java
+++ b/src/main/java/org/cryptomator/updater/MacOsDmgUpdateMechanism.java
@@ -3,9 +3,10 @@ package org.cryptomator.updater;
import org.cryptomator.integrations.common.DisplayName;
import org.cryptomator.integrations.common.OperatingSystem;
import org.cryptomator.integrations.common.Priority;
-import org.cryptomator.integrations.update.DownloadUpdateProcess;
+import org.cryptomator.integrations.update.DownloadUpdateStep;
import org.cryptomator.integrations.update.UpdateFailedException;
-import org.cryptomator.integrations.update.UpdateProcess;
+import org.cryptomator.integrations.update.UpdateStep;
+import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -26,8 +27,16 @@ public class MacOsDmgUpdateMechanism extends DownloadUpdateMechanism {
private static final Logger LOG = LoggerFactory.getLogger(MacOsDmgUpdateMechanism.class);
+ public MacOsDmgUpdateMechanism() {
+ String suffix = switch (System.getProperty("os.arch")) {
+ case "aarch64", "arm64" -> "arm64.dmg";
+ default -> "x64.dmg";
+ };
+ super(suffix);
+ }
+
@Override
- public UpdateProcess prepareUpdate() throws UpdateFailedException {
+ public UpdateStep firstStep() throws UpdateFailedException {
try {
Path workDir = Files.createTempDirectory("cryptomator-update");
return new UpdateProcessImpl(workDir);
@@ -36,18 +45,30 @@ public class MacOsDmgUpdateMechanism extends DownloadUpdateMechanism {
}
}
- private static class UpdateProcessImpl extends DownloadUpdateProcess {
+ private static class UpdateProcessImpl extends DownloadUpdateStep {
// FIXME: use URI and CHECKSUM from update API
private static final URI UPDATE_URI = URI.create("https://github.com/cryptomator/cryptomator/releases/download/1.17.0/Cryptomator-1.17.0-arm64.dmg");
private static final byte[] CHECKSUM = HexFormat.of().withLowerCase().parseHex("03f45e203204e93b39925cbb04e19c9316da4f77debaba4fb5071f0ec8e727e8");
+ private final Path workDir;
- public UpdateProcessImpl(Path downloadPath) {
- super(downloadPath, "update.dmg", UPDATE_URI, CHECKSUM,60_000_000L); // initially assume 60 MB for the update size
+ public UpdateProcessImpl(Path workDir) {
+ var destination = workDir.resolve("update.dmg");
+ super(UPDATE_URI, destination, CHECKSUM, 60_000_000L); // initially assume 60 MB for the update size
+ this.workDir = workDir;
}
@Override
- protected void postDownload(Path downloadPath) throws IOException {
+ public @Nullable UpdateStep nextStep() throws IllegalStateException, IOException {
+ if (!isDone()) {
+ throw new IllegalStateException("Update not yet downloaded");
+ } else if (downloadException != null) {
+ throw new UpdateFailedException("Download failed", downloadException);
+ }
+ return UpdateStep.of("Mounting...", this::mount);
+ }
+
+ private UpdateStep mount() throws IOException {
// Extract Cryptomator.app from the .dmg file
String script = """
hdiutil attach 'update.dmg' -mountpoint "/Volumes/Cryptomator_${MOUNT_ID}" -nobrowse -quiet &&
@@ -68,16 +89,10 @@ public class MacOsDmgUpdateMechanism extends DownloadUpdateMechanism {
} catch (InterruptedException e) {
throw new InterruptedIOException("Failed to extract DMG, interrupted");
}
+ return UpdateStep.of("Restarting...", this::restart);
}
- @Override
- public ProcessHandle applyUpdate() throws IllegalStateException, IOException {
- if (!isDone()) {
- throw new IllegalStateException("Update not yet downloaded");
- } else if (downloadException != null) {
- throw new IllegalStateException("Downloading update failed", downloadException);
- }
-
+ public UpdateStep restart() throws IllegalStateException, IOException {
String selfPath = ProcessHandle.current().info().command().orElse("");
String installPath;
if (selfPath.startsWith("/Applications/Cryptomator.app")) {
@@ -85,7 +100,8 @@ public class MacOsDmgUpdateMechanism extends DownloadUpdateMechanism {
} else if (selfPath.contains("/Cryptomator.app/")) {
installPath = selfPath.substring(0, selfPath.indexOf("/Cryptomator.app/")) + "/Cryptomator.app";
} else {
- throw new UpdateFailedException("Cannot determine destination path for Cryptomator.app, current path: " + selfPath);
+ installPath = "/Applications/Cryptomator.app";
+ // throw new UpdateFailedException("Cannot determine destination path for Cryptomator.app, current path: " + selfPath);
}
String script = """
while kill -0 ${CRYPTOMATOR_PID} 2> /dev/null; do sleep 0.5; done;
@@ -97,9 +113,10 @@ public class MacOsDmgUpdateMechanism extends DownloadUpdateMechanism {
processBuilder.directory(workDir.toFile());
processBuilder.environment().put("CRYPTOMATOR_PID", String.valueOf(ProcessHandle.current().pid()));
processBuilder.environment().put("CRYPTOMATOR_INSTALL_PATH", installPath);
- return processBuilder.start().toHandle();
+ processBuilder.start();
+
+ return UpdateStep.EXIT;
}
}
-
}
diff --git a/src/main/java/org/cryptomator/updater/UpdateService.java b/src/main/java/org/cryptomator/updater/UpdateService.java
new file mode 100644
index 000000000..d1e746dfd
--- /dev/null
+++ b/src/main/java/org/cryptomator/updater/UpdateService.java
@@ -0,0 +1,59 @@
+package org.cryptomator.updater;
+
+import org.cryptomator.integrations.update.UpdateMechanism;
+import org.cryptomator.integrations.update.UpdateStep;
+
+import javafx.concurrent.Service;
+import javafx.concurrent.Task;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A service that performs all update steps provided by the given {@link UpdateMechanism} in sequence.
+ */
+public class UpdateService extends Service {
+
+ private final UpdateMechanism updateMechanism;
+
+ public UpdateService(UpdateMechanism updateMechanism) {
+ this.updateMechanism = updateMechanism;
+ setExecutor(Executors.newVirtualThreadPerTaskExecutor()); }
+
+ @Override
+ protected Task createTask() {
+ return new RunAllStepsTask();
+ }
+
+ private class RunAllStepsTask extends Task {
+
+ @Override
+ protected UpdateStep call() throws IOException {
+ try {
+ UpdateStep step = updateMechanism.firstStep();
+ UpdateStep lastStep;
+ do {
+ step.start();
+ observeAndWaitFor(step);
+ lastStep = step;
+ step = step.nextStep();
+ } while (step != null);
+ return lastStep;
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new InterruptedIOException("Update interrupted");
+ }
+ }
+
+ private void observeAndWaitFor(UpdateStep step) throws InterruptedException {
+ do {
+ updateProgress(step.preparationProgress(), 1.0);
+ updateMessage(step.description());
+ } while (!step.await(100, TimeUnit.MILLISECONDS));
+ }
+
+ }
+
+
+}
diff --git a/src/main/resources/fxml/preferences_updates.fxml b/src/main/resources/fxml/preferences_updates.fxml
index 9d72e243a..0a37f0650 100644
--- a/src/main/resources/fxml/preferences_updates.fxml
+++ b/src/main/resources/fxml/preferences_updates.fxml
@@ -9,11 +9,11 @@
-
-
-
-
+
+
+
+
-