Переглянути джерело

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

infeo 4 роки тому
батько
коміт
0f56d424da

+ 0 - 7
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);
-		}
-
-	}
 }

+ 121 - 38
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.
+ * <p>
+ * Two strategies are implemented for this feature, the first uses the registry and the second one the autostart folder.
+ * <p>
+ * 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<Boolean> isAutoStartEnabled() {
+		return isAutoStartEnabledOverRegistry().thenCombine(isAutoStartEnabledOverFolder(), (bReg, bFolder) -> bReg || bFolder);
+	}
+
+	private CompletableFuture<Boolean> isAutoStartEnabledOverFolder() {
+		Path autoStartEntry = Path.of(System.getProperty("user.home") + WINDOWS_START_MENU_ENTRY);
+		this.activatedOverFolder = Files.exists(autoStartEntry);
+		return CompletableFuture.completedFuture(activatedOverFolder);
+	}
+
+	private CompletableFuture<Boolean> 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<Void> 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 {
+				throw new IOException("Process existed with error code " + proc.exitValue());
+			}
+		} catch (IOException e) {
+			LOG.debug("Registry could not be edited to set auto start.", e);
+			return CompletableFuture.failedFuture(new SystemCommandException("Adding registry value failed."));
+		}
+	}
+
+	private CompletableFuture<Void> 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 {
-				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("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 {
+		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.");
+		}
+	}
+
+	public CompletableFuture<Void> disableAutoStartOverRegistry() {
 		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);
+			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 {
-				LOG.debug("Registry could not be edited. Error code was {}.", proc.exitValue());
-				removeShortcutOfAppFromAutostartFolder();
-				throw new TogglingAutoStartFailedException("Removing registry value failed.");
+				throw new IOException("Process existed with error code " + proc.exitValue());
 			}
 		} catch (IOException e) {
-			removeShortcutOfAppFromAutostartFolder();
-			throw new TogglingAutoStartFailedException("Removing registry value failed. " + command, e);
+			LOG.debug("Registry could not be edited to remove auto start.", e);
+			return CompletableFuture.failedFuture(new SystemCommandException("Removing registry value failed."));
 		}
 	}
 
-	private static boolean waitForProcess(Process proc, int timeout, TimeUnit timeUnit) {
+	private CompletableFuture<Void> 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 {
 
-	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);
+		public SystemCommandException(String msg) {
+			super(msg);
 		}
 	}
 
-
 }