mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-22 04:31:27 +00:00
adjust to new multi-step update API
This commit is contained in:
@@ -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<UpdateProcess> updatePreparationTask;
|
||||
private final Service<UpdateStep> 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<ContentDisplay> checkForUpdatesButtonStateProperty() {
|
||||
@@ -236,8 +233,8 @@ public class UpdatesPreferencesController implements FxController {
|
||||
return checkFailed.getValue();
|
||||
}
|
||||
|
||||
public Task<UpdateProcess> getUpdatePreparationTask() {
|
||||
return updatePreparationTask;
|
||||
public Service<UpdateStep> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
59
src/main/java/org/cryptomator/updater/UpdateService.java
Normal file
59
src/main/java/org/cryptomator/updater/UpdateService.java
Normal file
@@ -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<UpdateStep> {
|
||||
|
||||
private final UpdateMechanism updateMechanism;
|
||||
|
||||
public UpdateService(UpdateMechanism updateMechanism) {
|
||||
this.updateMechanism = updateMechanism;
|
||||
setExecutor(Executors.newVirtualThreadPerTaskExecutor()); }
|
||||
|
||||
@Override
|
||||
protected Task<UpdateStep> createTask() {
|
||||
return new RunAllStepsTask();
|
||||
}
|
||||
|
||||
private class RunAllStepsTask extends Task<UpdateStep> {
|
||||
|
||||
@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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user