From 0f56d424da63aee65ed9349ea2cd86739601f9ff Mon Sep 17 00:00:00 2001 From: infeo Date: Thu, 20 Aug 2020 12:18:57 +0200 Subject: [PATCH] Refactored AutoStartStrategy: * prevented on best effort basis inconsistent states * extracted the registry setting as an own strategy (by methods) * refactored the overriden methods to call the strategies (registry or folder) depending on the different variables * removed Powershell specific ToggleException * added documentation --- .../ui/preferences/AutoStartStrategy.java | 7 - .../ui/preferences/AutoStartWinStrategy.java | 181 +++++++++++++----- 2 files changed, 132 insertions(+), 56 deletions(-) diff --git a/main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartStrategy.java b/main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartStrategy.java index 10a431e1f..99b21b4cd 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartStrategy.java +++ b/main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartStrategy.java @@ -21,11 +21,4 @@ public interface AutoStartStrategy { } } - class TogglingAutoStartWithPowershellFailedException extends TogglingAutoStartFailedException { - - public TogglingAutoStartWithPowershellFailedException(String message, Throwable cause) { - super(message, cause); - } - - } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartWinStrategy.java b/main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartWinStrategy.java index 02e4faa26..0d2f20536 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartWinStrategy.java +++ b/main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartWinStrategy.java @@ -1,87 +1,185 @@ package org.cryptomator.ui.preferences; -import org.cryptomator.common.Environment; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; +/** + * OS specific class to check, en- and disable the auto start on Windows. + *

+ * Two strategies are implemented for this feature, the first uses the registry and the second one the autostart folder. + *

+ * To check if it is enabled at all, both locations are checked. + * To enable it, first the registry is tried and only on failure the autostart folder is used. + * To disable it, first it is determined, which strategies must be used and in the second step those are executed. + * + * @apiNote This class is not thread safe, hence it should be avoided to be used simultaniously by the same threads. + */ class AutoStartWinStrategy implements AutoStartStrategy { private static final Logger LOG = LoggerFactory.getLogger(AutoStartWinStrategy.class); private static final String HKCU_AUTOSTART_KEY = "\"HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run\""; private static final String AUTOSTART_VALUE = "Cryptomator"; + private static final String WINDOWS_START_MENU_ENTRY = "\\AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Cryptomator.lnk"; + private final String exePath; - private static final String WINDOWS_START_MENU_FOLDER = "\\AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs"; + + private boolean activatedOverFolder; + private boolean activatedOverRegistry; public AutoStartWinStrategy(String exePath) { this.exePath = exePath; + this.activatedOverFolder = false; + this.activatedOverRegistry = false; } @Override public CompletionStage isAutoStartEnabled() { + return isAutoStartEnabledOverRegistry().thenCombine(isAutoStartEnabledOverFolder(), (bReg, bFolder) -> bReg || bFolder); + } + + private CompletableFuture isAutoStartEnabledOverFolder() { + Path autoStartEntry = Path.of(System.getProperty("user.home") + WINDOWS_START_MENU_ENTRY); + this.activatedOverFolder = Files.exists(autoStartEntry); + return CompletableFuture.completedFuture(activatedOverFolder); + } + + private CompletableFuture isAutoStartEnabledOverRegistry() { ProcessBuilder regQuery = new ProcessBuilder("reg", "query", HKCU_AUTOSTART_KEY, // "/v", AUTOSTART_VALUE); try { Process proc = regQuery.start(); - return proc.onExit().thenApply(p -> p.exitValue() == 0); + return proc.onExit().thenApply(p -> { + this.activatedOverRegistry = p.exitValue() == 0; + return activatedOverRegistry; + }); } catch (IOException e) { - LOG.warn("Failed to query {} from registry key {}", AUTOSTART_VALUE, HKCU_AUTOSTART_KEY); + LOG.debug("Failed to query {} from registry key {}", AUTOSTART_VALUE, HKCU_AUTOSTART_KEY); return CompletableFuture.completedFuture(false); } } @Override public void enableAutoStart() throws TogglingAutoStartFailedException { + try { + enableAutoStartOverRegistry().thenAccept((Void v) -> this.activatedOverRegistry = true).exceptionallyCompose(e -> { + LOG.debug("Falling back to using autostart folder."); + return this.enableAutoStartOverFolder(); + }).thenAccept((Void v) -> this.activatedOverFolder = true).get(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new TogglingAutoStartFailedException("Execution of enabling auto start setting was interrupted."); + } catch (ExecutionException e) { + throw new TogglingAutoStartFailedException("Enabling auto start failed both over registry and auto start folder."); + } + } + + private CompletableFuture enableAutoStartOverRegistry() { ProcessBuilder regAdd = new ProcessBuilder("reg", "add", HKCU_AUTOSTART_KEY, // "/v", AUTOSTART_VALUE, // "/t", "REG_SZ", // "/d", "\"" + exePath + "\"", // "/f"); - String command = regAdd.command().stream().collect(Collectors.joining(" ")); try { Process proc = regAdd.start(); - boolean finishedInTime = waitForProcess(proc, 5, TimeUnit.SECONDS); + boolean finishedInTime = waitForProcessOrCancel(proc, 5, TimeUnit.SECONDS); if (finishedInTime && proc.exitValue() == 0) { LOG.debug("Added {} to registry key {}.", AUTOSTART_VALUE, HKCU_AUTOSTART_KEY); + return CompletableFuture.completedFuture(null); } else { - LOG.debug("Registry could not be edited. Error code was {}.", proc.exitValue()); - addShortcutOfAppToAutostartFolder(); - throw new TogglingAutoStartFailedException("Adding registry value failed."); + throw new IOException("Process existed with error code " + proc.exitValue()); } } catch (IOException e) { - addShortcutOfAppToAutostartFolder(); - throw new TogglingAutoStartFailedException("Adding registry value failed. " + command, e); + LOG.debug("Registry could not be edited to set auto start.", e); + return CompletableFuture.failedFuture(new SystemCommandException("Adding registry value failed.")); } } + private CompletableFuture enableAutoStartOverFolder() { + String autoStartFolderEntry = System.getProperty("user.home") + WINDOWS_START_MENU_ENTRY; + String createShortcutCommand = "$s=(New-Object -COM WScript.Shell).CreateShortcut('" + autoStartFolderEntry + "');$s.TargetPath='" + exePath + "';$s.Save();"; + ProcessBuilder shortcutAdd = new ProcessBuilder("cmd", "/c", "Start powershell " + createShortcutCommand); + try { + Process proc = shortcutAdd.start(); + boolean finishedInTime = waitForProcessOrCancel(proc, 5, TimeUnit.SECONDS); + if (finishedInTime && proc.exitValue() == 0) { + LOG.debug("Created file {} for auto start.", autoStartFolderEntry); + return CompletableFuture.completedFuture(null); + } else { + throw new IOException("Process existed with error code " + proc.exitValue()); + } + } catch (IOException e) { + LOG.debug("Adding entry to auto start folder failed.", e); + return CompletableFuture.failedFuture(new SystemCommandException("Adding entry to auto start folder failed.")); + } + } + + @Override public void disableAutoStart() throws TogglingAutoStartFailedException { - ProcessBuilder regRemove = new ProcessBuilder("reg", "delete", HKCU_AUTOSTART_KEY, // - "/v", AUTOSTART_VALUE, // - "/f"); - String command = regRemove.command().stream().collect(Collectors.joining(" ")); - try { - Process proc = regRemove.start(); - boolean finishedInTime = waitForProcess(proc, 5, TimeUnit.SECONDS); - if (finishedInTime && proc.exitValue() == 0) { - LOG.debug("Removed {} from registry key {}.", AUTOSTART_VALUE, HKCU_AUTOSTART_KEY); - } else { - LOG.debug("Registry could not be edited. Error code was {}.", proc.exitValue()); - removeShortcutOfAppFromAutostartFolder(); - throw new TogglingAutoStartFailedException("Removing registry value failed."); - } - } catch (IOException e) { - removeShortcutOfAppFromAutostartFolder(); - throw new TogglingAutoStartFailedException("Removing registry value failed. " + command, e); + if (activatedOverRegistry) { + disableAutoStartOverRegistry().whenComplete((voit, ex) -> { + if (ex == null) { + this.activatedOverRegistry = false; + } + }); + } + + if (activatedOverFolder) { + disableAutoStartOverFolder().whenComplete((voit, ex) -> { + if (ex == null) { + this.activatedOverFolder = false; + } + }); + } + + if (activatedOverRegistry || activatedOverFolder) { + throw new TogglingAutoStartFailedException("Disabling auto start failed both over registry and auto start folder."); } } - private static boolean waitForProcess(Process proc, int timeout, TimeUnit timeUnit) { + public CompletableFuture disableAutoStartOverRegistry() { + ProcessBuilder regRemove = new ProcessBuilder("reg", "delete", HKCU_AUTOSTART_KEY, // + "/v", AUTOSTART_VALUE, // + "/f"); + try { + Process proc = regRemove.start(); + boolean finishedInTime = waitForProcessOrCancel(proc, 5, TimeUnit.SECONDS); + if (finishedInTime && proc.exitValue() == 0) { + LOG.debug("Removed {} from registry key {}.", AUTOSTART_VALUE, HKCU_AUTOSTART_KEY); + return CompletableFuture.completedFuture(null); + } else { + throw new IOException("Process existed with error code " + proc.exitValue()); + } + } catch (IOException e) { + LOG.debug("Registry could not be edited to remove auto start.", e); + return CompletableFuture.failedFuture(new SystemCommandException("Removing registry value failed.")); + } + } + + private CompletableFuture disableAutoStartOverFolder() { + try { + Files.delete(Path.of(WINDOWS_START_MENU_ENTRY)); + LOG.debug("Successfully deleted {}.", WINDOWS_START_MENU_ENTRY); + return CompletableFuture.completedFuture(null); + } catch (NoSuchFileException e) { + //that is also okay + return CompletableFuture.completedFuture(null); + } catch (IOException e) { + LOG.debug("Failed to delete entry from auto start folder.", e); + return CompletableFuture.failedFuture(e); + } + } + + private static boolean waitForProcessOrCancel(Process proc, int timeout, TimeUnit timeUnit) { boolean finishedInTime = false; try { finishedInTime = proc.waitFor(timeout, timeUnit); @@ -96,26 +194,11 @@ class AutoStartWinStrategy implements AutoStartStrategy { return finishedInTime; } - private void addShortcutOfAppToAutostartFolder() throws TogglingAutoStartWithPowershellFailedException { - String startMenuDirectory = System.getProperty("user.home") + WINDOWS_START_MENU_FOLDER + "\\Cryptomator.lnk"; - String createShortcutCommand = "$s=(New-Object -COM WScript.Shell).CreateShortcut('" + startMenuDirectory + "');$s.TargetPath='" + exePath + "';$s.Save();"; - ProcessBuilder shortcutAdd = new ProcessBuilder("cmd", "/c", "Start powershell " + createShortcutCommand); - try { - shortcutAdd.start(); - } catch (IOException e) { - throw new TogglingAutoStartWithPowershellFailedException("Adding shortcut to autostart folder failed.", e); + public class SystemCommandException extends RuntimeException { + + public SystemCommandException(String msg) { + super(msg); } } - private void removeShortcutOfAppFromAutostartFolder() throws TogglingAutoStartWithPowershellFailedException { - String startMenuDirectory = System.getProperty("user.home") + WINDOWS_START_MENU_FOLDER + "\\Cryptomator.lnk"; - ProcessBuilder shortcutRemove = new ProcessBuilder("cmd", "/c del \"" + startMenuDirectory + "\""); - try { - shortcutRemove.start(); - } catch (IOException e) { - throw new TogglingAutoStartWithPowershellFailedException("Removing shortcut from autostart folder failed.", e); - } - } - - }