Skip to content

Commit 30075b1

Browse files
Merge pull request #117 from purejava/feature/flatpak-update-portal
Flatpak Update Mechanism
2 parents 69e58cb + f03c9f1 commit 30075b1

5 files changed

Lines changed: 279 additions & 5 deletions

File tree

pom.xml

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,12 @@
3939
<project.jdk.version>25</project.jdk.version>
4040

4141
<!-- runtime dependencies -->
42-
43-
<api.version>1.8.0-SNAPSHOT</api.version>
42+
<api.version>1.8.0-beta1</api.version>
43+
<slf4j.version>2.0.17</slf4j.version>
44+
<jackson.version>2.20.0</jackson.version>
4445
<secret-service.version>2.0.1-alpha</secret-service.version>
4546
<kdewallet.version>1.4.0</kdewallet.version>
46-
<slf4j.version>2.0.17</slf4j.version>
47+
<flatpakupdateportal.version>1.1.0</flatpakupdateportal.version>
4748
<appindicator.version>1.4.2</appindicator.version>
4849

4950
<!-- test dependencies -->
@@ -86,6 +87,12 @@
8687
<artifactId>slf4j-api</artifactId>
8788
<version>${slf4j.version}</version>
8889
</dependency>
90+
<dependency>
91+
<groupId>com.fasterxml.jackson.core</groupId>
92+
<artifactId>jackson-databind</artifactId>
93+
<version>${jackson.version}</version>
94+
</dependency>
95+
8996
<dependency>
9097
<groupId>de.swiesend</groupId>
9198
<artifactId>secret-service</artifactId>
@@ -102,6 +109,11 @@
102109
<artifactId>libappindicator-gtk3-java-minimal</artifactId>
103110
<version>${appindicator.version}</version>
104111
</dependency>
112+
<dependency>
113+
<groupId>org.purejava</groupId>
114+
<artifactId>flatpak-update-portal</artifactId>
115+
<version>${flatpakupdateportal.version}</version>
116+
</dependency>
105117
<dependency>
106118
<groupId>org.junit.jupiter</groupId>
107119
<artifactId>junit-jupiter</artifactId>

src/main/java/module-info.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,37 @@
33
import org.cryptomator.integrations.quickaccess.QuickAccessService;
44
import org.cryptomator.integrations.revealpath.RevealPathService;
55
import org.cryptomator.integrations.tray.TrayMenuController;
6+
import org.cryptomator.integrations.update.UpdateMechanism;
67
import org.cryptomator.linux.autostart.FreedesktopAutoStartService;
7-
import org.cryptomator.linux.keychain.KDEWalletKeychainAccess;
88
import org.cryptomator.linux.keychain.GnomeKeyringKeychainAccess;
9+
import org.cryptomator.linux.keychain.KDEWalletKeychainAccess;
910
import org.cryptomator.linux.quickaccess.DolphinPlaces;
1011
import org.cryptomator.linux.quickaccess.NautilusBookmarks;
1112
import org.cryptomator.linux.revealpath.DBusSendRevealPathService;
1213
import org.cryptomator.linux.tray.AppindicatorTrayMenuController;
14+
import org.cryptomator.linux.update.FlatpakUpdater;
1315

1416
module org.cryptomator.integrations.linux {
1517
requires org.cryptomator.integrations.api;
1618
requires org.slf4j;
1719
requires org.freedesktop.dbus;
1820
requires org.purejava.appindicator;
1921
requires org.purejava.kwallet;
22+
requires org.purejava.portal;
2023
requires de.swiesend.secretservice;
2124
requires java.xml;
25+
requires java.net.http;
26+
requires com.fasterxml.jackson.databind;
2227

2328
provides AutoStartProvider with FreedesktopAutoStartService;
2429
provides KeychainAccessProvider with GnomeKeyringKeychainAccess, KDEWalletKeychainAccess;
2530
provides RevealPathService with DBusSendRevealPathService;
2631
provides TrayMenuController with AppindicatorTrayMenuController;
2732
provides QuickAccessService with NautilusBookmarks, DolphinPlaces;
33+
provides UpdateMechanism with FlatpakUpdater;
2834

2935
opens org.cryptomator.linux.tray to org.cryptomator.integrations.api;
3036
opens org.cryptomator.linux.quickaccess to org.cryptomator.integrations.api;
3137
opens org.cryptomator.linux.autostart to org.cryptomator.integrations.api;
32-
}
38+
opens org.cryptomator.linux.update to org.cryptomator.integrations.api, com.fasterxml.jackson.databind;
39+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package org.cryptomator.linux.update;
2+
3+
import org.cryptomator.integrations.update.UpdateInfo;
4+
import org.cryptomator.integrations.update.UpdateMechanism;
5+
6+
public record FlatpakUpdateInfo(String version, UpdateMechanism<FlatpakUpdateInfo> updateMechanism) implements UpdateInfo<FlatpakUpdateInfo> {
7+
}
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
package org.cryptomator.linux.update;
2+
3+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4+
import com.fasterxml.jackson.annotation.JsonProperty;
5+
import com.fasterxml.jackson.databind.ObjectMapper;
6+
import org.cryptomator.integrations.common.CheckAvailability;
7+
import org.cryptomator.integrations.common.DisplayName;
8+
import org.cryptomator.integrations.common.OperatingSystem;
9+
import org.cryptomator.integrations.update.UpdateFailedException;
10+
import org.cryptomator.integrations.update.UpdateMechanism;
11+
import org.cryptomator.integrations.update.UpdateStep;
12+
import org.freedesktop.dbus.FileDescriptor;
13+
import org.freedesktop.dbus.exceptions.DBusException;
14+
import org.freedesktop.dbus.types.UInt32;
15+
import org.freedesktop.dbus.types.Variant;
16+
import org.purejava.portal.Flatpak;
17+
import org.purejava.portal.FlatpakSpawnFlag;
18+
import org.purejava.portal.UpdatePortal;
19+
import org.purejava.portal.Util;
20+
import org.slf4j.Logger;
21+
import org.slf4j.LoggerFactory;
22+
23+
import java.io.IOException;
24+
import java.net.URI;
25+
import java.net.http.HttpClient;
26+
import java.net.http.HttpRequest;
27+
import java.net.http.HttpResponse;
28+
import java.util.Collections;
29+
import java.util.Comparator;
30+
import java.util.List;
31+
import java.util.Map;
32+
import java.util.concurrent.CountDownLatch;
33+
import java.util.concurrent.TimeUnit;
34+
35+
@CheckAvailability
36+
@DisplayName("Update via Flatpak update")
37+
@OperatingSystem(OperatingSystem.Value.LINUX)
38+
public class FlatpakUpdater implements UpdateMechanism<FlatpakUpdateInfo> {
39+
40+
private static final Logger LOG = LoggerFactory.getLogger(FlatpakUpdater.class);
41+
private static final String FLATHUB_API_BASE_URL = "https://flathub.org/api/v2/appstream/";
42+
private static final String APP_NAME = "org.cryptomator.Cryptomator";
43+
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
44+
45+
private final UpdatePortal portal;
46+
47+
public FlatpakUpdater() {
48+
this.portal = new UpdatePortal();
49+
portal.CreateUpdateMonitor(UpdatePortal.OPTIONS_DUMMY);
50+
}
51+
52+
@CheckAvailability
53+
public boolean isSupported() {
54+
return portal.isAvailable();
55+
}
56+
57+
@Override
58+
public FlatpakUpdateInfo checkForUpdate(String currentVersion, HttpClient httpClient) throws UpdateFailedException {
59+
var uri = URI.create(FLATHUB_API_BASE_URL + APP_NAME);
60+
var request = HttpRequest.newBuilder(uri).GET().build();
61+
try {
62+
var response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
63+
if (response.statusCode() != 200) {
64+
LOG.warn("GET {} resulted in status {}", uri, response.statusCode());
65+
return null;
66+
} else {
67+
var appstream = OBJECT_MAPPER.reader().readValue(response.body(), AppstreamResponse.class);
68+
var updateVersion = appstream.releases().stream()
69+
.filter(release -> "stable".equalsIgnoreCase(release.type))
70+
.max(Comparator.comparing(AppstreamReleases::timestamp)) // we're interested in the newest stable release
71+
.map(AppstreamReleases::version)
72+
.orElse("0.0.0"); // fallback should always be smaller than current version
73+
74+
75+
// FIXME: remove this block! see https://github.com/cryptomator/cryptomator/issues/4058
76+
if (currentVersion.startsWith("1.18.0-beta")) {
77+
return new FlatpakUpdateInfo(updateVersion, this);
78+
}
79+
// END FIXME
80+
81+
82+
if (UpdateMechanism.isUpdateAvailable(updateVersion, currentVersion)) {
83+
return new FlatpakUpdateInfo(updateVersion, this);
84+
} else {
85+
return null;
86+
}
87+
}
88+
} catch (IOException e) {
89+
throw new UpdateFailedException("Check for updates failed.", e);
90+
} catch (InterruptedException e) {
91+
Thread.currentThread().interrupt();
92+
LOG.warn("Update check interrupted", e);
93+
return null;
94+
}
95+
}
96+
97+
@JsonIgnoreProperties(ignoreUnknown = true)
98+
public record AppstreamResponse(
99+
@JsonProperty("releases") List<AppstreamReleases> releases
100+
) {}
101+
102+
@JsonIgnoreProperties(ignoreUnknown = true)
103+
public record AppstreamReleases(
104+
@JsonProperty("timestamp") long timestamp,
105+
@JsonProperty("version") String version,
106+
@JsonProperty("type") String type
107+
) {}
108+
109+
@Override
110+
public UpdateStep firstStep(FlatpakUpdateInfo updateInfo) throws UpdateFailedException {
111+
var monitorPath = portal.CreateUpdateMonitor(UpdatePortal.OPTIONS_DUMMY);
112+
if (monitorPath == null) {
113+
throw new UpdateFailedException("Failed to create UpdateMonitor on DBus");
114+
}
115+
116+
return new FlatpakUpdateStep(portal.getUpdateMonitor(monitorPath.toString()));
117+
}
118+
119+
private class FlatpakUpdateStep implements UpdateStep {
120+
121+
private final CountDownLatch latch = new CountDownLatch(1);
122+
private final Flatpak.UpdateMonitor monitor;
123+
private volatile double progress = 0.0;
124+
private volatile UpdateFailedException error;
125+
private AutoCloseable signalHandler;
126+
127+
private FlatpakUpdateStep(Flatpak.UpdateMonitor monitor) {
128+
this.monitor = monitor;
129+
}
130+
131+
@Override
132+
public String description() {
133+
return "Updating via Flatpak... %1.0f%%".formatted(preparationProgress() * 100);
134+
}
135+
136+
@Override
137+
public void start() {
138+
try {
139+
this.signalHandler = portal.getDBusConnection().addSigHandler(Flatpak.UpdateMonitor.Progress.class, this::handleProgressSignal);
140+
} catch (DBusException e) {
141+
LOG.error("DBus error", e);
142+
latch.countDown();
143+
}
144+
portal.updateApp("x11:0", monitor, UpdatePortal.OPTIONS_DUMMY);
145+
}
146+
147+
private void handleProgressSignal(Flatpak.UpdateMonitor.Progress signal) {
148+
int status = ((UInt32) signal.info.get("status").getValue()).intValue();
149+
switch (status) {
150+
case 0 -> { // In progress
151+
Variant<?> progressVariant = signal.info.get("progress");
152+
if (progressVariant != null) {
153+
progress = ((UInt32) progressVariant.getValue()).doubleValue() / 100.0; // progress reported as int in range [0, 100]
154+
}
155+
}
156+
case 1 -> { // No update available
157+
error = new UpdateFailedException("No update available");
158+
latch.countDown();
159+
}
160+
case 2 -> { // Update complete
161+
progress = 1.0;
162+
latch.countDown();
163+
}
164+
case 3 -> { // Update failed
165+
error = new UpdateFailedException("Update preparation failed");
166+
latch.countDown();
167+
}
168+
default -> {
169+
error = new UpdateFailedException("Unknown update status " + status);
170+
latch.countDown();
171+
}
172+
}
173+
}
174+
175+
private void stopReceivingSignals() {
176+
if (signalHandler != null) {
177+
try {
178+
signalHandler.close();
179+
} catch (Exception e) {
180+
LOG.error("Failed to close signal handler", e);
181+
}
182+
signalHandler = null;
183+
}
184+
}
185+
186+
@Override
187+
public double preparationProgress() {
188+
return progress;
189+
}
190+
191+
@Override
192+
public void cancel() {
193+
portal.cancelUpdateMonitor(monitor);
194+
stopReceivingSignals();
195+
portal.close(); // TODO: is this right? belongs to parent class. update can not be retried afterwards. or should each process have its own portal instance?
196+
error = new UpdateFailedException("Update cancelled by user");
197+
}
198+
199+
@Override
200+
public void await() throws InterruptedException {
201+
latch.await();
202+
}
203+
204+
@Override
205+
public boolean await(long timeout, TimeUnit unit) throws InterruptedException {
206+
return latch.await(timeout, unit);
207+
}
208+
209+
@Override
210+
public boolean isDone() {
211+
try {
212+
return latch.await(0, TimeUnit.MILLISECONDS);
213+
} catch (InterruptedException e) {
214+
Thread.currentThread().interrupt();
215+
return false;
216+
}
217+
}
218+
219+
@Override
220+
public UpdateStep nextStep() throws IllegalStateException, IOException {
221+
return UpdateStep.of("Restarting application", this::applyUpdate);
222+
}
223+
224+
public UpdateStep applyUpdate() throws IllegalStateException, IOException {
225+
if (!isDone()) {
226+
throw new IllegalStateException("Update preparation is not complete");
227+
}
228+
stopReceivingSignals();
229+
if (error != null) {
230+
throw error;
231+
}
232+
233+
// spawn new Cryptomator process:
234+
var cwdPath = Util.stringToByteList(System.getProperty("user.dir"));
235+
List<List<Byte>> argv = List.of(
236+
Util.stringToByteList(APP_NAME));
237+
Map<UInt32, FileDescriptor> fds = Collections.emptyMap();
238+
Map<String, String> envs = Map.of();
239+
UInt32 flags = new UInt32(FlatpakSpawnFlag.LATEST_VERSION.getValue());
240+
Map<String, Variant<?>> options = UpdatePortal.OPTIONS_DUMMY;
241+
var pid = portal.Spawn(cwdPath, argv, fds, envs, flags, options).longValue();
242+
LOG.info("Spawned updated Cryptomator process with PID {}", pid);
243+
return UpdateStep.EXIT;
244+
}
245+
}
246+
247+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
org.cryptomator.linux.update.FlatpakUpdater

0 commit comments

Comments
 (0)