~exprez135/cryptomator-libre

1fbcf6d517b9e9ad3065405d9bce756e1af5ec3d — Armin Schrenk 9 months ago 8253809
closes #1439
6 files changed, 10 insertions(+), 315 deletions(-)

D main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartMacStrategy.java
D main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartModule.java
D main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartStrategy.java
D main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartWinStrategy.java
M main/ui/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java
M main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesModule.java
D main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartMacStrategy.java => main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartMacStrategy.java +0 -46
@@ 1,46 0,0 @@
package org.cryptomator.ui.preferences;

import org.cryptomator.integrations.autostart.AutoStartProvider;
import org.cryptomator.integrations.autostart.ToggleAutoStartFailedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;

@Deprecated
class AutoStartMacStrategy implements AutoStartStrategy {

	private static final Logger LOG = LoggerFactory.getLogger(AutoStartMacStrategy.class);

	private final AutoStartProvider autoStartProvider;

	public AutoStartMacStrategy(AutoStartProvider autoStartProvider) {
		this.autoStartProvider = autoStartProvider;
	}

	@Override
	public CompletionStage<Boolean> isAutoStartEnabled() {
		return CompletableFuture.completedFuture(autoStartProvider.isEnabled());
	}

	@Override
	public void enableAutoStart() throws TogglingAutoStartFailedException {
		try {
			autoStartProvider.enable();
			LOG.debug("Added login item.");
		} catch (ToggleAutoStartFailedException e) {
			throw new TogglingAutoStartFailedException("Failed to add login item.");
		}
	}

	@Override
	public void disableAutoStart() throws TogglingAutoStartFailedException {
		try {
			autoStartProvider.disable();
			LOG.debug("Removed login item.");
		} catch (ToggleAutoStartFailedException e) {
			throw new TogglingAutoStartFailedException("Failed to remove login item.");
		}
	}
}

D main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartModule.java => main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartModule.java +0 -27
@@ 1,27 0,0 @@
package org.cryptomator.ui.preferences;

import dagger.Module;
import dagger.Provides;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.integrations.autostart.AutoStartProvider;

import java.util.Optional;

@Deprecated
@Module
abstract class AutoStartModule {

	@Provides
	@PreferencesScoped
	public static Optional<AutoStartStrategy> provideAutoStartStrategy(Optional<AutoStartProvider> autoStartProvider) {
		if (SystemUtils.IS_OS_MAC_OSX && autoStartProvider.isPresent()) {
			return Optional.of(new AutoStartMacStrategy(autoStartProvider.get()));
		} else if (SystemUtils.IS_OS_WINDOWS) {
			Optional<String> exeName = ProcessHandle.current().info().command();
			return exeName.map(AutoStartWinStrategy::new);
		} else {
			return Optional.empty();
		}
	}

}

D main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartStrategy.java => main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartStrategy.java +0 -25
@@ 1,25 0,0 @@
package org.cryptomator.ui.preferences;

import java.util.concurrent.CompletionStage;

@Deprecated
public interface AutoStartStrategy {

	CompletionStage<Boolean> isAutoStartEnabled();

	void enableAutoStart() throws TogglingAutoStartFailedException;

	void disableAutoStart() throws TogglingAutoStartFailedException;

	class TogglingAutoStartFailedException extends Exception {

		public TogglingAutoStartFailedException(String message) {
			super(message);
		}

		public TogglingAutoStartFailedException(String message, Throwable cause) {
			super(message, cause);
		}

	}
}

D main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartWinStrategy.java => main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartWinStrategy.java +0 -209
@@ 1,209 0,0 @@
package org.cryptomator.ui.preferences;

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;

/**
 * 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>
 * The registry strategy checks/add/removes at the registry key {@value HKCU_AUTOSTART_KEY} an entry for Cryptomator.
 * The folder strategy checks/add/removes at the location {@value WINDOWS_START_MENU_ENTRY}.
 * <p>
 * To check if the feature is active, both strategies are applied.
 * To enable the feature, first the registry is tried and only on failure the autostart folder is used.
 * To disable it, first it is determined by an internal state, 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 call its methods simultaniously by different threads.
 * @deprecated To be moved to integration-win project
 */
@Deprecated
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 boolean activatedUsingFolder;
	private boolean activatedUsingRegistry;

	public AutoStartWinStrategy(String exePath) {
		this.exePath = exePath;
		this.activatedUsingFolder = false;
		this.activatedUsingRegistry = false;
	}

	@Override
	public CompletionStage<Boolean> isAutoStartEnabled() {
		return isAutoStartEnabledUsingRegistry().thenCombine(isAutoStartEnabledUsingFolder(), (bReg, bFolder) -> bReg || bFolder);
	}

	private CompletableFuture<Boolean> isAutoStartEnabledUsingFolder() {
		Path autoStartEntry = Path.of(System.getProperty("user.home") + WINDOWS_START_MENU_ENTRY);
		this.activatedUsingFolder = Files.exists(autoStartEntry);
		return CompletableFuture.completedFuture(activatedUsingFolder);
	}

	private CompletableFuture<Boolean> isAutoStartEnabledUsingRegistry() {
		ProcessBuilder regQuery = new ProcessBuilder("reg", "query", HKCU_AUTOSTART_KEY, //
				"/v", AUTOSTART_VALUE);
		try {
			Process proc = regQuery.start();
			return proc.onExit().thenApply(p -> {
				this.activatedUsingRegistry = p.exitValue() == 0;
				return activatedUsingRegistry;
			});
		} catch (IOException e) {
			LOG.debug("Failed to query {} from registry key {}", AUTOSTART_VALUE, HKCU_AUTOSTART_KEY);
			return CompletableFuture.completedFuture(false);
		}
	}

	@Override
	public void enableAutoStart() throws TogglingAutoStartFailedException {
		try {
			enableAutoStartUsingRegistry().thenAccept((Void v) -> this.activatedUsingRegistry = true).exceptionallyCompose(e -> {
				LOG.debug("Falling back to using autostart folder.");
				return this.enableAutoStartUsingFolder();
			}).thenAccept((Void v) -> this.activatedUsingFolder = 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 using registry and auto start folder.");
		}
	}

	private CompletableFuture<Void> enableAutoStartUsingRegistry() {
		ProcessBuilder regAdd = new ProcessBuilder("reg", "add", HKCU_AUTOSTART_KEY, //
				"/v", AUTOSTART_VALUE, //
				"/t", "REG_SZ", //
				"/d", "\"" + exePath + "\"", //
				"/f");
		try {
			Process proc = regAdd.start();
			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 exited 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> enableAutoStartUsingFolder() {
		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 exited 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 {
		if (activatedUsingRegistry) {
			disableAutoStartUsingRegistry().whenComplete((voit, ex) -> {
				if (ex == null) {
					this.activatedUsingRegistry = false;
				}
			});
		}

		if (activatedUsingFolder) {
			disableAutoStartUsingFolder().whenComplete((voit, ex) -> {
				if (ex == null) {
					this.activatedUsingFolder = false;
				}
			});
		}

		if (activatedUsingRegistry || activatedUsingFolder) {
			throw new TogglingAutoStartFailedException("Disabling auto start failed using registry and/or auto start folder.");
		}
	}

	public CompletableFuture<Void> disableAutoStartUsingRegistry() {
		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 exited 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<Void> disableAutoStartUsingFolder() {
		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);
		} catch (InterruptedException e) {
			LOG.error("Timeout while reading registry", e);
			Thread.currentThread().interrupt();
		} finally {
			if (!finishedInTime) {
				proc.destroyForcibly();
			}
		}
		return finishedInTime;
	}

	private class SystemCommandException extends RuntimeException {

		public SystemCommandException(String msg) {
			super(msg);
		}
	}

}

M main/ui/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java => main/ui/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java +9 -7
@@ 5,6 5,7 @@ import org.cryptomator.common.LicenseHolder;
import org.cryptomator.common.settings.KeychainBackend;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.UiTheme;
import org.cryptomator.integrations.autostart.AutoStartProvider;
import org.cryptomator.integrations.keychain.KeychainAccessProvider;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxController;


@@ 31,6 32,7 @@ import java.util.Arrays;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.stream.Collectors;



@@ 42,7 44,7 @@ public class GeneralPreferencesController implements FxController {
	private final Stage window;
	private final Settings settings;
	private final boolean trayMenuSupported;
	private final Optional<AutoStartStrategy> autoStartStrategy;
	private final Optional<AutoStartProvider> autoStartStrategy;
	private final ObjectProperty<SelectedPreferencesTab> selectedTabProperty;
	private final LicenseHolder licenseHolder;
	private final ExecutorService executor;


@@ 61,7 63,7 @@ public class GeneralPreferencesController implements FxController {
	public RadioButton nodeOrientationRtl;

	@Inject
	GeneralPreferencesController(@PreferencesWindow Stage window, Settings settings, @Named("trayMenuSupported") boolean trayMenuSupported, Optional<AutoStartStrategy> autoStartStrategy, Set<KeychainAccessProvider> keychainAccessProviders, ObjectProperty<SelectedPreferencesTab> selectedTabProperty, LicenseHolder licenseHolder, ExecutorService executor, ResourceBundle resourceBundle, Application application, Environment environment, ErrorComponent.Builder errorComponent) {
	GeneralPreferencesController(@PreferencesWindow Stage window, Settings settings, @Named("trayMenuSupported") boolean trayMenuSupported, Optional<AutoStartProvider> autoStartStrategy, Set<KeychainAccessProvider> keychainAccessProviders, ObjectProperty<SelectedPreferencesTab> selectedTabProperty, LicenseHolder licenseHolder, ExecutorService executor, ResourceBundle resourceBundle, Application application, Environment environment, ErrorComponent.Builder errorComponent) {
		this.window = window;
		this.settings = settings;
		this.trayMenuSupported = trayMenuSupported;


@@ 90,7 92,7 @@ public class GeneralPreferencesController implements FxController {
		debugModeCheckbox.selectedProperty().bindBidirectional(settings.debugMode());

		autoStartStrategy.ifPresent(autoStart -> {
			autoStart.isAutoStartEnabled().thenAccept(enabled -> {
			CompletableFuture.completedFuture(autoStart.isEnabled()).thenAccept(enabled -> {
				Platform.runLater(() -> autoStartCheckbox.setSelected(enabled));
			});
		});


@@ 198,10 200,10 @@ public class GeneralPreferencesController implements FxController {

	private static class ToggleAutoStartTask extends Task<Void> {

		private final AutoStartStrategy autoStart;
		private final AutoStartProvider autoStart;
		private final boolean enable;

		public ToggleAutoStartTask(AutoStartStrategy autoStart, boolean enable) {
		public ToggleAutoStartTask(AutoStartProvider autoStart, boolean enable) {
			this.autoStart = autoStart;
			this.enable = enable;



@@ 211,9 213,9 @@ public class GeneralPreferencesController implements FxController {
		@Override
		protected Void call() throws Exception {
			if (enable) {
				autoStart.enableAutoStart();
				autoStart.enable();
			} else {
				autoStart.disableAutoStart();
				autoStart.disable();
			}
			return null;
		}

M main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesModule.java => main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesModule.java +1 -1
@@ 20,7 20,7 @@ import javafx.stage.Stage;
import java.util.Map;
import java.util.ResourceBundle;

@Module(includes = {AutoStartModule.class})
@Module
abstract class PreferencesModule {

	@Provides