|
4 | 4 | import org.cryptomator.integrations.common.DisplayName; |
5 | 5 | import org.cryptomator.integrations.common.OperatingSystem; |
6 | 6 | import org.cryptomator.integrations.common.Priority; |
7 | | -import org.cryptomator.integrations.update.Progress; |
8 | | -import org.cryptomator.integrations.update.ProgressListener; |
9 | 7 | import org.cryptomator.integrations.update.UpdateFailedException; |
10 | | -import org.cryptomator.integrations.update.UpdateService; |
| 8 | +import org.cryptomator.integrations.update.UpdateMechanism; |
| 9 | +import org.cryptomator.integrations.update.UpdateProcess; |
11 | 10 | import org.freedesktop.dbus.FileDescriptor; |
12 | 11 | import org.freedesktop.dbus.exceptions.DBusException; |
13 | 12 | import org.freedesktop.dbus.types.UInt32; |
|
16 | 15 | import org.purejava.portal.FlatpakSpawnFlag; |
17 | 16 | import org.purejava.portal.UpdatePortal; |
18 | 17 | import org.purejava.portal.Util; |
19 | | -import org.purejava.portal.rest.UpdateCheckerTask; |
20 | 18 | import org.slf4j.Logger; |
21 | 19 | import org.slf4j.LoggerFactory; |
22 | 20 |
|
| 21 | +import java.io.IOException; |
23 | 22 | import java.util.Collections; |
24 | 23 | import java.util.List; |
25 | 24 | import java.util.Map; |
26 | 25 | import java.util.concurrent.CopyOnWriteArrayList; |
| 26 | +import java.util.concurrent.CountDownLatch; |
| 27 | +import java.util.concurrent.TimeUnit; |
| 28 | +import java.util.concurrent.atomic.AtomicBoolean; |
27 | 29 |
|
28 | 30 | @Priority(1000) |
29 | 31 | @CheckAvailability |
30 | 32 | @DisplayName("Update via Flatpak update") |
31 | 33 | @OperatingSystem(OperatingSystem.Value.LINUX) |
32 | | -public class FlatpakUpdater implements UpdateService, AutoCloseable { |
| 34 | +public class FlatpakUpdater implements UpdateMechanism { |
33 | 35 |
|
34 | 36 | private static final Logger LOG = LoggerFactory.getLogger(FlatpakUpdater.class); |
35 | 37 | private static final String APP_NAME = "org.cryptomator.Cryptomator"; |
36 | 38 |
|
37 | | - private final List<ProgressListener> progressListeners = new CopyOnWriteArrayList<>(); |
38 | | - |
39 | 39 | private final UpdatePortal portal; |
40 | | - private Flatpak.UpdateMonitor updateMonitor; |
41 | 40 |
|
42 | 41 | public FlatpakUpdater() { |
43 | 42 | this.portal = new UpdatePortal(); |
44 | 43 | portal.CreateUpdateMonitor(UpdatePortal.OPTIONS_DUMMY); |
45 | 44 | } |
46 | 45 |
|
47 | | - @Override |
| 46 | + @CheckAvailability |
48 | 47 | public boolean isSupported() { |
49 | 48 | return portal.isAvailable(); |
50 | 49 | } |
51 | 50 |
|
52 | 51 | @Override |
53 | | - public UpdateCheckerTask getLatestReleaseChecker() { |
| 52 | + public boolean isUpdateAvailable() { |
| 53 | + var cdl = new CountDownLatch(1); |
54 | 54 | portal.setUpdateCheckerTaskFor(APP_NAME); |
55 | | - return portal.getUpdateCheckerTaskFor(APP_NAME); |
| 55 | + var checkTask = portal.getUpdateCheckerTaskFor(APP_NAME); |
| 56 | + var updateAvailable = new AtomicBoolean(false); |
| 57 | + checkTask.setOnSucceeded(latestVersion -> { |
| 58 | + updateAvailable.set(true); // TODO: compare version strings before setting this to true |
| 59 | + cdl.countDown(); |
| 60 | + }); |
| 61 | + checkTask.setOnFailed(error -> { |
| 62 | + LOG.warn("Error while checking for updates.", error); |
| 63 | + cdl.countDown(); |
| 64 | + }); |
| 65 | + try { |
| 66 | + cdl.await(); |
| 67 | + return updateAvailable.get(); |
| 68 | + } catch (InterruptedException e) { |
| 69 | + checkTask.cancel(); |
| 70 | + Thread.currentThread().interrupt(); |
| 71 | + return false; |
| 72 | + } |
56 | 73 | } |
57 | 74 |
|
58 | 75 | @Override |
59 | | - public void triggerUpdate() throws UpdateFailedException { |
60 | | - var monitor = getUpdateMonitor(); |
61 | | - portal.updateApp("x11:0", monitor, UpdatePortal.OPTIONS_DUMMY); |
62 | | - } |
| 76 | + public UpdateProcess prepareUpdate() throws UpdateFailedException { |
| 77 | + var monitorPath = portal.CreateUpdateMonitor(UpdatePortal.OPTIONS_DUMMY); |
| 78 | + if (monitorPath == null) { |
| 79 | + throw new UpdateFailedException("Failed to create UpdateMonitor on DBus"); |
| 80 | + } |
63 | 81 |
|
64 | | - @Override |
65 | | - public long spawnApp() { |
66 | | - var cwdPath = Util.stringToByteList(System.getProperty("user.dir")); |
67 | | - List<List<Byte>> argv = List.of( |
68 | | - Util.stringToByteList(APP_NAME)); |
69 | | - Map<UInt32, FileDescriptor> fds = Collections.emptyMap(); |
70 | | - Map<String, String> envs = Map.of(); |
71 | | - UInt32 flags = new UInt32(FlatpakSpawnFlag.LATEST_VERSION.getValue()); |
72 | | - Map<String, Variant<?>> options = UpdatePortal.OPTIONS_DUMMY; |
73 | | - |
74 | | - return spawnApp(cwdPath, argv, fds, envs, flags, options).longValue(); |
| 82 | + return new FlatpakUpdateProcess(portal.getUpdateMonitor(monitorPath.toString())); |
75 | 83 | } |
76 | 84 |
|
77 | | - @Override |
78 | | - public boolean doesRequireElevatedPermissions() { |
79 | | - return false; |
80 | | - } |
| 85 | + private class FlatpakUpdateProcess implements UpdateProcess { |
81 | 86 |
|
82 | | - @Override |
83 | | - public void close() throws Exception { |
84 | | - try { |
85 | | - if (null != updateMonitor) { |
86 | | - portal.cancelUpdateMonitor(updateMonitor); |
| 87 | + private final CountDownLatch latch = new CountDownLatch(1); |
| 88 | + private final Flatpak.UpdateMonitor monitor; |
| 89 | + private volatile double progress = 0.0; |
| 90 | + private volatile UpdateFailedException error; |
| 91 | + private AutoCloseable signalHandler; |
| 92 | + |
| 93 | + private FlatpakUpdateProcess(Flatpak.UpdateMonitor monitor) { |
| 94 | + this.monitor = monitor; |
| 95 | + startUpdate(); |
| 96 | + } |
| 97 | + |
| 98 | + private void startUpdate() { |
| 99 | + try { |
| 100 | + this.signalHandler = portal.getDBusConnection().addSigHandler(Flatpak.UpdateMonitor.Progress.class, this::handleProgressSignal); |
| 101 | + } catch (DBusException e) { |
| 102 | + LOG.error("DBus error", e); |
| 103 | + latch.countDown(); |
| 104 | + } |
| 105 | + portal.updateApp("x11:0", monitor, UpdatePortal.OPTIONS_DUMMY); |
| 106 | + } |
| 107 | + |
| 108 | + private void handleProgressSignal(Flatpak.UpdateMonitor.Progress signal) { |
| 109 | + int status = ((UInt32) signal.info.get("status").getValue()).intValue(); |
| 110 | + switch (status) { |
| 111 | + case 0 -> { // In progress |
| 112 | + Variant<?> progressVariant = signal.info.get("progress"); |
| 113 | + if (progressVariant != null) { |
| 114 | + progress = ((UInt32) progressVariant.getValue()).doubleValue() / 100.0; // progress reported as int in range [0, 100] |
| 115 | + } |
| 116 | + } |
| 117 | + case 1 -> { // No update available |
| 118 | + error = new UpdateFailedException("No update available"); |
| 119 | + latch.countDown(); |
| 120 | + } |
| 121 | + case 2 -> { // Update complete |
| 122 | + progress = 1.0; |
| 123 | + latch.countDown(); |
| 124 | + } |
| 125 | + case 3 -> { // Update failed |
| 126 | + error = new UpdateFailedException("Update preparation failed"); |
| 127 | + latch.countDown(); |
| 128 | + } |
| 129 | + default -> { |
| 130 | + error = new UpdateFailedException("Unknown update status " + status); |
| 131 | + latch.countDown(); |
| 132 | + } |
87 | 133 | } |
88 | | - portal.close(); |
89 | | - } catch (Exception e) { |
90 | | - LOG.error(e.toString(), e.getCause()); |
91 | 134 | } |
92 | | - } |
93 | 135 |
|
94 | | - private synchronized Flatpak.UpdateMonitor getUpdateMonitor() { |
95 | | - if (updateMonitor == null) { |
96 | | - var updateMonitorPath = portal.CreateUpdateMonitor(UpdatePortal.OPTIONS_DUMMY); |
97 | | - if (updateMonitorPath != null) { |
98 | | - LOG.debug("UpdateMonitor successful created at {}", updateMonitorPath); |
99 | | - updateMonitor = portal.getUpdateMonitor(updateMonitorPath.toString()); |
| 136 | + private void stopReceivingSignals() { |
| 137 | + if (signalHandler != null) { |
100 | 138 | try { |
101 | | - portal.getDBusConnection().addSigHandler(Flatpak.UpdateMonitor.Progress.class, signal -> { |
102 | | - notifyOnUpdateProceeds(signal); |
103 | | - }); |
104 | | - } catch (DBusException e) { |
105 | | - LOG.error(e.toString(), e.getCause()); |
| 139 | + signalHandler.close(); |
| 140 | + } catch (Exception e) { |
| 141 | + LOG.error("Failed to close signal handler", e); |
106 | 142 | } |
107 | | - } else { |
108 | | - LOG.error("Failed to create UpdateMonitor on DBus"); |
| 143 | + signalHandler = null; |
109 | 144 | } |
110 | 145 | } |
111 | | - return updateMonitor; |
112 | | - } |
113 | 146 |
|
114 | | - @Override |
115 | | - public void addProgressListener(ProgressListener listener) { |
116 | | - progressListeners.add(listener); |
117 | | - } |
| 147 | + @Override |
| 148 | + public double preparationProgress() { |
| 149 | + return progress; |
| 150 | + } |
118 | 151 |
|
119 | | - @Override |
120 | | - public void removeProgressListener(ProgressListener listener) { |
121 | | - progressListeners.remove(listener); |
122 | | - } |
| 152 | + @Override |
| 153 | + public void cancel() { |
| 154 | + portal.cancelUpdateMonitor(monitor); |
| 155 | + stopReceivingSignals(); |
| 156 | + 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? |
| 157 | + error = new UpdateFailedException("Update cancelled by user"); |
| 158 | + } |
123 | 159 |
|
124 | | - private void notifyOnUpdateProceeds(Flatpak.UpdateMonitor.Progress signal) { |
125 | | - long status = ((UInt32) signal.info.get("status").getValue()).longValue(); |
126 | | - long progress = 0; |
127 | | - Variant<?> progressVariant = signal.info.get("progress"); |
128 | | - if (null != progressVariant) { |
129 | | - progress = ((UInt32) progressVariant.getValue()).longValue(); |
| 160 | + @Override |
| 161 | + public void await() throws InterruptedException { |
| 162 | + latch.await(); |
130 | 163 | } |
131 | | - Progress p = new Progress(status, progress); |
132 | | - for (ProgressListener listener : progressListeners) { |
133 | | - listener.onProgress(p); |
| 164 | + |
| 165 | + @Override |
| 166 | + public boolean await(long timeout, TimeUnit unit) throws InterruptedException { |
| 167 | + return latch.await(timeout, unit); |
| 168 | + } |
| 169 | + |
| 170 | + private boolean isDone() { |
| 171 | + try { |
| 172 | + return latch.await(0, TimeUnit.MILLISECONDS); |
| 173 | + } catch (InterruptedException e) { |
| 174 | + Thread.currentThread().interrupt(); |
| 175 | + return false; |
| 176 | + } |
134 | 177 | } |
135 | | - } |
136 | 178 |
|
137 | | - private UInt32 spawnApp(List<Byte> cwdPath, List<List<Byte>> argv, Map<UInt32, FileDescriptor> fds, Map<String, String> envs, UInt32 flags, Map<String, Variant<?>> options) { |
138 | | - return portal.Spawn(cwdPath, argv, fds, envs, flags, options); |
| 179 | + @Override |
| 180 | + public ProcessHandle applyUpdate() throws IllegalStateException, IOException { |
| 181 | + if (!isDone()) { |
| 182 | + throw new IllegalStateException("Update preparation is not complete"); |
| 183 | + } |
| 184 | + stopReceivingSignals(); |
| 185 | + if (error != null) { |
| 186 | + throw error; |
| 187 | + } |
| 188 | + |
| 189 | + // spawn new Cryptomator process: |
| 190 | + var cwdPath = Util.stringToByteList(System.getProperty("user.dir")); |
| 191 | + List<List<Byte>> argv = List.of( |
| 192 | + Util.stringToByteList(APP_NAME)); |
| 193 | + Map<UInt32, FileDescriptor> fds = Collections.emptyMap(); |
| 194 | + Map<String, String> envs = Map.of(); |
| 195 | + UInt32 flags = new UInt32(FlatpakSpawnFlag.LATEST_VERSION.getValue()); |
| 196 | + Map<String, Variant<?>> options = UpdatePortal.OPTIONS_DUMMY; |
| 197 | + var pid = portal.Spawn(cwdPath, argv, fds, envs, flags, options).longValue(); |
| 198 | + return ProcessHandle.of(pid).orElseThrow(); |
| 199 | + } |
139 | 200 | } |
| 201 | + |
140 | 202 | } |
0 commit comments