cleanup updater

This commit is contained in:
Sebastian Stenzel
2025-11-04 11:12:43 +01:00
parent c938c42c00
commit 093f0e8c94
7 changed files with 75 additions and 92 deletions

View File

@@ -17,7 +17,6 @@ import javafx.beans.binding.StringExpression;
import javafx.beans.property.ObjectProperty;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.concurrent.Worker;
import javafx.util.Duration;
import java.net.http.HttpClient;
import java.time.Instant;
@@ -106,6 +105,10 @@ public class UpdateChecker extends ScheduledService<UpdateInfo> {
/* Observable Properties */
public String getLatestVersion() {
return latestVersion.get();
}
public StringExpression latestVersionProperty() {
return latestVersion;
}
@@ -118,10 +121,18 @@ public class UpdateChecker extends ScheduledService<UpdateInfo> {
return updateAvailable;
}
public boolean isCheckFailed() {
return checkFailed.get();
}
public BooleanBinding checkFailedProperty() {
return checkFailed;
}
public Instant getLastSuccessfulUpdateCheck() {
return lastSuccessfulUpdateCheck.get();
}
public ObjectProperty<Instant> lastSuccessfulUpdateCheckProperty() {
return lastSuccessfulUpdateCheck;
}

View File

@@ -2,7 +2,6 @@ package org.cryptomator.ui.preferences;
import org.cryptomator.common.Environment;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.integrations.update.UpdateInfo;
import org.cryptomator.integrations.update.UpdateStep;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.fxapp.UpdateChecker;
@@ -16,11 +15,9 @@ import javafx.animation.PauseTransition;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.BooleanExpression;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.binding.StringBinding;
import javafx.beans.binding.StringExpression;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableValue;
@@ -43,27 +40,21 @@ import java.util.ResourceBundle;
public class UpdatesPreferencesController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(UpdatesPreferencesController.class);
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(Locale.getDefault());
private final Application application;
private final Environment environment;
private final ResourceBundle resourceBundle;
private final Settings settings;
private final UpdateChecker updateChecker;
private final ObjectBinding<ContentDisplay> updateButtonState;
private final StringExpression latestVersion;
private final ObservableValue<Instant> lastSuccessfulUpdateCheck;
private final StringBinding lastUpdateCheckMessage;
private final ObservableValue<String> timeDifferenceMessage;
private final String currentVersion;
private final BooleanBinding checkFailed;
private final BooleanProperty upToDateLabelVisible = new SimpleBooleanProperty(false);
private final DateTimeFormatter formatter;
private final BooleanBinding upToDate;
private final UpdateService updateService;
private final StringBinding updateButtonTitle;
private final ObjectBinding<Worker<?>> worker;
private final BooleanExpression running;
private final StringBinding updateButtonTitle;
private final ObjectBinding<ContentDisplay> updateButtonState;
private final ObservableValue<String> timeDifferenceMessage;
private final StringBinding lastUpdateCheckMessage;
private final BooleanProperty upToDateLabelVisible = new SimpleBooleanProperty(false);
/* FXML */
public CheckBox checkForUpdatesCheckbox;
@@ -75,39 +66,27 @@ public class UpdatesPreferencesController implements FxController {
this.resourceBundle = resourceBundle;
this.settings = settings;
this.updateChecker = updateChecker;
this.latestVersion = updateChecker.latestVersionProperty();
this.lastSuccessfulUpdateCheck = updateChecker.lastSuccessfulUpdateCheckProperty();
this.timeDifferenceMessage = Bindings.createStringBinding(this::getTimeDifferenceMessage, lastSuccessfulUpdateCheck);
this.lastUpdateCheckMessage = Bindings.createStringBinding(this::getLastUpdateCheckMessage, lastSuccessfulUpdateCheck);
this.currentVersion = updateChecker.getCurrentVersion();
this.formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(Locale.getDefault());
this.upToDate = updateChecker.updateCheckStateProperty().isEqualTo(UpdateChecker.UpdateCheckState.CHECK_SUCCESSFUL).and(latestVersion.isEqualTo(currentVersion));
this.checkFailed = updateChecker.checkFailedProperty();
this.updateService = new UpdateService(updateChecker.lastValueProperty().map(UpdateInfo::updateMechanism));
this.worker = Bindings.when(updateChecker.updateAvailableProperty()).<Worker<?>>then(updateService).otherwise(updateChecker);
this.updateService = new UpdateService(updateChecker.lastValueProperty());
this.worker = Bindings.when(updateChecker.updateAvailableProperty()).<Worker<?>>then(this.updateService).otherwise(this.updateChecker);
this.running = Bindings.createBooleanBinding(this::isRunning, updateService.stateProperty(), updateChecker.stateProperty());
this.updateButtonTitle = Bindings.createStringBinding(this::getUpdateButtonTitle, worker, updateService.stateProperty(), updateService.messageProperty());
this.updateButtonState = Bindings.createObjectBinding(this::getUpdateButtonState, updateChecker.stateProperty(), updateService.stateProperty());
updateChecker.updateAvailableProperty().addListener((_, _, newVal) -> LOG.info("Update available: {}", newVal));
updateService.setOnSucceeded(this::updateSucceeded);
updateService.setOnFailed(this::updateFailed);
this.timeDifferenceMessage = Bindings.createStringBinding(this::getTimeDifferenceMessage, updateChecker.lastSuccessfulUpdateCheckProperty());
this.lastUpdateCheckMessage = Bindings.createStringBinding(this::getLastUpdateCheckMessage, updateChecker.lastSuccessfulUpdateCheckProperty());
}
public void initialize() {
checkForUpdatesCheckbox.selectedProperty().bindBidirectional(settings.checkForUpdates);
upToDate.addListener((_, _, newVal) -> {
if (newVal) {
updateChecker.updateAvailableProperty().addListener((_, _, hasUpdate) -> {
if (!hasUpdate) {
upToDateLabelVisible.set(true);
PauseTransition delay = new PauseTransition(javafx.util.Duration.seconds(5));
delay.setOnFinished(_ -> upToDateLabelVisible.set(false));
delay.play();
}
});
updateService.setOnSucceeded(this::updateSucceeded);
updateService.setOnFailed(this::updateFailed);
}
@FXML
@@ -158,26 +137,14 @@ public class UpdatesPreferencesController implements FxController {
}
}
public StringExpression latestVersionProperty() {
return latestVersion;
}
public String getLatestVersion() {
return latestVersion.get();
}
public String getCurrentVersion() {
return currentVersion;
}
public StringBinding lastUpdateCheckMessageProperty() {
return lastUpdateCheckMessage;
}
public String getLastUpdateCheckMessage() {
Instant lastCheck = lastSuccessfulUpdateCheck.getValue();
Instant lastCheck = updateChecker.getLastSuccessfulUpdateCheck();
if (lastCheck != null && !lastCheck.equals(Settings.DEFAULT_TIMESTAMP)) {
return formatter.format(LocalDateTime.ofInstant(lastCheck, ZoneId.systemDefault()));
return FORMATTER.format(LocalDateTime.ofInstant(lastCheck, ZoneId.systemDefault()));
} else {
return "-";
}
@@ -188,7 +155,7 @@ public class UpdatesPreferencesController implements FxController {
}
public String getTimeDifferenceMessage() {
var lastSuccessCheck = lastSuccessfulUpdateCheck.getValue();
var lastSuccessCheck = updateChecker.getLastSuccessfulUpdateCheck();
var duration = Duration.between(lastSuccessCheck, Instant.now());
var hours = duration.toHours();
if (lastSuccessCheck.equals(Settings.DEFAULT_TIMESTAMP)) {
@@ -210,14 +177,6 @@ public class UpdatesPreferencesController implements FxController {
return upToDateLabelVisible.get();
}
public BooleanBinding checkFailedProperty() {
return checkFailed;
}
public boolean isCheckFailed() {
return checkFailed.getValue();
}
public ObjectBinding<Worker<?>> workerProperty() {
return worker;
}

View File

@@ -18,14 +18,20 @@ import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.List;
public abstract class DownloadUpdateMechanism implements UpdateMechanism {
public abstract class DownloadUpdateMechanism implements UpdateMechanism<DownloadUpdateMechanism.DownloadUpdateInfo> {
private static final Logger LOG = LoggerFactory .getLogger(DownloadUpdateMechanism.class);
private static final String LATEST_VERSION_API_URL = "https://api.cryptomator.org/connect/apps/desktop/latest-version?format=1";
private static final ObjectMapper MAPPER = new ObjectMapper();
public record DownloadUpdateInfo(
DownloadUpdateMechanism updateMechanism,
String version,
Asset asset
) implements UpdateInfo {}
@Override
public UpdateInfo checkForUpdate(String currentVersion, HttpClient httpClient) {
public DownloadUpdateInfo checkForUpdate(String currentVersion, HttpClient httpClient) {
try {
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(LATEST_VERSION_API_URL)).build();
HttpResponse<InputStream> response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
@@ -46,7 +52,7 @@ public abstract class DownloadUpdateMechanism implements UpdateMechanism {
@Nullable
@Blocking
abstract UpdateInfo checkForUpdate(String currentVersion, LatestVersionResponse response);
abstract DownloadUpdateInfo checkForUpdate(String currentVersion, LatestVersionResponse response);
@JsonIgnoreProperties(ignoreUnknown = true)
public record LatestVersionResponse(

View File

@@ -32,7 +32,7 @@ import java.util.concurrent.TimeUnit;
@FxApplicationScoped
@Priority(Priority.FALLBACK)
@DisplayName("Show Download Page") // TODO localize
public class FallbackUpdateMechanism implements UpdateMechanism {
public class FallbackUpdateMechanism implements UpdateMechanism<UpdateInfo> {
private static final Logger LOG = LoggerFactory.getLogger(FallbackUpdateMechanism.class);
private static final String LATEST_VERSION_API_URL = "https://api.cryptomator.org/connect/apps/desktop/latest-version";
@@ -62,7 +62,7 @@ public class FallbackUpdateMechanism implements UpdateMechanism {
var release = MAPPER.readValue(response.body(), LatestVersion.class);
var updateVersion = release.versionForCurrentOS();
if (UpdateMechanism.isUpdateAvailable(currentVersion, updateVersion)) {
return new UpdateInfo(updateVersion, this);
return UpdateInfo.of(updateVersion, this);
} else {
return null;
}
@@ -73,7 +73,7 @@ public class FallbackUpdateMechanism implements UpdateMechanism {
}
@Override
public UpdateStep firstStep() {
public UpdateStep firstStep(UpdateInfo updateInfo) {
return UpdateStep.of("Go to download page", this::openDownloadPage); // TODO localize
}

View File

@@ -5,7 +5,6 @@ import org.cryptomator.integrations.common.OperatingSystem;
import org.cryptomator.integrations.common.Priority;
import org.cryptomator.integrations.update.DownloadUpdateStep;
import org.cryptomator.integrations.update.UpdateFailedException;
import org.cryptomator.integrations.update.UpdateInfo;
import org.cryptomator.integrations.update.UpdateMechanism;
import org.cryptomator.integrations.update.UpdateStep;
import org.jetbrains.annotations.Nullable;
@@ -15,8 +14,10 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.HexFormat;
import java.util.List;
import java.util.UUID;
@@ -30,24 +31,25 @@ public class MacOsDmgUpdateMechanism extends DownloadUpdateMechanism {
private static final Logger LOG = LoggerFactory.getLogger(MacOsDmgUpdateMechanism.class);
@Override
UpdateInfo checkForUpdate(String currentVersion, LatestVersionResponse response) {
DownloadUpdateInfo checkForUpdate(String currentVersion, LatestVersionResponse response) {
String suffix = switch (System.getProperty("os.arch")) {
case "aarch64", "arm64" -> "arm64.dmg";
default -> "x64.dmg";
};
if (UpdateMechanism.isUpdateAvailable(response.latestVersion().macVersion(), currentVersion)
&& response.assets().stream().map(Asset::name).anyMatch(s -> s.endsWith(suffix))) {
return new UpdateInfo(response.latestVersion().macVersion(), this);
var updateVersion = response.latestVersion().macVersion();
var asset = response.assets().stream().filter(a -> a.name().endsWith(suffix)).findAny().orElse(null);
if (UpdateMechanism.isUpdateAvailable(updateVersion, currentVersion) && asset != null) {
return new DownloadUpdateMechanism.DownloadUpdateInfo(this, updateVersion, asset);
} else {
return null;
}
}
@Override
public UpdateStep firstStep() throws UpdateFailedException {
public UpdateStep firstStep(DownloadUpdateInfo updateInfo) throws UpdateFailedException {
try {
Path workDir = Files.createTempDirectory("cryptomator-update");
return new UpdateProcessImpl(workDir);
return new UpdateProcessImpl(workDir, updateInfo);
} catch (IOException e) {
throw new UpdateFailedException("Failed to create temporary directory for update", e);
}
@@ -55,14 +57,13 @@ public class MacOsDmgUpdateMechanism extends DownloadUpdateMechanism {
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 workDir) {
public UpdateProcessImpl(Path workDir, DownloadUpdateInfo updateInfo) {
var destination = workDir.resolve("update.dmg");
super(UPDATE_URI, destination, CHECKSUM, 60_000_000L); // initially assume 60 MB for the update size
var downloadUri = URI.create(updateInfo.asset().downloadUrl());
var checksum = HexFormat.of().withLowerCase().parseHex(updateInfo.asset().digest().substring(7)); // remove "sha256:" prefix
super(downloadUri, destination, checksum, 60_000_000L); // initially assume 60 MB for the update size
this.workDir = workDir;
}
@@ -111,12 +112,18 @@ public class MacOsDmgUpdateMechanism extends DownloadUpdateMechanism {
installPath = "/Applications/Cryptomator.app";
// throw new UpdateFailedException("Cannot determine destination path for Cryptomator.app, current path: " + selfPath);
}
LOG.info("Restarting to apply Update in {} now...", workDir);
String script = """
while kill -0 ${CRYPTOMATOR_PID} 2> /dev/null; do sleep 0.5; done;
cp -R 'Cryptomator.app' "${CRYPTOMATOR_INSTALL_PATH}";
open -a "${CRYPTOMATOR_INSTALL_PATH}"
if [ -d "${CRYPTOMATOR_INSTALL_PATH}" ]; then
echo "Removing old installation at ${CRYPTOMATOR_INSTALL_PATH}";
rm -rf "${CRYPTOMATOR_INSTALL_PATH}"
fi
mv 'Cryptomator.app' "${CRYPTOMATOR_INSTALL_PATH}";
open -a "${CRYPTOMATOR_INSTALL_PATH}";
""";
var command = List.of("bash", "-c", "nohup bash -c \"" + script + "\" >/Users/sebastian/Downloads/nohup.out 2>&1 &");
Files.writeString(workDir.resolve("install.sh"), script, StandardCharsets.US_ASCII, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
var command = List.of("bash", "-c", "nohup bash install.sh >install.log 2>&1 &");
var processBuilder = new ProcessBuilder(command);
processBuilder.directory(workDir.toFile());
processBuilder.environment().put("CRYPTOMATOR_PID", String.valueOf(ProcessHandle.current().pid()));

View File

@@ -1,5 +1,6 @@
package org.cryptomator.updater;
import org.cryptomator.integrations.update.UpdateInfo;
import org.cryptomator.integrations.update.UpdateMechanism;
import org.cryptomator.integrations.update.UpdateStep;
@@ -17,30 +18,30 @@ import java.util.concurrent.TimeUnit;
*/
public class UpdateService extends Service<UpdateStep> {
private ObservableValue<UpdateMechanism> updateMechanism;
private ObservableValue<UpdateInfo> updateInfo;
public UpdateService(ObservableValue<UpdateMechanism> updateMechanism) {
public UpdateService(ObservableValue<UpdateInfo> updateInfo) {
setExecutor(Executors.newVirtualThreadPerTaskExecutor());
this.updateMechanism = updateMechanism;
this.updateInfo = updateInfo;
}
@Override
protected Task<UpdateStep> createTask() {
return new RunAllStepsTask(updateMechanism.getValue());
return new RunAllStepsTask(updateInfo.getValue());
}
private static class RunAllStepsTask extends Task<UpdateStep> {
private final UpdateMechanism updateMechanism;
private final UpdateInfo updateInfo;
public RunAllStepsTask(UpdateMechanism updateMechanism) {
this.updateMechanism = Objects.requireNonNull(updateMechanism);
public RunAllStepsTask(UpdateInfo updateInfo) {
this.updateInfo = Objects.requireNonNull(updateInfo);
}
@Override
protected UpdateStep call() throws IOException {
try {
UpdateStep step = updateMechanism.firstStep();
UpdateStep step = updateInfo.updateMechanism().firstStep(updateInfo);
UpdateStep lastStep;
do {
step.start();