-
Notifications
You must be signed in to change notification settings - Fork 5
External update mechanism #62
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
purejava
wants to merge
12
commits into
cryptomator:develop
Choose a base branch
from
purejava:update-mechanism
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 10 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
88f7231
Invent UpdateService
purejava a501a2b
Have multiple UpdateServices
purejava 6af0aa8
Invent @DistributionChannel
purejava a2a6f98
Drop isUpdateAvailable in favor of getLatestReleaseChecker
purejava f3f3c35
Add spawnApp()
purejava 5f29005
Add listener interfaces
purejava 28680db
Implement missing signals
purejava eaa63e5
Correlate with API as suggested in a separate PoC: UpdateMechanism an…
purejava 84076df
Reduce API surface
overheadhunter e875adb
Merge branch 'develop' into update-mechanism
overheadhunter dde78a1
API refinement
overheadhunter 0f765d6
added test case
overheadhunter File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
174 changes: 174 additions & 0 deletions
174
src/main/java/org/cryptomator/integrations/update/DownloadUpdateProcess.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
package org.cryptomator.integrations.update; | ||
|
||
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 String downloadFileName; | ||
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; | ||
|
||
/** | ||
* Creates a new DownloadUpdateProcess instance. | ||
* @param workDir A temporary directory where to download the update file. | ||
* @param downloadFileName The name of the file to which the update will be downloaded | ||
* @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, String downloadFileName, URI uri, byte[] checksum, long estDownloadSize) { | ||
this.workDir = workDir; | ||
this.downloadFileName = downloadFileName; | ||
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(); | ||
} | ||
|
||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
@Override | ||
public void await() throws InterruptedException { | ||
downloadCompleted.await(); | ||
} | ||
|
||
@Override | ||
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { | ||
return downloadCompleted.await(timeout, unit); | ||
} | ||
|
||
protected boolean isDone() { | ||
try { | ||
return downloadCompleted.await(0, TimeUnit.MILLISECONDS); | ||
} catch (InterruptedException e) { | ||
Thread.currentThread().interrupt(); | ||
return false; | ||
} | ||
} | ||
|
||
@Override | ||
public void cancel() { | ||
downloadThread.interrupt(); | ||
} | ||
|
||
private void download() { | ||
try { | ||
download(workDir.resolve(downloadFileName)); | ||
} 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 downloadPath The path to where to save the downloaded file. | ||
* @throws IOException indicating I/O errors during the download or file writing process or due to checksum mismatch | ||
*/ | ||
protected void download(Path downloadPath) throws IOException { | ||
var request = HttpRequest.newBuilder().uri(uri).GET().build(); | ||
try (HttpClient client = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.ALWAYS).build()) { // TODO: make http client injectable? | ||
// make download request | ||
var response = client.send(request, HttpResponse.BodyHandlers.ofInputStream()); | ||
if (response.statusCode() != 200) { | ||
overheadhunter marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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(downloadPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW)) { | ||
dst.transferFrom(src, 0, totalBytes.get()); | ||
} | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// verify checksum if provided | ||
byte[] calculatedChecksum = sha256.digest(); | ||
if (!MessageDigest.isEqual(calculatedChecksum, checksum)) { | ||
throw new IOException("Checksum verification failed for downloaded file: " + downloadPath); | ||
} | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// post-download processing | ||
postDownload(downloadPath); | ||
} catch (InterruptedException e) { | ||
throw new InterruptedIOException("Download interrupted"); | ||
} | ||
} | ||
|
||
protected void postDownload(Path downloadPath) 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; | ||
} | ||
overheadhunter marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
@Override | ||
public int read() throws IOException { | ||
int b = super.read(); | ||
if (b != -1) { | ||
digest.update((byte) b); | ||
counter.increment(); | ||
} | ||
return b; | ||
} | ||
|
||
} | ||
|
||
} |
17 changes: 17 additions & 0 deletions
17
src/main/java/org/cryptomator/integrations/update/UpdateFailedException.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package org.cryptomator.integrations.update; | ||
|
||
import org.jetbrains.annotations.ApiStatus; | ||
|
||
import java.io.IOException; | ||
|
||
@ApiStatus.Experimental | ||
public class UpdateFailedException extends IOException { | ||
|
||
public UpdateFailedException(String message) { | ||
super(message); | ||
} | ||
|
||
public UpdateFailedException(String message, Throwable cause) { | ||
super(message, cause); | ||
} | ||
} |
29 changes: 29 additions & 0 deletions
29
src/main/java/org/cryptomator/integrations/update/UpdateMechanism.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package org.cryptomator.integrations.update; | ||
|
||
import org.cryptomator.integrations.common.IntegrationsLoader; | ||
import org.cryptomator.integrations.common.NamedServiceProvider; | ||
import org.jetbrains.annotations.ApiStatus; | ||
import org.jetbrains.annotations.Blocking; | ||
|
||
@ApiStatus.Experimental | ||
public interface UpdateMechanism extends NamedServiceProvider { | ||
|
||
static UpdateMechanism get() { | ||
return IntegrationsLoader.load(UpdateMechanism.class).orElseThrow(); // Fallback "show download page" mechanism always available. | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If no implementation is found the caller gets a naked 🤖 Prompt for AI Agents
|
||
|
||
/** | ||
* Checks whether an update is available. | ||
* @return <code>true</code> if an update is available, <code>false</code> otherwise. | ||
*/ | ||
@Blocking | ||
boolean isUpdateAvailable(); // TODO: let it throw? | ||
|
||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/** | ||
* Performs as much as possible to prepare the update. This may include downloading the update, checking signatures, etc. | ||
* @return a new {@link UpdateProcess} that can be used to monitor the progress of the update preparation. The task will complete when the preparation is done. | ||
* @throws UpdateFailedException If no update process can be started, e.g. due to network or I/O issues. | ||
*/ | ||
UpdateProcess prepareUpdate() throws UpdateFailedException; | ||
|
||
} |
54 changes: 54 additions & 0 deletions
54
src/main/java/org/cryptomator/integrations/update/UpdateProcess.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
package org.cryptomator.integrations.update; | ||
|
||
import org.jetbrains.annotations.ApiStatus; | ||
|
||
import java.io.IOException; | ||
import java.util.concurrent.TimeUnit; | ||
|
||
@ApiStatus.Experimental | ||
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(); | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/** | ||
* 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 ProcessHandle} 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 the update preparation failed or starting the update process failed | ||
*/ | ||
ProcessHandle applyUpdate() throws IllegalStateException, IOException; | ||
|
||
|
||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.