-
Notifications
You must be signed in to change notification settings - Fork 2
add UpdateMechanism for .dmg files
#92
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
base: develop
Are you sure you want to change the base?
Changes from 3 commits
f954cbe
6a43c5e
a9542ea
2788e54
20837a2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| package org.cryptomator.macos.update; | ||
|
|
||
| import org.cryptomator.integrations.common.LocalizedDisplayName; | ||
| import org.cryptomator.integrations.common.OperatingSystem; | ||
| import org.cryptomator.integrations.update.DownloadUpdateInfo; | ||
| import org.cryptomator.integrations.update.DownloadUpdateMechanism; | ||
| import org.cryptomator.integrations.update.UpdateFailedException; | ||
| import org.cryptomator.integrations.update.UpdateMechanism; | ||
| import org.cryptomator.integrations.update.UpdateStep; | ||
| import org.cryptomator.macos.common.Localization; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| import java.io.IOException; | ||
| import java.io.InterruptedIOException; | ||
| import java.nio.charset.StandardCharsets; | ||
| import java.nio.file.Files; | ||
| import java.nio.file.Path; | ||
| import java.nio.file.StandardOpenOption; | ||
| import java.util.List; | ||
| import java.util.UUID; | ||
|
|
||
| @OperatingSystem(OperatingSystem.Value.MAC) | ||
| @LocalizedDisplayName(bundle = "MacIntegrationsBundle", key = "org.cryptomator.macos.update.dmg.displayName") | ||
| public class DmgUpdateMechanism extends DownloadUpdateMechanism { | ||
|
|
||
| private static final Logger LOG = LoggerFactory.getLogger(DmgUpdateMechanism.class); | ||
|
|
||
| @Override | ||
| protected DownloadUpdateInfo checkForUpdate(String currentVersion, LatestVersionResponse response) { | ||
| String suffix = switch (System.getProperty("os.arch")) { | ||
| case "aarch64", "arm64" -> "arm64.dmg"; | ||
| default -> "x64.dmg"; | ||
| }; | ||
| var updateVersion = response.latestVersion().macVersion(); | ||
| var asset = response.assets().stream().filter(a -> a.name().endsWith(suffix)).findAny().orElse(null); | ||
| if (UpdateMechanism.isUpdateAvailable(updateVersion, currentVersion) && asset != null) { | ||
| return new DownloadUpdateInfo(this, updateVersion, asset); | ||
| } else { | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public UpdateStep secondStep(Path workDir, Path assetPath, DownloadUpdateInfo updateInfo) { | ||
| return UpdateStep.of(Localization.get().getString("org.cryptomator.macos.update.dmg.unpacking"), () -> this.unpack(workDir, assetPath)); | ||
| } | ||
|
|
||
| private UpdateStep unpack(Path workDir, Path assetPath) throws IOException { | ||
| // Extract Cryptomator.app from the .dmg file | ||
| String script = """ | ||
| hdiutil attach "${DMG_PATH}" -mountpoint "/Volumes/Cryptomator_${MOUNT_ID}" -nobrowse -quiet && | ||
| cp -R "/Volumes/Cryptomator_${MOUNT_ID}/Cryptomator.app" 'Cryptomator.app' && | ||
| hdiutil detach "/Volumes/Cryptomator_${MOUNT_ID}" -quiet | ||
| """; | ||
| var command = List.of("/bin/zsh", "-c", script); | ||
| var processBuilder = new ProcessBuilder(command); | ||
| processBuilder.directory(workDir.toFile()); | ||
| processBuilder.environment().put("DMG_PATH", assetPath.toString()); | ||
| processBuilder.environment().put("MOUNT_ID", UUID.randomUUID().toString()); | ||
| Process p = processBuilder.start(); | ||
| try { | ||
| if (p.waitFor() != 0) { | ||
| LOG.error("Failed to extract DMG, exit code: {}, output: {}", p.exitValue(), new String(p.getErrorStream().readAllBytes())); | ||
| throw new IOException("Failed to extract DMG, exit code: " + p.exitValue()); | ||
|
Comment on lines
+52
to
+65
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. Mountpoint needs to be created and cleaned up in the DMG extraction script
🤖 Prompt for AI Agents
Member
Author
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. It doesn't fail. macOS treats 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. 🧩 Analysis chain🌐 Web query: 💡 Result: Short answer: hdiutil will normally create a volume directory under /Volumes automatically when you attach an image. If you supply -mountpoint or -mountroot, the target directory must already exist (so create it with mkdir -p first). Examples:
Notes:
Sources: hdiutil man pages and Apple discussions. [1] [2] [3] 🌐 Web query: 💡 Result: Short summary of macOS /Volumes + autofs/automount behavior and gotchas
Sources
If you want, I can:
@overheadhunter, thank you for the clarification! I found documentation indicating that However, there's still a separate concern about cleanup: the current script uses trap 'hdiutil detach "/Volumes/Cryptomator_${MOUNT_ID}" -quiet 2>/dev/null || true' EXIT
hdiutil attach "${DMG_PATH}" -mountpoint "/Volumes/Cryptomator_${MOUNT_ID}" -nobrowse -quiet
cp -R "/Volumes/Cryptomator_${MOUNT_ID}/Cryptomator.app" 'Cryptomator.app'This would ensure the volume is always detached regardless of whether the copy succeeds or fails. What do you think? |
||
| } | ||
| LOG.debug("Update ready: {}", workDir.resolve("Cryptomator.app")); | ||
| } catch (InterruptedException e) { | ||
| Thread.currentThread().interrupt(); | ||
| throw new InterruptedIOException("Failed to extract DMG, interrupted"); | ||
| } | ||
| return UpdateStep.of(Localization.get().getString("org.cryptomator.macos.update.dmg.restarting"), () -> this.restart(workDir)); | ||
| } | ||
|
|
||
| public UpdateStep restart(Path workDir) throws IllegalStateException, IOException { | ||
| String selfPath = ProcessHandle.current().info().command().orElse(""); | ||
| String installPath; | ||
| if (selfPath.startsWith("/Applications/Cryptomator.app")) { | ||
| installPath = "/Applications/Cryptomator.app"; | ||
| } else if (selfPath.contains("/Cryptomator.app/")) { | ||
| installPath = selfPath.substring(0, selfPath.indexOf("/Cryptomator.app/")) + "/Cryptomator.app"; | ||
| } else { | ||
| throw new UpdateFailedException("Cannot determine destination path for Cryptomator.app, current path: " + selfPath); | ||
| } | ||
| LOG.info("Restarting to apply Update in {} now...", workDir); | ||
| String script = """ | ||
| while kill -0 ${CRYPTOMATOR_PID} 2> /dev/null; do sleep 0.2; done; | ||
| if [ -d "${CRYPTOMATOR_INSTALL_PATH}" ]; then | ||
| echo "Removing old installation at ${CRYPTOMATOR_INSTALL_PATH}"; | ||
| rm -rf "${CRYPTOMATOR_INSTALL_PATH}" | ||
| fi | ||
| mv 'Cryptomator.app' "${CRYPTOMATOR_INSTALL_PATH}"; | ||
| open -a "${CRYPTOMATOR_INSTALL_PATH}"; | ||
| """; | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| Files.writeString(workDir.resolve("install.sh"), script, StandardCharsets.US_ASCII, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW); | ||
| var command = List.of("bash", "-c", "/usr/bin/nohup bash install.sh >install.log 2>&1 &"); | ||
| var processBuilder = new ProcessBuilder(command); | ||
| processBuilder.directory(workDir.toFile()); | ||
| processBuilder.environment().put("CRYPTOMATOR_PID", String.valueOf(ProcessHandle.current().pid())); | ||
| processBuilder.environment().put("CRYPTOMATOR_INSTALL_PATH", installPath); | ||
| processBuilder.start(); | ||
|
|
||
| return UpdateStep.EXIT; | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,5 @@ | ||
| org.cryptomator.macos.keychain.displayName=macOS Keychain | ||
| org.cryptomator.macos.keychain.touchIdDisplayName=Touch ID | ||
| org.cryptomator.macos.keychain.touchIdDisplayName=Touch ID | ||
| org.cryptomator.macos.update.dmg.displayName=Download .dmg file | ||
| org.cryptomator.macos.update.dmg.unpacking=Unpacking... | ||
| org.cryptomator.macos.update.dmg.restarting=Restarting... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
Always detach DMG and verify signature; avoid leaks on failure.
&&skips detach on copy failure, leaving the image mounted. Add trap + strict mode. Also verify codesign/notarization before proceeding.If you have an expected Team ID, also assert it (e.g., via
codesign -dv --verbose=4and grep TeamIdentifier).🏁 Script executed:
Length of output: 110
🏁 Script executed:
Length of output: 1965
🏁 Script executed:
Length of output: 0
Always ensure the DMG is detached on failure and the app is properly signed before use
unpackmethod (src/main/java/org/cryptomator/macos/update/DmgUpdateMechanism.java), prepend your shell script withset -euo pipefailand usedittoovercp -Rto copy the app bundle (it preserves extended attributes and ACLs).codesign -dv --verbose=4), to guard against tampered or unsigned binaries.🤖 Prompt for AI Agents