Jelajahi Sumber

moved update API to `integrations-api`

Sebastian Stenzel 2 minggu lalu
induk
melakukan
8f4392711e

+ 1 - 1
pom.xml

@@ -34,7 +34,7 @@
 
 		<!-- cryptomator dependencies -->
 		<cryptomator.cryptofs.version>2.9.0</cryptomator.cryptofs.version>
-		<cryptomator.integrations.version>1.6.0</cryptomator.integrations.version>
+		<cryptomator.integrations.version>1.7.0-SNAPSHOT</cryptomator.integrations.version>
 		<cryptomator.integrations.win.version>1.5.0</cryptomator.integrations.win.version>
 		<cryptomator.integrations.mac.version>1.4.0</cryptomator.integrations.mac.version>
 		<cryptomator.integrations.linux.version>1.6.0</cryptomator.integrations.linux.version>

+ 2 - 2
src/main/java/module-info.java

@@ -1,4 +1,5 @@
 import ch.qos.logback.classic.spi.Configurator;
+import org.cryptomator.integrations.update.UpdateMechanism;
 import org.cryptomator.networking.SSLContextWithPKCS12TrustStore;
 import org.cryptomator.common.locationpresets.DropboxLinuxLocationPresetsProvider;
 import org.cryptomator.common.locationpresets.DropboxMacLocationPresetsProvider;
@@ -21,7 +22,6 @@ import org.cryptomator.integrations.tray.TrayMenuController;
 import org.cryptomator.logging.LogbackConfiguratorFactory;
 import org.cryptomator.ui.traymenu.AwtTrayMenuController;
 import org.cryptomator.updater.MacOsDmgUpdateMechanism;
-import org.cryptomator.updater.UpdateMechanism;
 
 open module org.cryptomator.desktop {
 	requires static org.jetbrains.annotations;
@@ -64,7 +64,7 @@ open module org.cryptomator.desktop {
 	uses org.cryptomator.event.NotificationHandler;
 
 	// opens org.cryptomator.updater to org.cryptomator.integrations.api;
-	provides UpdateMechanism with MacOsDmgUpdateMechanism; // TODO: move to integrations-mac
+	provides UpdateMechanism with MacOsDmgUpdateMechanism;
 
 	provides TrayMenuController with AwtTrayMenuController;
 	provides Configurator with LogbackConfiguratorFactory;

+ 7 - 3
src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java

@@ -2,10 +2,10 @@ 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.ui.common.FxController;
 import org.cryptomator.ui.fxapp.UpdateChecker;
-import org.cryptomator.updater.UpdateMechanism;
-import org.cryptomator.updater.UpdateProcess;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -139,7 +139,11 @@ public class UpdatesPreferencesController implements FxController {
 			try {
 				// TODO: check if all vaults closed?
 				var restartProcess = updatePreparationTask.get().applyUpdate();
-				assert restartProcess.isAlive();
+				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

+ 1 - 0
src/main/java/org/cryptomator/updater/DownloadUpdateMechanism.java

@@ -3,6 +3,7 @@ package org.cryptomator.updater;
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import org.cryptomator.integrations.update.UpdateMechanism;
 
 import java.io.IOException;
 import java.io.InputStream;

+ 0 - 165
src/main/java/org/cryptomator/updater/DownloadUpdateProcess.java

@@ -1,165 +0,0 @@
-package org.cryptomator.updater;
-
-import java.io.FilterInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InterruptedIOException;
-import java.net.URI;
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-import java.nio.channels.Channels;
-import java.nio.channels.FileChannel;
-import java.nio.file.Path;
-import java.nio.file.StandardOpenOption;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicLong;
-import java.util.concurrent.atomic.LongAdder;
-
-public abstract class DownloadUpdateProcess implements UpdateProcess {
-
-	protected final Path workDir;
-	private final URI uri;
-	private final byte[] checksum;
-	private final AtomicLong totalBytes;
-	private final LongAdder loadedBytes = new LongAdder();
-	private final Thread downloadThread;
-	private final CountDownLatch downloadCompleted = new CountDownLatch(1);
-	protected volatile IOException downloadException;
-	protected volatile boolean downloadSuccessful;
-
-	/**
-	 * Creates a new DownloadUpdateProcess instance.
-	 * @param workdir The directory where the update will be downloaded to. Ideally, this should be a temporary directory that is cleaned up after the update process is complete.
-	 * @param uri The URI from which the update will be downloaded.
-	 * @param checksum (optional) The expected SHA-256 checksum of the downloaded file, can be null if not required.
-	 * @param estDownloadSize The estimated size of the download in bytes.
-	 */
-	protected DownloadUpdateProcess(Path workdir, URI uri, byte[] checksum, long estDownloadSize) {
-		this.workDir = workdir;
-		this.uri = uri;
-		this.checksum = checksum;
-		this.totalBytes = new AtomicLong(estDownloadSize);
-		this.downloadThread = Thread.ofVirtual().start(this::download);
-	}
-
-	@Override
-	public double preparationProgress() {
-		return (double) loadedBytes.sum() / totalBytes.get();
-	}
-
-	@Override
-	public void await() throws InterruptedException {
-		downloadCompleted.await();
-	}
-
-	@Override
-	public boolean await(long timeout, TimeUnit unit) throws InterruptedException {
-		return downloadCompleted.await(timeout, unit);
-	}
-
-	@Override
-	public void cancel() {
-		downloadThread.interrupt();
-	}
-
-	private void download() {
-		try {
-			download("update.dmg");
-			downloadSuccessful = true;
-		} catch (IOException e) {
-			// TODO: eventually handle this via structured concurrency?
-			downloadException = e;
-		} finally {
-			downloadCompleted.countDown();
-		}
-	}
-
-	/**
-	 * Downloads the update from the given URI and saves it to the specified filename in the working directory.
-	 * @param filename the name of the file to save the update as in the working directory
-	 * @throws IOException indicating I/O errors during the download or file writing process or due to checksum mismatch
-	 */
-	protected void download(String filename) throws IOException {
-		var request = HttpRequest.newBuilder().uri(uri).GET().build();
-		var downloadFile = workDir.resolve(filename);
-		try (HttpClient client = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.ALWAYS).build()) {
-			// make download request
-			var response = client.send(request, HttpResponse.BodyHandlers.ofInputStream());
-			if (response.statusCode() != 200) {
-				throw new IOException("Failed to download update, status code: " + response.statusCode());
-			}
-
-			// update totalBytes
-			response.headers().firstValueAsLong("Content-Length").ifPresent(totalBytes::set);
-
-			// prepare checksum calculation
-			MessageDigest sha256;
-			try {
-				sha256 = MessageDigest.getInstance("SHA-256"); // Initialize SHA-256 digest, not used here but can be extended for checksum validation
-			} catch (NoSuchAlgorithmException e) {
-				throw new AssertionError("Every implementation of the Java platform is required to support [...] SHA-256", e);
-			}
-
-			// write bytes to file
-			try (var in = new DownloadInputStream(response.body(), loadedBytes, sha256);
-				 var src = Channels.newChannel(in);
-				 var dst = FileChannel.open(downloadFile, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW)) {
-				dst.transferFrom(src, 0, totalBytes.get());
-			}
-
-			// verify checksum if provided
-			byte[] calculatedChecksum = sha256.digest();
-			if (!MessageDigest.isEqual(calculatedChecksum, checksum)) {
-				throw new IOException("Checksum verification failed for downloaded file: " + filename);
-			}
-
-			// post-download processing
-			postDownload(downloadFile);
-		} catch (InterruptedException e) {
-			throw new InterruptedIOException("Download interrupted");
-		}
-	}
-
-	protected void postDownload(Path downloadedFile) throws IOException {
-		// Default implementation does nothing, can be overridden by subclasses for specific post-download actions
-	}
-
-	/**
-	 * An InputStream decorator that counts the number of bytes read and updates a MessageDigest for checksum calculation.
-	 */
-	private static class DownloadInputStream extends FilterInputStream {
-
-		private final LongAdder counter;
-		private final MessageDigest digest;
-
-		protected DownloadInputStream(InputStream in, LongAdder counter, MessageDigest digest) {
-			super(in);
-			this.counter = counter;
-			this.digest = digest;
-		}
-
-		@Override
-		public int read(byte[] b, int off, int len) throws IOException {
-			int n = super.read(b, off, len);
-			digest.update(b, off, n);
-			counter.add(n);
-			return n;
-		}
-
-		@Override
-		public int read() throws IOException {
-			int b = super.read();
-			if (b != -1) {
-				digest.update((byte) b);
-				counter.increment();
-			}
-			return b;
-		}
-
-	}
-
-}

+ 32 - 14
src/main/java/org/cryptomator/updater/MacOsDmgUpdateMechanism.java

@@ -3,6 +3,9 @@ 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.UpdateFailedException;
+import org.cryptomator.integrations.update.UpdateProcess;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -24,9 +27,13 @@ public class MacOsDmgUpdateMechanism extends DownloadUpdateMechanism {
 	private static final Logger LOG = LoggerFactory.getLogger(MacOsDmgUpdateMechanism.class);
 
 	@Override
-	public UpdateProcess prepareUpdate() throws IOException {
-		Path workDir = Files.createTempDirectory("cryptomator-update");
-		return new UpdateProcessImpl(workDir);
+	public UpdateProcess prepareUpdate() throws UpdateFailedException {
+		try {
+			Path workDir = Files.createTempDirectory("cryptomator-update");
+			return new UpdateProcessImpl(workDir);
+		} catch (IOException e) {
+			throw new UpdateFailedException("Failed to create temporary directory for update", e);
+		}
 	}
 
 	private static class UpdateProcessImpl extends DownloadUpdateProcess {
@@ -35,12 +42,12 @@ public class MacOsDmgUpdateMechanism extends DownloadUpdateMechanism {
 		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");
 
-		public UpdateProcessImpl(Path workDir) {
-			super(workDir, UPDATE_URI, CHECKSUM,60_000_000L); // initially assume 60 MB for the update size
+		public UpdateProcessImpl(Path downloadPath) {
+			super(downloadPath, "update.dmg", UPDATE_URI, CHECKSUM,60_000_000L); // initially assume 60 MB for the update size
 		}
 
 		@Override
-		protected void postDownload(Path downloadedFile) throws IOException {
+		protected void postDownload(Path downloadPath) throws IOException {
 			// Extract Cryptomator.app from the .dmg file
 			String script = """
 					hdiutil attach 'update.dmg' -mountpoint "/Volumes/Cryptomator_${MOUNT_ID}" -nobrowse -quiet &&
@@ -57,29 +64,40 @@ public class MacOsDmgUpdateMechanism extends DownloadUpdateMechanism {
 					LOG.error("Failed to extract DMG, exit code: {}, output: {}", p.exitValue(), new String(p.getErrorStream().readAllBytes()));
 					throw new IOException("Failed to extract DMG, exit code: " + p.exitValue());
 				}
+				LOG.debug("Update ready: {}", workDir.resolve("Cryptomator.app"));
 			} catch (InterruptedException e) {
 				throw new InterruptedIOException("Failed to extract DMG, interrupted");
 			}
 		}
 
 		@Override
-		public Process applyUpdate() throws IllegalStateException, IOException {
-			if (downloadException != null) {
-				throw new IllegalStateException("Downloading update failed", downloadException);
-			} else if (!downloadSuccessful) {
+		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);
+			}
+
+			String selfPath = ProcessHandle.current().info().command().orElse("");
+			String installPath;
+			if (selfPath.startsWith("/Applications/Cryptomator.app")) {
+				installPath = "/Applications/Cryptomator.app";
+			} 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);
 			}
-			// TODO: use /Applications/Cryptomator.app or ~/Applications/Cryptomator.app depending on the path of the current process (ProcessHandle.current().info().command()?)
 			String script = """
 					while kill -0 ${CRYPTOMATOR_PID} 2> /dev/null; do sleep 0.5; done;
-					cp -R 'Cryptomator.app' '/Applications/Cryptomator.app';
-					open -a '/Applications/Cryptomator.app'
+					cp -R '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 &");
 			var processBuilder = new ProcessBuilder(command);
 			processBuilder.directory(workDir.toFile());
 			processBuilder.environment().put("CRYPTOMATOR_PID", String.valueOf(ProcessHandle.current().pid()));
-			return processBuilder.start();
+			processBuilder.environment().put("CRYPTOMATOR_INSTALL_PATH", installPath);
+			return processBuilder.start().toHandle();
 		}
 	}
 

+ 0 - 29
src/main/java/org/cryptomator/updater/UpdateMechanism.java

@@ -1,29 +0,0 @@
-package org.cryptomator.updater;
-
-import org.cryptomator.integrations.common.NamedServiceProvider;
-import org.jetbrains.annotations.Blocking;
-
-import javafx.concurrent.Task;
-import java.io.IOException;
-
-public interface UpdateMechanism extends NamedServiceProvider {
-
-	static UpdateMechanism get() {
-		return new MacOsDmgUpdateMechanism(); // TODO: IntegrationsLoader.load(UpdateMechanism.class).orElseThrow();
-	}
-
-	/**
-	 * Checks whether an update is available.
-	 * @return <code>true</code> if an update is available, <code>false</code> otherwise.
-	 */
-	@Blocking
-	boolean isUpdateAvailable();
-
-	/**
-	 * Performs as much as possible to prepare the update. This may include downloading the update, checking signatures, etc.
-	 * @return a new {@link Task} that can be used to monitor the progress of the update preparation. The task will complete when the preparation is done.
-	 * @throws IOException I/O error during preparation, such as network issues or file access problems.
-	 */
-	UpdateProcess prepareUpdate() throws IOException; // TODO: exception types?
-
-}

+ 0 - 51
src/main/java/org/cryptomator/updater/UpdateProcess.java

@@ -1,51 +0,0 @@
-package org.cryptomator.updater;
-
-import java.io.IOException;
-import java.util.concurrent.TimeUnit;
-
-public interface UpdateProcess {
-
-	/**
-	 * A thread-safe method to check the progress of the update preparation.
-	 * @return a value between 0.0 and 1.0 indicating the progress of the update preparation.
-	 */
-	double preparationProgress();
-
-	/**
-	 * Cancels the update process and cleans up any resources that were used during the preparation.
-	 */
-	void cancel();
-
-	/**
-	 * Blocks the current thread until the update preparation is complete or an error occurs.
-	 * <p>
-	 * If the preparation is already complete, this method returns immediately.
-	 *
-	 * @throws InterruptedException if the current thread is interrupted while waiting.
-	 */
-	void await() throws InterruptedException;
-
-	/**
-	 * Blocks the current thread until the update preparation is complete or an error occurs, or until the specified timeout expires.
-	 * <p>
-	 * If the preparation is already complete, this method returns immediately.
-	 *
-	 * @param timeout the maximum time to wait
-	 * @param unit the time unit of the {@code timeout} argument
-	 * @return true if the update is prepared
-	 */
-	boolean await(long timeout, TimeUnit unit) throws InterruptedException;
-
-	/**
-	 * Once the update preparation is complete, this method can be called to launch the external update process.
-	 * <p>
-	 * This method shall be called after making sure that the application is ready to be restarted, e.g. after locking all vaults.
-	 *
-	 * @return a {@link Process} that represents the external update process.
-	 * @throws IllegalStateException if the update preparation is not complete or if the update process cannot be launched.
-	 * @throws IOException if starting the update process fails
-	 */
-	Process applyUpdate() throws IllegalStateException, IOException;
-
-
-}

+ 0 - 25
src/test/java/org/cryptomator/updater/MacOsDmgUpdateMechanismTest.java

@@ -1,25 +0,0 @@
-package org.cryptomator.updater;
-
-import java.io.IOException;
-import java.util.concurrent.TimeUnit;
-
-class MacOsDmgUpdateMechanismTest {
-
-	public static void main(String args[]) throws InterruptedException, IOException {
-		UpdateMechanism updateMechanism = new MacOsDmgUpdateMechanism();
-		if (updateMechanism.isUpdateAvailable()) {
-			System.out.println("Update is available.");
-		}
-		var updateProcess = updateMechanism.prepareUpdate();
-		do {
-			double percentage = updateProcess.preparationProgress() * 100.0;
-			System.out.printf("\rPreparing update: %.2f%%", percentage);
-		} while (!updateProcess.await(100, TimeUnit.MILLISECONDS));
-		System.out.println("\nUpdate ready...");
-		Process p = updateProcess.applyUpdate();
-		p.isAlive();
-		System.out.println("Update running, exiting...");
-		// exit.
-	}
-
-}