Skip to content

Commit 37835e8

Browse files
Merge pull request #92 from cryptomator/feature/update-api
add `UpdateMechanism` for .dmg files
2 parents df6ee27 + 356c3d3 commit 37835e8

File tree

4 files changed

+154
-2
lines changed

4 files changed

+154
-2
lines changed

pom.xml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
<project.jdk.version>25</project.jdk.version>
3333

3434
<!-- runtime dependencies -->
35-
<api.version>1.7.0</api.version>
35+
<api.version>1.8.0-beta1</api.version>
3636
<slf4j.version>2.0.17</slf4j.version>
3737

3838
<!-- test dependencies -->
@@ -63,6 +63,20 @@
6363
</license>
6464
</licenses>
6565

66+
<repositories>
67+
<repository>
68+
<name>Central Portal Snapshots</name>
69+
<id>central-portal-snapshots</id>
70+
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
71+
<releases>
72+
<enabled>false</enabled>
73+
</releases>
74+
<snapshots>
75+
<enabled>true</enabled>
76+
</snapshots>
77+
</repository>
78+
</repositories>
79+
6680
<dependencies>
6781
<dependency>
6882
<groupId>org.cryptomator</groupId>

src/main/java/module-info.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
import org.cryptomator.integrations.revealpath.RevealPathService;
44
import org.cryptomator.integrations.tray.TrayIntegrationProvider;
55
import org.cryptomator.integrations.uiappearance.UiAppearanceProvider;
6+
import org.cryptomator.integrations.update.UpdateMechanism;
67
import org.cryptomator.macos.autostart.MacAutoStartProvider;
78
import org.cryptomator.macos.keychain.MacSystemKeychainAccess;
89
import org.cryptomator.macos.keychain.TouchIdKeychainAccess;
910
import org.cryptomator.macos.revealpath.OpenCmdRevealPathService;
1011
import org.cryptomator.macos.tray.MacTrayIntegrationProvider;
1112
import org.cryptomator.macos.uiappearance.MacUiAppearanceProvider;
13+
import org.cryptomator.macos.update.DmgUpdateMechanism;
1214

1315
module org.cryptomator.integrations.mac {
1416
requires org.cryptomator.integrations.api;
@@ -19,4 +21,5 @@
1921
provides RevealPathService with OpenCmdRevealPathService;
2022
provides TrayIntegrationProvider with MacTrayIntegrationProvider;
2123
provides UiAppearanceProvider with MacUiAppearanceProvider;
24+
provides UpdateMechanism with DmgUpdateMechanism;
2225
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package org.cryptomator.macos.update;
2+
3+
import org.cryptomator.integrations.common.LocalizedDisplayName;
4+
import org.cryptomator.integrations.common.OperatingSystem;
5+
import org.cryptomator.integrations.update.DownloadUpdateInfo;
6+
import org.cryptomator.integrations.update.DownloadUpdateMechanism;
7+
import org.cryptomator.integrations.update.UpdateFailedException;
8+
import org.cryptomator.integrations.update.UpdateMechanism;
9+
import org.cryptomator.integrations.update.UpdateStep;
10+
import org.cryptomator.macos.common.Localization;
11+
import org.slf4j.Logger;
12+
import org.slf4j.LoggerFactory;
13+
14+
import java.io.IOException;
15+
import java.io.InterruptedIOException;
16+
import java.nio.charset.StandardCharsets;
17+
import java.nio.file.Files;
18+
import java.nio.file.Path;
19+
import java.nio.file.StandardOpenOption;
20+
import java.util.List;
21+
import java.util.UUID;
22+
23+
@OperatingSystem(OperatingSystem.Value.MAC)
24+
@LocalizedDisplayName(bundle = "MacIntegrationsBundle", key = "org.cryptomator.macos.update.dmg.displayName")
25+
public class DmgUpdateMechanism extends DownloadUpdateMechanism {
26+
27+
private static final Logger LOG = LoggerFactory.getLogger(DmgUpdateMechanism.class);
28+
29+
@Override
30+
protected DownloadUpdateInfo checkForUpdate(String currentVersion, LatestVersionResponse response) {
31+
String suffix = switch (System.getProperty("os.arch")) {
32+
case "aarch64", "arm64" -> "arm64.dmg";
33+
default -> "x64.dmg";
34+
};
35+
var updateVersion = response.latestVersion().macVersion();
36+
var asset = response.assets().stream().filter(a -> a.name().endsWith(suffix)).findAny().orElse(null);
37+
if (UpdateMechanism.isUpdateAvailable(updateVersion, currentVersion) && asset != null) {
38+
return new DownloadUpdateInfo(this, updateVersion, asset);
39+
} else {
40+
return null;
41+
}
42+
}
43+
44+
@Override
45+
public UpdateStep secondStep(Path workDir, Path assetPath, DownloadUpdateInfo updateInfo) {
46+
return UpdateStep.of(Localization.get().getString("org.cryptomator.macos.update.dmg.unpacking"), () -> this.unpack(workDir, assetPath));
47+
}
48+
49+
private UpdateStep unpack(Path workDir, Path assetPath) throws IOException {
50+
// Extract Cryptomator.app from the .dmg file
51+
var processBuilder = new ProcessBuilder(List.of("/bin/zsh", "-s"));
52+
processBuilder.directory(workDir.toFile());
53+
processBuilder.environment().put("DMG_PATH", assetPath.toString());
54+
processBuilder.environment().put("MOUNT_ID", UUID.randomUUID().toString());
55+
Process p = processBuilder.start();
56+
try {
57+
try (var stdin = p.outputWriter()) {
58+
stdin.write("""
59+
trap 'hdiutil detach "/Volumes/Cryptomator_${MOUNT_ID}" -quiet || true' EXIT
60+
hdiutil attach "${DMG_PATH}" -mountpoint "/Volumes/Cryptomator_${MOUNT_ID}" -nobrowse -quiet
61+
cp -R "/Volumes/Cryptomator_${MOUNT_ID}/Cryptomator.app" 'Cryptomator.app'
62+
""");
63+
}
64+
if (p.waitFor() != 0) {
65+
LOG.error("Failed to extract DMG, exit code: {}, output: {}", p.exitValue(), new String(p.getErrorStream().readAllBytes()));
66+
throw new IOException("Failed to extract DMG, exit code: " + p.exitValue());
67+
}
68+
LOG.debug("Unpacked app: {}", workDir.resolve("Cryptomator.app"));
69+
} catch (InterruptedException e) {
70+
Thread.currentThread().interrupt();
71+
throw new InterruptedIOException("Failed to extract DMG, interrupted");
72+
}
73+
return UpdateStep.of(Localization.get().getString("org.cryptomator.macos.update.dmg.verifying"), () -> this.verify(workDir, assetPath));
74+
}
75+
76+
private UpdateStep verify(Path workDir, Path assetPath) throws IOException {
77+
// Verify code signature of the extracted .app
78+
var processBuilder = new ProcessBuilder(List.of("/bin/zsh", "-s"));
79+
processBuilder.directory(workDir.toFile());
80+
Process p = processBuilder.start();
81+
try {
82+
try (var stdin = p.outputWriter()) {
83+
stdin.write("""
84+
codesign --verify --deep --strict 'Cryptomator.app'
85+
spctl --assess --type execute 'Cryptomator.app'
86+
""");
87+
}
88+
if (p.waitFor() != 0) {
89+
LOG.error("Checking code signature failed: {}, output: {}", p.exitValue(), new String(p.getErrorStream().readAllBytes()));
90+
throw new UpdateFailedException("Invalid Code Signature.");
91+
}
92+
LOG.debug("Verified app: {}", workDir.resolve("Cryptomator.app"));
93+
} catch (InterruptedException e) {
94+
Thread.currentThread().interrupt();
95+
throw new InterruptedIOException("Code signature verification interrupted");
96+
}
97+
return UpdateStep.of(Localization.get().getString("org.cryptomator.macos.update.dmg.restarting"), () -> this.restart(workDir));
98+
}
99+
100+
public UpdateStep restart(Path workDir) throws IllegalStateException, IOException {
101+
String selfPath = ProcessHandle.current().info().command().orElse("");
102+
String installPath;
103+
if (selfPath.startsWith("/Applications/Cryptomator.app")) {
104+
installPath = "/Applications/Cryptomator.app";
105+
} else if (selfPath.contains("/Cryptomator.app/")) {
106+
installPath = selfPath.substring(0, selfPath.indexOf("/Cryptomator.app/")) + "/Cryptomator.app";
107+
} else {
108+
throw new UpdateFailedException("Cannot determine destination path for Cryptomator.app, current path: " + selfPath);
109+
}
110+
LOG.info("Restarting to apply Update in {} now...", workDir);
111+
String script = """
112+
while kill -0 ${CRYPTOMATOR_PID} 2> /dev/null; do sleep 0.2; done;
113+
if [ -d "${CRYPTOMATOR_INSTALL_PATH}" ]; then
114+
echo "Removing old installation at ${CRYPTOMATOR_INSTALL_PATH}";
115+
rm -rf "${CRYPTOMATOR_INSTALL_PATH}"
116+
fi
117+
mv 'Cryptomator.app' "${CRYPTOMATOR_INSTALL_PATH}";
118+
open "${CRYPTOMATOR_INSTALL_PATH}";
119+
""";
120+
Files.writeString(workDir.resolve("install.sh"), script, StandardCharsets.US_ASCII, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
121+
var command = List.of("/bin/zsh", "-c", "/usr/bin/nohup zsh install.sh >install.log 2>&1 &");
122+
var processBuilder = new ProcessBuilder(command);
123+
processBuilder.directory(workDir.toFile());
124+
processBuilder.environment().put("CRYPTOMATOR_PID", String.valueOf(ProcessHandle.current().pid()));
125+
processBuilder.environment().put("CRYPTOMATOR_INSTALL_PATH", installPath);
126+
processBuilder.start();
127+
128+
return UpdateStep.EXIT;
129+
}
130+
131+
}
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
11
org.cryptomator.macos.keychain.displayName=macOS Keychain
2-
org.cryptomator.macos.keychain.touchIdDisplayName=Touch ID
2+
org.cryptomator.macos.keychain.touchIdDisplayName=Touch ID
3+
org.cryptomator.macos.update.dmg.displayName=Download .dmg file
4+
org.cryptomator.macos.update.dmg.unpacking=Unpacking...
5+
org.cryptomator.macos.update.dmg.verifying=Verifying...
6+
org.cryptomator.macos.update.dmg.restarting=Restarting...

0 commit comments

Comments
 (0)