Skip to content

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
wants to merge 12 commits into
base: develop
Choose a base branch
from
3 changes: 3 additions & 0 deletions src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.cryptomator.integrations.keychain.KeychainAccessProvider;
import org.cryptomator.integrations.tray.TrayIntegrationProvider;
import org.cryptomator.integrations.uiappearance.UiAppearanceProvider;
import org.cryptomator.integrations.update.UpdateService;


module org.cryptomator.integrations.api {
Expand All @@ -20,6 +21,7 @@
exports org.cryptomator.integrations.tray;
exports org.cryptomator.integrations.uiappearance;
exports org.cryptomator.integrations.quickaccess;
exports org.cryptomator.integrations.update;

uses AutoStartProvider;
uses KeychainAccessProvider;
Expand All @@ -29,4 +31,5 @@
uses TrayMenuController;
uses UiAppearanceProvider;
uses QuickAccessService;
uses UpdateService;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.cryptomator.integrations.common;

import org.jetbrains.annotations.ApiStatus;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Repeatable(DistributionChannel.DistributionChannels.class)
@ApiStatus.Experimental
public @interface DistributionChannel {
Value value() default Value.UNKNOWN;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@interface DistributionChannels {
DistributionChannel[] value();
}

enum Value {
LINUX_APPIMAGE,
LINUX_AUR,
LINUX_FLATPAK,
LINUX_NIXOS,
LINUX_PPA,
MAC_BREW,
MAC_DMG,
WINDOWS_EXE,
WINDOWS_MSI,
WINDOWS_WINGET,
UNKNOWN;

}
}
32 changes: 32 additions & 0 deletions src/main/java/org/cryptomator/integrations/update/Progress.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.cryptomator.integrations.update;

public class Progress {
private final long nOps;
private final long oP;
private final long status;
private final long progress;
private final String error;
private final String errorMessage;

public Progress(long nOps, long oP, long status, long progress, String error, String errorMessage) {
this.nOps = nOps;
this.oP = oP;
this.status = status;
this.progress = progress;
this.error = error;
this.errorMessage = errorMessage;
}

public long getOP() { return oP; }
public long getNOps() {
return nOps;
}
public long getStatus() {
return status;
}
public long getProgress() {
return progress;
}
public String getError() { return error; }
public String getErrorMessage() { return errorMessage; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.cryptomator.integrations.update;

@FunctionalInterface
public interface ProgressListener {
void onProgress(Progress progress);
}
14 changes: 14 additions & 0 deletions src/main/java/org/cryptomator/integrations/update/SpawnExited.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.cryptomator.integrations.update;

public class SpawnExited {
private final long pid;
private final long exitStatus;

public SpawnExited(long pid, long exitStatus) {
this.pid = pid;
this.exitStatus = exitStatus;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add input validation for PID values.

Consider adding validation to ensure the PID value is non-negative. Note that exitStatus can legitimately be negative (e.g., Unix signal codes), so validation should only apply to pid.

 public SpawnExited(long pid, long exitStatus) {
+	if (pid < 0) {
+		throw new IllegalArgumentException("pid must be non-negative");
+	}
 	this.pid = pid;
 	this.exitStatus = exitStatus;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public SpawnExited(long pid, long exitStatus) {
this.pid = pid;
this.exitStatus = exitStatus;
}
public SpawnExited(long pid, long exitStatus) {
+ if (pid < 0) {
+ throw new IllegalArgumentException("pid must be non-negative");
+ }
this.pid = pid;
this.exitStatus = exitStatus;
}
🤖 Prompt for AI Agents
In src/main/java/org/cryptomator/integrations/update/SpawnExited.java around
lines 7 to 10, add input validation to ensure the pid parameter is non-negative.
Modify the constructor to check if pid is less than zero and throw an
IllegalArgumentException if so, while leaving exitStatus unchanged since it can
be negative.


public long getPid() { return pid; }
public long getExitStatus() { return exitStatus; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.cryptomator.integrations.update;

@FunctionalInterface
public interface SpawnExitedListener {
void onSpawnExited(SpawnExited spawnExited);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.cryptomator.integrations.update;

public class SpawnStarted {
private final long pid;
private final long relPid;

public SpawnStarted(long pid, long relPid) {
this.pid = pid;
this.relPid = relPid;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add input validation for PID values.

Consider adding validation to ensure PID values are non-negative, as negative process IDs are typically invalid.

 public SpawnStarted(long pid, long relPid) {
+	if (pid < 0) {
+		throw new IllegalArgumentException("pid must be non-negative");
+	}
+	if (relPid < 0) {
+		throw new IllegalArgumentException("relPid must be non-negative");
+	}
 	this.pid = pid;
 	this.relPid = relPid;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public SpawnStarted(long pid, long relPid) {
this.pid = pid;
this.relPid = relPid;
}
public SpawnStarted(long pid, long relPid) {
if (pid < 0) {
throw new IllegalArgumentException("pid must be non-negative");
}
if (relPid < 0) {
throw new IllegalArgumentException("relPid must be non-negative");
}
this.pid = pid;
this.relPid = relPid;
}
🤖 Prompt for AI Agents
In src/main/java/org/cryptomator/integrations/update/SpawnStarted.java around
lines 7 to 10, the constructor accepts PID values without validation. Add checks
to ensure both pid and relPid are non-negative, throwing an
IllegalArgumentException if either is negative, to prevent invalid process ID
values from being set.


public long getPid() { return pid; }
public long getRelPid() { return relPid; }
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add validation and comprehensive documentation.

The immutable design is excellent, but the class needs parameter validation and documentation to clarify the purpose of each field.

+/**
+ * Represents the start of a spawned process during an update operation.
+ */
 public class SpawnStarted {
 	private final long pid;
 	private final long relPid;
 
+	/**
+	 * Creates a new spawn started event.
+	 * 
+	 * @param pid the process ID, must be positive
+	 * @param relPid the relative process ID or parent process ID, must be positive
+	 * @throws IllegalArgumentException if any PID is not positive
+	 */
 	public SpawnStarted(long pid, long relPid) {
+		if (pid <= 0) {
+			throw new IllegalArgumentException("PID must be positive, got: " + pid);
+		}
+		if (relPid <= 0) {
+			throw new IllegalArgumentException("Relative PID must be positive, got: " + relPid);
+		}
 		this.pid = pid;
 		this.relPid = relPid;
 	}
 
+	/**
+	 * @return the process ID
+	 */
 	public long getPid() {  return pid; }
+	
+	/**
+	 * @return the relative process ID
+	 */
 	public long getRelPid() { return relPid; }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public class SpawnStarted {
private final long pid;
private final long relPid;
public SpawnStarted(long pid, long relPid) {
this.pid = pid;
this.relPid = relPid;
}
public long getPid() { return pid; }
public long getRelPid() { return relPid; }
}
/**
* Represents the start of a spawned process during an update operation.
*/
public class SpawnStarted {
private final long pid;
private final long relPid;
/**
* Creates a new spawn started event.
*
* @param pid the process ID, must be positive
* @param relPid the relative process ID or parent process ID, must be positive
* @throws IllegalArgumentException if any PID is not positive
*/
public SpawnStarted(long pid, long relPid) {
if (pid <= 0) {
throw new IllegalArgumentException("PID must be positive, got: " + pid);
}
if (relPid <= 0) {
throw new IllegalArgumentException("Relative PID must be positive, got: " + relPid);
}
this.pid = pid;
this.relPid = relPid;
}
/**
* @return the process ID
*/
public long getPid() { return pid; }
/**
* @return the relative process ID
*/
public long getRelPid() { return relPid; }
}
🤖 Prompt for AI Agents
In src/main/java/org/cryptomator/integrations/update/SpawnStarted.java around
lines 3 to 14, add validation to the constructor parameters to ensure pid and
relPid are positive values or meet expected constraints. Also, add JavaDoc
comments to the class and its fields/methods to clearly describe the purpose and
meaning of pid and relPid, explaining their roles and usage in the context of
the class.

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.cryptomator.integrations.update;

@FunctionalInterface
public interface SpawnStartedListener {
void onSpawnStarted(SpawnStarted spawnStarted);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add parameter validation and documentation.

The functional interface is well-designed, but consider adding parameter validation and JavaDoc documentation for this public API.

+/**
+ * Functional interface for listening to spawn start events during update operations.
+ */
 @FunctionalInterface
 public interface SpawnStartedListener {
-	void onSpawnStarted(SpawnStarted spawnStarted);
+	/**
+	 * Called when a new process is spawned during an update operation.
+	 * 
+	 * @param spawnStarted the spawn start event details, must not be null
+	 */
+	void onSpawnStarted(SpawnStarted spawnStarted);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@FunctionalInterface
public interface SpawnStartedListener {
void onSpawnStarted(SpawnStarted spawnStarted);
}
/**
* Functional interface for listening to spawn start events during update operations.
*/
@FunctionalInterface
public interface SpawnStartedListener {
/**
* Called when a new process is spawned during an update operation.
*
* @param spawnStarted the spawn start event details, must not be null
*/
void onSpawnStarted(SpawnStarted spawnStarted);
}
🤖 Prompt for AI Agents
In src/main/java/org/cryptomator/integrations/update/SpawnStartedListener.java
at lines 3 to 6, add JavaDoc comments to describe the interface and its method
clearly. Also, include parameter validation in the onSpawnStarted method
implementation to check for null values and handle them appropriately, ensuring
robustness of this public API.

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.cryptomator.integrations.update;

public class UpdateAvailable {
private final String runningCommit;
private final String localCommit;
private final String remoteCommit;

public UpdateAvailable(String runningCommit, String localCommit, String remoteCommit) {
this.runningCommit = runningCommit;
this.localCommit = localCommit;
this.remoteCommit = remoteCommit;
}

public String getRunningCommit() {
return runningCommit;
}
public String getLocalCommit() {
return localCommit;
}
public String getRemoteCommit() {
return remoteCommit;
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add null validation and comprehensive documentation.

The immutable design is excellent, but the class needs null validation and documentation to clarify the purpose of each commit field.

+import java.util.Objects;
+
+/**
+ * Represents information about available software updates with commit identifiers.
+ */
 public class UpdateAvailable {
 	private final String runningCommit;
 	private final String localCommit;
 	private final String remoteCommit;
 
+	/**
+	 * Creates a new update available event.
+	 * 
+	 * @param runningCommit the commit hash of the currently running version, must not be null
+	 * @param localCommit the commit hash of the local repository, must not be null
+	 * @param remoteCommit the commit hash of the remote repository, must not be null
+	 * @throws NullPointerException if any parameter is null
+	 */
 	public UpdateAvailable(String runningCommit, String localCommit, String remoteCommit) {
-		this.runningCommit = runningCommit;
-		this.localCommit = localCommit;
-		this.remoteCommit = remoteCommit;
+		this.runningCommit = Objects.requireNonNull(runningCommit, "runningCommit must not be null");
+		this.localCommit = Objects.requireNonNull(localCommit, "localCommit must not be null");
+		this.remoteCommit = Objects.requireNonNull(remoteCommit, "remoteCommit must not be null");
 	}
 
+	/**
+	 * @return the commit hash of the currently running version
+	 */
 	public String getRunningCommit() {
 		return runningCommit;
 	}
+	
+	/**
+	 * @return the commit hash of the local repository
+	 */
 	public String getLocalCommit() {
 		return localCommit;
 	}
+	
+	/**
+	 * @return the commit hash of the remote repository
+	 */
 	public String getRemoteCommit() {
 		return remoteCommit;
 	}
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public class UpdateAvailable {
private final String runningCommit;
private final String localCommit;
private final String remoteCommit;
public UpdateAvailable(String runningCommit, String localCommit, String remoteCommit) {
this.runningCommit = runningCommit;
this.localCommit = localCommit;
this.remoteCommit = remoteCommit;
}
public String getRunningCommit() {
return runningCommit;
}
public String getLocalCommit() {
return localCommit;
}
public String getRemoteCommit() {
return remoteCommit;
}
}
import java.util.Objects;
/**
* Represents information about available software updates with commit identifiers.
*/
public class UpdateAvailable {
private final String runningCommit;
private final String localCommit;
private final String remoteCommit;
/**
* Creates a new update available event.
*
* @param runningCommit the commit hash of the currently running version, must not be null
* @param localCommit the commit hash of the local repository, must not be null
* @param remoteCommit the commit hash of the remote repository, must not be null
* @throws NullPointerException if any parameter is null
*/
public UpdateAvailable(String runningCommit, String localCommit, String remoteCommit) {
this.runningCommit = Objects.requireNonNull(runningCommit, "runningCommit must not be null");
this.localCommit = Objects.requireNonNull(localCommit, "localCommit must not be null");
this.remoteCommit = Objects.requireNonNull(remoteCommit, "remoteCommit must not be null");
}
/**
* @return the commit hash of the currently running version
*/
public String getRunningCommit() {
return runningCommit;
}
/**
* @return the commit hash of the local repository
*/
public String getLocalCommit() {
return localCommit;
}
/**
* @return the commit hash of the remote repository
*/
public String getRemoteCommit() {
return remoteCommit;
}
}
🤖 Prompt for AI Agents
In src/main/java/org/cryptomator/integrations/update/UpdateAvailable.java around
lines 3 to 23, add null checks in the constructor to validate that
runningCommit, localCommit, and remoteCommit are not null, throwing
IllegalArgumentException if any are null. Additionally, add JavaDoc comments to
the class and its constructor and getter methods to clearly describe the purpose
of each commit field and the overall class functionality.

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.cryptomator.integrations.update;

@FunctionalInterface
public interface UpdateAvailableListener {
void onUpdateAvailable(UpdateAvailable updateAvailable);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.cryptomator.integrations.update;

public class UpdateFailedException extends Exception {

public UpdateFailedException(String message) {
super(message);
}

public UpdateFailedException(String message, Throwable cause) {
super(message, cause);
}
}
119 changes: 119 additions & 0 deletions src/main/java/org/cryptomator/integrations/update/UpdateService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package org.cryptomator.integrations.update;

import org.cryptomator.integrations.common.DistributionChannel;
import org.cryptomator.integrations.common.IntegrationsLoader;

import java.util.stream.Stream;

/**
* This is the interface used by Cryptomator to provide a way to update Cryptomator in a convinient way.
* It's idependent of the supported platforms and package distribution channels.
*/
public interface UpdateService {

static Stream<UpdateService> get() {
return IntegrationsLoader.loadAll(UpdateService.class).filter(UpdateService::isSupported);
}

/**
* @return <code>true</code> if this UppdateService can update the app.
* @implSpec This method must not throw any exceptions and should fail fast
* returning <code>false</code> if it's not possible to use this UppdateService
*/
boolean isSupported();

/**
* Retrieve an object to check for the latest release published on the given channel.
*
* @param channel The DistributionChannel.Value to check.
* @return An object that is capable of checking asynchronously for the latest release.
*/
Object getLatestReleaseChecker(DistributionChannel.Value channel);

/**
* Trigger updating the app.
*
* @throws UpdateFailedException If the udpate wasn't successful or was cancelled.
*/
void triggerUpdate() throws UpdateFailedException;

/**
* Start a new instance of the application.
*
* @return The PID of the new process.
*/
long spawnApp();

/**
* A flag indicating whether elevated permissions or sudo is required during update
* (so the user can be prepared for a corresponding prompt)
*
* @return <code>true</code> if elevated permissions are required, <code>false</code> otherwise.
*/
boolean doesRequireElevatedPermissions();

/**
* Get a meaningful description of the update available to display it in the app
* like "Update via apt"
*
* @return The text to describes the update.
*/
String getDisplayName();

/**
* Register a listener to receive update available events.
*
* @param listener The listener to register.
*/
void addUpdateAvailableListener(UpdateAvailableListener listener);

/**
* Unregister a previously registered update available listener.
*
* @param listener The listener to unregister.
*/
void removeUpdateAvailableListener(UpdateAvailableListener listener);

/**
* Register a listener to receive update progress events.
*
* @param listener The listener to register.
*/
void addProgressListener(ProgressListener listener);

/**
* Unregister a previously registered update progress listener.
*
* @param listener The listener to unregister.
*/
void removeProgressListener(ProgressListener listener);

/**
* Register a listener to receive an event containing the pid of a spawned process.
*
* @param listener The listener to register.
*/
void addSpawnStartedListener(SpawnStartedListener listener);

/**
* Unregister a previously registered spawned process listener.
*
* @param listener The listener to unregister.
*/
void removeSpawnStartedListener(SpawnStartedListener listener);

/**
* Register a listener to receive an event containing the pid
* and exit status of a process that exits.
*
* @param listener The listener to register.
*/
void addSpawnExitedListener(SpawnExitedListener listener);

/**
* Unregister a previously registered process exits listener.
*
* @param listener The listener to unregister.
*/
void removeSpawnExitedListener(SpawnExitedListener listener);
}