瀏覽代碼

Improved error handling

* Created AsyncTaskService to build async UI operations which always log
uncaught exceptions
* Changed all executor service invocations in the UI to invocations of
AsyncTaskService
* Improved error handling in some other places, especially
try-with-resources
* Unlocking read/write locks in NioFile when opening of a channel fails
Markus Kreusch 9 年之前
父節點
當前提交
7022a80c95

+ 1 - 1
main/commons/src/main/java/org/cryptomator/common/ConsumerThrowingException.java

@@ -1,7 +1,7 @@
 package org.cryptomator.common;
 
 @FunctionalInterface
-public interface ConsumerThrowingException<T, E extends Exception> {
+public interface ConsumerThrowingException<T, E extends Throwable> {
 
 	void accept(T t) throws E;
 

+ 1 - 1
main/commons/src/main/java/org/cryptomator/common/RunnableThrowingException.java

@@ -1,7 +1,7 @@
 package org.cryptomator.common;
 
 @FunctionalInterface
-public interface RunnableThrowingException<T extends Exception> {
+public interface RunnableThrowingException<T extends Throwable> {
 
 	void run() throws T;
 

+ 56 - 0
main/commons/src/main/java/org/cryptomator/common/StackTrace.java

@@ -0,0 +1,56 @@
+/*******************************************************************************
+ * Copyright (c) 2016 Markus Kreusch and others.
+ * This file is licensed under the terms of the MIT license.
+ * See the LICENSE.txt file for more info.
+ *
+ * Contributors:
+ *     Markus Kreusch - initial implementation
+ *******************************************************************************/
+package org.cryptomator.common;
+
+import java.util.stream.Stream;
+
+/**
+ * Utility to print stack traces while analyzing issues.
+ * 
+ * @author Markus Kreusch
+ */
+public class StackTrace {
+
+	public static void print(String message) {
+		Thread thread = Thread.currentThread();
+		System.err.println(stackTraceFor(message, thread));
+	}
+
+	private static String stackTraceFor(String message, Thread thread) {
+		StringBuilder result = new StringBuilder();
+		appendMessageAndThreadName(result, message, thread);
+		appendStackTrace(thread, result);
+		return result.toString();
+	}
+
+	private static void appendStackTrace(Thread thread, StringBuilder result) {
+		Stream.of(thread.getStackTrace()) //
+				.skip(4) //
+				.forEach(stackTraceElement -> append(stackTraceElement, result));
+	}
+
+	private static void appendMessageAndThreadName(StringBuilder result, String message, Thread thread) {
+		result //
+				.append('[') //
+				.append(thread.getName()) //
+				.append("] ") //
+				.append(message);
+	}
+
+	private static void append(StackTraceElement stackTraceElement, StringBuilder result) {
+		String className = stackTraceElement.getClassName();
+		String methodName = stackTraceElement.getMethodName();
+		String fileName = stackTraceElement.getFileName();
+		int lineNumber = stackTraceElement.getLineNumber();
+		result.append('\n') //
+				.append(className).append(':').append(methodName) //
+				.append(" (").append(fileName).append(':').append(lineNumber).append(')');
+	}
+
+}

+ 8 - 0
main/commons/src/main/java/org/cryptomator/common/SupplierThrowingException.java

@@ -0,0 +1,8 @@
+package org.cryptomator.common;
+
+@FunctionalInterface
+public interface SupplierThrowingException<T, E extends Throwable> {
+
+	T get() throws E;
+
+}

+ 4 - 1
main/filesystem-api/src/main/java/org/cryptomator/io/FileContents.java

@@ -4,6 +4,7 @@ import java.io.IOException;
 import java.io.Reader;
 import java.io.UncheckedIOException;
 import java.nio.channels.Channels;
+import java.nio.channels.ReadableByteChannel;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 
@@ -28,7 +29,9 @@ public final class FileContents {
 	 * @return The file's content interpreted in this FileContents' charset.
 	 */
 	public String readContents(File file) {
-		try (Reader reader = Channels.newReader(file.openReadable(), charset.newDecoder(), -1)) {
+		try ( //
+				ReadableByteChannel channel = file.openReadable(); //
+				Reader reader = Channels.newReader(channel, charset.newDecoder(), -1)) {
 			return IOUtils.toString(reader);
 		} catch (IOException e) {
 			throw new UncheckedIOException(e);

+ 10 - 3
main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/Masterkeys.java

@@ -16,6 +16,7 @@ import java.io.InputStream;
 import java.io.UncheckedIOException;
 import java.nio.ByteBuffer;
 import java.nio.channels.Channels;
+import java.nio.channels.ReadableByteChannel;
 
 import javax.inject.Inject;
 import javax.inject.Provider;
@@ -52,10 +53,14 @@ class Masterkeys {
 	public Cryptor decrypt(Folder vaultLocation, CharSequence passphrase) throws InvalidPassphraseException {
 		File masterkeyFile = vaultLocation.file(MASTERKEY_FILENAME);
 		Cryptor cryptor = cryptorProvider.get();
+		boolean success = false;
 		try {
 			readMasterKey(masterkeyFile, cryptor, passphrase);
-		} catch (UncheckedIOException e) {
-			cryptor.destroy();
+			success = true;
+		} finally {
+			if (!success) {
+				cryptor.destroy();
+			}
 		}
 		return cryptor;
 	}
@@ -86,7 +91,9 @@ class Masterkeys {
 	/* I/O */
 
 	private static void readMasterKey(File file, Cryptor cryptor, CharSequence passphrase) throws UncheckedIOException, InvalidPassphraseException {
-		try (InputStream in = Channels.newInputStream(file.openReadable())) {
+		try ( //
+				ReadableByteChannel channel = file.openReadable(); //
+				InputStream in = Channels.newInputStream(channel)) {
 			final byte[] fileContents = IOUtils.toByteArray(in);
 			cryptor.readKeysFromMasterkeyFile(fileContents, passphrase);
 		} catch (IOException e) {

+ 4 - 2
main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryReadableFile.java

@@ -64,8 +64,10 @@ class InMemoryReadableFile implements ReadableFile {
 
 	@Override
 	public void close() throws UncheckedIOException {
-		open.set(false);
-		readLock.unlock();
+		if (open.get()) {
+			open.set(false);
+			readLock.unlock();
+		}
 	}
 
 }

+ 22 - 6
main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFile.java

@@ -30,13 +30,21 @@ class NioFile extends NioNode implements File {
 	@Override
 	public ReadableFile openReadable() throws UncheckedIOException {
 		if (lock.getWriteHoldCount() > 0) {
-			throw new IllegalStateException("Current thread is currently writing this file");
+			throw new IllegalStateException("Current thread is currently writing " + path);
 		}
 		if (lock.getReadHoldCount() > 0) {
-			throw new IllegalStateException("Current thread is already reading this file");
+			throw new IllegalStateException("Current thread is already reading " + path);
 		}
 		lock.readLock().lock();
-		return instanceFactory.readableNioFile(path, sharedChannel, this::unlockReadLock);
+		ReadableFile result = null;
+		try {
+			result = instanceFactory.readableNioFile(path, sharedChannel, this::unlockReadLock);
+		} finally {
+			if (result == null) {
+				unlockReadLock();
+			}
+		}
+		return result;
 	}
 
 	private void unlockReadLock() {
@@ -46,13 +54,21 @@ class NioFile extends NioNode implements File {
 	@Override
 	public WritableFile openWritable() throws UncheckedIOException {
 		if (lock.getWriteHoldCount() > 0) {
-			throw new IllegalStateException("Current thread is already writing this file");
+			throw new IllegalStateException("Current thread is already writing " + path);
 		}
 		if (lock.getReadHoldCount() > 0) {
-			throw new IllegalStateException("Current thread is currently reading this file");
+			throw new IllegalStateException("Current thread is currently reading " + path);
 		}
 		lockWriteLock();
-		return instanceFactory.writableNioFile(fileSystem(), path, sharedChannel, this::unlockWriteLock);
+		WritableFile result = null;
+		try {
+			result = instanceFactory.writableNioFile(fileSystem(), path, sharedChannel, this::unlockWriteLock);
+		} finally {
+			if (result == null) {
+				unlockWriteLock();
+			}
+		}
+		return result;
 	}
 
 	// visible for testing

+ 13 - 8
main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioFileTest.java

@@ -99,10 +99,11 @@ public class NioFileTest {
 
 		@Test
 		public void testOpenReadableInvokedBeforeInvokingAfterCloseOperationThrowsIllegalStateException() {
+			when(instanceFactory.readableNioFile(same(path), same(channel), any())).thenReturn(mock(ReadableNioFile.class));
 			inTest.openReadable();
 
 			thrown.expect(IllegalStateException.class);
-			thrown.expectMessage("already reading this file");
+			thrown.expectMessage("already reading " + path);
 
 			inTest.openReadable();
 		}
@@ -111,7 +112,7 @@ public class NioFileTest {
 		public void testOpenReadableInvokedAfterAfterCloseOperationCreatesNewReadableFile() {
 			ReadableNioFile readableNioFile = mock(ReadableNioFile.class);
 			ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
-			when(instanceFactory.readableNioFile(same(path), same(channel), captor.capture())).thenReturn(null, readableNioFile);
+			when(instanceFactory.readableNioFile(same(path), same(channel), captor.capture())).thenReturn(mock(ReadableNioFile.class), readableNioFile);
 			inTest.openReadable();
 			captor.getValue().run();
 
@@ -122,10 +123,11 @@ public class NioFileTest {
 
 		@Test
 		public void testOpenReadableInvokedBeforeInvokingAfterCloseOperationOfOpenWritableThrowsIllegalStateException() {
+			when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), any())).thenReturn(mock(WritableNioFile.class));
 			inTest.openWritable();
 
 			thrown.expect(IllegalStateException.class);
-			thrown.expectMessage("currently writing this file");
+			thrown.expectMessage("currently writing " + path);
 
 			inTest.openReadable();
 		}
@@ -133,7 +135,7 @@ public class NioFileTest {
 		@Test
 		public void testOpenReadableInvokedAfterInvokingAfterCloseOperationWorks() {
 			ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
-			when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), captor.capture())).thenReturn(null);
+			when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), captor.capture())).thenReturn(mock(WritableNioFile.class));
 			inTest.openWritable();
 			captor.getValue().run();
 
@@ -154,7 +156,7 @@ public class NioFileTest {
 		public void testOpenWritableInvokedAfterAfterCloseOperationCreatesNewWritableFile() {
 			WritableNioFile writableNioFile = mock(WritableNioFile.class);
 			ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
-			when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), captor.capture())).thenReturn(null, writableNioFile);
+			when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), captor.capture())).thenReturn(mock(WritableNioFile.class), writableNioFile);
 			inTest.openWritable();
 			captor.getValue().run();
 
@@ -165,28 +167,31 @@ public class NioFileTest {
 
 		@Test
 		public void testOpenWritableInvokedBeforeInvokingAfterCloseOperationThrowsIllegalStateException() {
+			when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), any())).thenReturn(mock(WritableNioFile.class));
 			inTest.openWritable();
 
 			thrown.expect(IllegalStateException.class);
-			thrown.expectMessage("already writing this file");
+			thrown.expectMessage("already writing " + path);
 
 			inTest.openWritable();
 		}
 
 		@Test
 		public void testOpenWritableInvokedBeforeInvokingAfterCloseOperationFromOpenReadableThrowsIllegalStateException() {
+			when(instanceFactory.readableNioFile(same(path), same(channel), any())).thenReturn(mock(ReadableNioFile.class));
 			inTest.openReadable();
 
 			thrown.expect(IllegalStateException.class);
-			thrown.expectMessage("currently reading this file");
+			thrown.expectMessage("currently reading " + path);
 
 			inTest.openWritable();
 		}
 
 		@Test
 		public void testOpenWritableInvokedAfterInvokingAfterCloseOperationWorks() {
+			ReadableNioFile readableNioFile = mock(ReadableNioFile.class);
 			ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
-			when(instanceFactory.readableNioFile(same(path), same(channel), captor.capture())).thenReturn(null);
+			when(instanceFactory.readableNioFile(same(path), same(channel), captor.capture())).thenReturn(readableNioFile);
 			inTest.openReadable();
 			captor.getValue().run();
 

+ 4 - 0
main/ui/src/main/java/org/cryptomator/ui/CryptomatorComponent.java

@@ -14,6 +14,7 @@ import javax.inject.Singleton;
 
 import org.cryptomator.ui.controllers.MainController;
 import org.cryptomator.ui.settings.Localization;
+import org.cryptomator.ui.util.AsyncTaskService;
 import org.cryptomator.ui.util.DeferredCloser;
 
 import dagger.Component;
@@ -21,6 +22,9 @@ import dagger.Component;
 @Singleton
 @Component(modules = CryptomatorModule.class)
 interface CryptomatorComponent {
+
+	AsyncTaskService asyncTaskService();
+
 	ExecutorService executorService();
 
 	DeferredCloser deferredCloser();

+ 5 - 6
main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java

@@ -11,7 +11,6 @@ package org.cryptomator.ui.controllers;
 import java.net.URL;
 import java.util.Comparator;
 import java.util.Optional;
-import java.util.concurrent.ExecutorService;
 
 import javax.inject.Inject;
 
@@ -27,6 +26,7 @@ import org.cryptomator.ui.controls.SecPasswordField;
 import org.cryptomator.ui.model.Vault;
 import org.cryptomator.ui.settings.Localization;
 import org.cryptomator.ui.settings.Settings;
+import org.cryptomator.ui.util.AsyncTaskService;
 import org.fxmisc.easybind.EasyBind;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -56,7 +56,7 @@ public class UnlockController extends LocalizedFXMLViewController {
 	private static final Logger LOG = LoggerFactory.getLogger(UnlockController.class);
 
 	private final Application app;
-	private final ExecutorService exec;
+	private final AsyncTaskService asyncTaskService;
 	private final Lazy<FrontendFactory> frontendFactory;
 	private final Settings settings;
 	private final WindowsDriveLetters driveLetters;
@@ -65,10 +65,10 @@ public class UnlockController extends LocalizedFXMLViewController {
 	private Optional<UnlockListener> listener = Optional.empty();
 
 	@Inject
-	public UnlockController(Application app, Localization localization, ExecutorService exec, Lazy<FrontendFactory> frontendFactory, Settings settings, WindowsDriveLetters driveLetters) {
+	public UnlockController(Application app, Localization localization, AsyncTaskService asyncTaskService, Lazy<FrontendFactory> frontendFactory, Settings settings, WindowsDriveLetters driveLetters) {
 		super(localization);
 		this.app = app;
-		this.exec = exec;
+		this.asyncTaskService = asyncTaskService;
 		this.frontendFactory = frontendFactory;
 		this.settings = settings;
 		this.driveLetters = driveLetters;
@@ -275,8 +275,7 @@ public class UnlockController extends LocalizedFXMLViewController {
 		progressIndicator.setVisible(true);
 		downloadsPageLink.setVisible(false);
 		CharSequence password = passwordField.getCharacters();
-		exec.submit(() -> this.unlock(vault.get(), password));
-
+		asyncTaskService.asyncTaskOf(() -> this.unlock(vault.get(), password)).run();
 	}
 
 	private void unlock(Vault vault, CharSequence password) {

+ 16 - 30
main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockedController.java

@@ -10,7 +10,6 @@ package org.cryptomator.ui.controllers;
 
 import java.net.URL;
 import java.util.Optional;
-import java.util.concurrent.ExecutorService;
 
 import javax.inject.Inject;
 import javax.inject.Provider;
@@ -19,6 +18,7 @@ import org.cryptomator.frontend.CommandFailedException;
 import org.cryptomator.ui.model.Vault;
 import org.cryptomator.ui.settings.Localization;
 import org.cryptomator.ui.util.ActiveWindowStyleSupport;
+import org.cryptomator.ui.util.AsyncTaskService;
 import org.fxmisc.easybind.EasyBind;
 
 import javafx.animation.Animation;
@@ -52,16 +52,16 @@ public class UnlockedController extends LocalizedFXMLViewController {
 
 	private final Stage macWarningsWindow = new Stage();
 	private final MacWarningsController macWarningsController;
-	private final ExecutorService exec;
+	private final AsyncTaskService asyncTaskService;
 	private final ObjectProperty<Vault> vault = new SimpleObjectProperty<>();
 	private Optional<LockListener> listener = Optional.empty();
 	private Timeline ioAnimation;
 
 	@Inject
-	public UnlockedController(Localization localization, Provider<MacWarningsController> macWarningsControllerProvider, ExecutorService exec) {
+	public UnlockedController(Localization localization, Provider<MacWarningsController> macWarningsControllerProvider, AsyncTaskService asyncTaskService) {
 		super(localization);
 		this.macWarningsController = macWarningsControllerProvider.get();
-		this.exec = exec;
+		this.asyncTaskService = asyncTaskService;
 
 		macWarningsController.vault.bind(this.vault);
 	}
@@ -116,18 +116,14 @@ public class UnlockedController extends LocalizedFXMLViewController {
 
 	@FXML
 	private void didClickLockVault(ActionEvent event) {
-		exec.submit(() -> {
-			try {
-				vault.get().unmount();
-			} catch (CommandFailedException e) {
-				Platform.runLater(() -> {
-					messageLabel.setText(localization.getString("unlocked.label.unmountFailed"));
-				});
-				return;
-			}
+		asyncTaskService.asyncTaskOf(() -> {
+			vault.get().unmount();
 			vault.get().deactivateFrontend();
-			listener.ifPresent(this::invokeListenerLater);
-		});
+		}).onError(CommandFailedException.class, () -> {
+			messageLabel.setText(localization.getString("unlocked.label.unmountFailed"));
+		}).andFinally(() -> {
+			listener.ifPresent(listener -> listener.didLock(this));
+		}).run();
 	}
 
 	@FXML
@@ -142,15 +138,11 @@ public class UnlockedController extends LocalizedFXMLViewController {
 
 	@FXML
 	private void didClickRevealVault(ActionEvent event) {
-		exec.submit(() -> {
-			try {
-				vault.get().reveal();
-			} catch (CommandFailedException e) {
-				Platform.runLater(() -> {
-					messageLabel.setText(localization.getString("unlocked.label.revealFailed"));
-				});
-			}
-		});
+		asyncTaskService.asyncTaskOf(() -> {
+			vault.get().reveal();
+		}).onError(CommandFailedException.class, () -> {
+			messageLabel.setText(localization.getString("unlocked.label.revealFailed"));
+		}).run();
 	}
 
 	@FXML
@@ -258,12 +250,6 @@ public class UnlockedController extends LocalizedFXMLViewController {
 		this.listener = Optional.ofNullable(listener);
 	}
 
-	private void invokeListenerLater(LockListener listener) {
-		Platform.runLater(() -> {
-			listener.didLock(this);
-		});
-	}
-
 	@FunctionalInterface
 	interface LockListener {
 		void didLock(UnlockedController ctrl);

+ 16 - 25
main/ui/src/main/java/org/cryptomator/ui/controllers/UpgradeController.java

@@ -3,7 +3,6 @@ package org.cryptomator.ui.controllers;
 import java.net.URL;
 import java.util.Objects;
 import java.util.Optional;
-import java.util.concurrent.ExecutorService;
 
 import javax.inject.Inject;
 
@@ -13,11 +12,9 @@ import org.cryptomator.ui.model.UpgradeStrategy;
 import org.cryptomator.ui.model.UpgradeStrategy.UpgradeFailedException;
 import org.cryptomator.ui.model.Vault;
 import org.cryptomator.ui.settings.Localization;
+import org.cryptomator.ui.util.AsyncTaskService;
 import org.fxmisc.easybind.EasyBind;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
-import javafx.application.Platform;
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.SimpleObjectProperty;
 import javafx.event.ActionEvent;
@@ -28,19 +25,17 @@ import javafx.scene.control.ProgressIndicator;
 
 public class UpgradeController extends LocalizedFXMLViewController {
 
-	private static final Logger LOG = LoggerFactory.getLogger(UpgradeController.class);
-
 	final ObjectProperty<Vault> vault = new SimpleObjectProperty<>();
 	final ObjectProperty<Optional<UpgradeStrategy>> strategy = new SimpleObjectProperty<>();
 	private final UpgradeStrategies strategies;
-	private final ExecutorService exec;
+	private final AsyncTaskService asyncTaskService;
 	private Optional<UpgradeListener> listener = Optional.empty();
 
 	@Inject
-	public UpgradeController(Localization localization, UpgradeStrategies strategies, ExecutorService exec) {
+	public UpgradeController(Localization localization, UpgradeStrategies strategies, AsyncTaskService asyncTaskService) {
 		super(localization);
 		this.strategies = strategies;
-		this.exec = exec;
+		this.asyncTaskService = asyncTaskService;
 	}
 
 	@FXML
@@ -103,26 +98,22 @@ public class UpgradeController extends LocalizedFXMLViewController {
 		Vault v = Objects.requireNonNull(vault.getValue());
 		passwordField.setDisable(true);
 		progressIndicator.setVisible(true);
-		exec.submit(() -> {
-			if (!instruction.isApplicable(v)) {
-				LOG.error("No upgrade needed for " + v.path().getValue());
-				throw new IllegalStateException("No ugprade needed for " + v.path().getValue());
-			}
-			try {
-				instruction.upgrade(v, passwordField.getCharacters());
-				Platform.runLater(this::showNextUpgrade);
-			} catch (UpgradeFailedException e) {
-				Platform.runLater(() -> {
+		asyncTaskService //
+				.asyncTaskOf(() -> {
+					if (!instruction.isApplicable(v)) {
+						throw new IllegalStateException("No ugprade needed for " + v.path().getValue());
+					}
+					instruction.upgrade(v, passwordField.getCharacters());
+				}) //
+				.onSuccess(this::showNextUpgrade) //
+				.onError(UpgradeFailedException.class, e -> {
 					errorLabel.setText(e.getLocalizedMessage());
-				});
-			} finally {
-				Platform.runLater(() -> {
+				}) //
+				.andFinally(() -> {
 					progressIndicator.setVisible(false);
 					passwordField.setDisable(false);
 					passwordField.swipe();
-				});
-			}
-		});
+				}).run();
 	}
 
 	private void showNextUpgrade() {

+ 17 - 24
main/ui/src/main/java/org/cryptomator/ui/controllers/WelcomeController.java

@@ -8,14 +8,12 @@
  ******************************************************************************/
 package org.cryptomator.ui.controllers;
 
-import java.io.IOException;
 import java.io.InputStream;
 import java.net.URL;
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Optional;
-import java.util.concurrent.ExecutorService;
 
 import javax.inject.Inject;
 import javax.inject.Named;
@@ -31,6 +29,7 @@ import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang3.SystemUtils;
 import org.cryptomator.ui.settings.Localization;
 import org.cryptomator.ui.settings.Settings;
+import org.cryptomator.ui.util.AsyncTaskService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -54,15 +53,15 @@ public class WelcomeController extends LocalizedFXMLViewController {
 	private final Application app;
 	private final Settings settings;
 	private final Comparator<String> semVerComparator;
-	private final ExecutorService executor;
+	private final AsyncTaskService asyncTaskService;
 
 	@Inject
-	public WelcomeController(Application app, Localization localization, Settings settings, @Named("SemVer") Comparator<String> semVerComparator, ExecutorService executor) {
+	public WelcomeController(Application app, Localization localization, Settings settings, @Named("SemVer") Comparator<String> semVerComparator, AsyncTaskService asyncTaskService) {
 		super(localization);
 		this.app = app;
 		this.settings = settings;
 		this.semVerComparator = semVerComparator;
-		this.executor = executor;
+		this.asyncTaskService = asyncTaskService;
 	}
 
 	@FXML
@@ -82,7 +81,7 @@ public class WelcomeController extends LocalizedFXMLViewController {
 		if (areUpdatesManagedExternally()) {
 			checkForUpdatesContainer.setVisible(false);
 		} else if (settings.isCheckForUpdatesEnabled()) {
-			executor.execute(this::checkForUpdates);
+			this.checkForUpdates();
 		}
 	}
 
@@ -100,16 +99,14 @@ public class WelcomeController extends LocalizedFXMLViewController {
 	}
 
 	private void checkForUpdates() {
-		Platform.runLater(() -> {
-			checkForUpdatesStatus.setText(localization.getString("welcome.checkForUpdates.label.currentlyChecking"));
-			checkForUpdatesIndicator.setVisible(true);
-		});
-		final HttpClient client = new HttpClient();
-		final HttpMethod method = new GetMethod("https://cryptomator.org/downloads/latestVersion.json");
-		client.getParams().setParameter(HttpClientParams.USER_AGENT, "Cryptomator VersionChecker/" + applicationVersion().orElse("SNAPSHOT"));
-		client.getParams().setCookiePolicy(CookiePolicy.IGNORE_COOKIES);
-		client.getParams().setConnectionManagerTimeout(5000);
-		try {
+		checkForUpdatesStatus.setText(localization.getString("welcome.checkForUpdates.label.currentlyChecking"));
+		checkForUpdatesIndicator.setVisible(true);
+		asyncTaskService.asyncTaskOf(() -> {
+			final HttpClient client = new HttpClient();
+			final HttpMethod method = new GetMethod("https://cryptomator.org/downloads/latestVersion.json");
+			client.getParams().setParameter(HttpClientParams.USER_AGENT, "Cryptomator VersionChecker/" + applicationVersion().orElse("SNAPSHOT"));
+			client.getParams().setCookiePolicy(CookiePolicy.IGNORE_COOKIES);
+			client.getParams().setConnectionManagerTimeout(5000);
 			client.executeMethod(method);
 			final InputStream responseBodyStream = method.getResponseBodyAsStream();
 			if (method.getStatusCode() == HttpStatus.SC_OK && responseBodyStream != null) {
@@ -121,14 +118,10 @@ public class WelcomeController extends LocalizedFXMLViewController {
 					this.compareVersions(map);
 				}
 			}
-		} catch (IOException e) {
-			// no error handling required. Maybe next time the version check is successful.
-		} finally {
-			Platform.runLater(() -> {
-				checkForUpdatesStatus.setText("");
-				checkForUpdatesIndicator.setVisible(false);
-			});
-		}
+		}).andFinally(() -> {
+			checkForUpdatesStatus.setText("");
+			checkForUpdatesIndicator.setVisible(false);
+		}).run();
 	}
 
 	private Optional<String> applicationVersion() {

+ 174 - 0
main/ui/src/main/java/org/cryptomator/ui/util/AsyncTaskService.java

@@ -0,0 +1,174 @@
+package org.cryptomator.ui.util;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import org.cryptomator.common.ConsumerThrowingException;
+import org.cryptomator.common.RunnableThrowingException;
+import org.cryptomator.common.SupplierThrowingException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javafx.application.Platform;
+
+@Singleton
+public class AsyncTaskService {
+
+	private static final Logger LOG = LoggerFactory.getLogger(AsyncTaskService.class);
+
+	private final ExecutorService executor;
+
+	@Inject
+	public AsyncTaskService(ExecutorService executor) {
+		this.executor = executor;
+	}
+
+	public AsyncTaskWithoutSuccessHandler<Void> asyncTaskOf(RunnableThrowingException<?> task) {
+		return new AsyncTaskImpl<>(() -> {
+			task.run();
+			return null;
+		});
+	}
+
+	public <ResultType> AsyncTaskWithoutSuccessHandler<ResultType> asyncTaskOf(SupplierThrowingException<ResultType, ?> task) {
+		return new AsyncTaskImpl<>(task);
+	}
+
+	private class AsyncTaskImpl<ResultType> implements AsyncTaskWithoutSuccessHandler<ResultType> {
+
+		private final SupplierThrowingException<ResultType, ?> task;
+
+		private ConsumerThrowingException<ResultType, ?> successHandler = value -> {
+		};
+		private List<ErrorHandler<Throwable>> errorHandlers = new ArrayList<>();
+		private RunnableThrowingException<?> finallyHandler = () -> {
+		};
+
+		public AsyncTaskImpl(SupplierThrowingException<ResultType, ?> task) {
+			this.task = task;
+		}
+
+		@Override
+		public AsyncTaskWithoutErrorHandler onSuccess(ConsumerThrowingException<ResultType, ?> handler) {
+			successHandler = handler;
+			return this;
+		}
+
+		@Override
+		public AsyncTaskWithoutErrorHandler onSuccess(RunnableThrowingException<?> handler) {
+			return onSuccess(result -> handler.run());
+		}
+
+		@SuppressWarnings({"unchecked", "rawtypes"})
+		@Override
+		public <ErrorType extends Throwable> AsyncTaskWithoutErrorHandler onError(Class<ErrorType> type, ConsumerThrowingException<ErrorType, ?> handler) {
+			errorHandlers.add((ErrorHandler) new ErrorHandler<>(type, handler));
+			return this;
+		}
+
+		@Override
+		public <ErrorType extends Throwable> AsyncTaskWithoutErrorHandler onError(Class<ErrorType> type, RunnableThrowingException<?> handler) {
+			return onError(type, error -> handler.run());
+		}
+
+		@Override
+		public AsyncTask andFinally(RunnableThrowingException<?> handler) {
+			finallyHandler = handler;
+			return this;
+		}
+
+		@Override
+		public void run() {
+			errorHandlers.add(ErrorHandler.LOGGING_HANDLER);
+			executor.execute(() -> logExceptions(() -> {
+				try {
+					ResultType result = task.get();
+					Platform.runLater(() -> {
+						try {
+							successHandler.accept(result);
+						} catch (Throwable e) {
+							LOG.error("Uncaught exception", e);
+						}
+					});
+				} catch (Throwable e) {
+					ErrorHandler<Throwable> errorHandler = errorHandlerFor(e);
+					Platform.runLater(toRunnableLoggingException(() -> errorHandler.accept(e)));
+				} finally {
+					Platform.runLater(toRunnableLoggingException(finallyHandler));
+				}
+			}));
+		}
+
+		private ErrorHandler<Throwable> errorHandlerFor(Throwable e) {
+			return errorHandlers.stream().filter(handler -> handler.handles(e)).findFirst().get();
+		}
+
+	}
+
+	private static Runnable toRunnableLoggingException(RunnableThrowingException<?> delegate) {
+		return () -> logExceptions(delegate);
+	}
+
+	private static void logExceptions(RunnableThrowingException<?> delegate) {
+		try {
+			delegate.run();
+		} catch (Throwable e) {
+			LOG.error("Uncaught exception", e);
+		}
+	}
+
+	private static class ErrorHandler<ErrorType> implements ConsumerThrowingException<ErrorType, Throwable> {
+
+		public static final ErrorHandler<Throwable> LOGGING_HANDLER = new ErrorHandler<Throwable>(Throwable.class, error -> {
+			LOG.error("Uncaught exception", error);
+		});
+
+		private final Class<ErrorType> type;
+		private final ConsumerThrowingException<ErrorType, ?> delegate;
+
+		public ErrorHandler(Class<ErrorType> type, ConsumerThrowingException<ErrorType, ?> delegate) {
+			this.type = type;
+			this.delegate = delegate;
+		}
+
+		public boolean handles(Throwable error) {
+			return type.isInstance(error);
+		}
+
+		@Override
+		public void accept(ErrorType error) throws Throwable {
+			delegate.accept(error);
+		}
+
+	}
+
+	public interface AsyncTaskWithoutSuccessHandler<ResultType> extends AsyncTaskWithoutErrorHandler {
+
+		AsyncTaskWithoutErrorHandler onSuccess(ConsumerThrowingException<ResultType, ?> handler);
+
+		AsyncTaskWithoutErrorHandler onSuccess(RunnableThrowingException<?> handler);
+
+	}
+
+	public interface AsyncTaskWithoutErrorHandler extends AsyncTaskWithoutFinallyHandler {
+
+		<ErrorType extends Throwable> AsyncTaskWithoutErrorHandler onError(Class<ErrorType> type, ConsumerThrowingException<ErrorType, ?> handler);
+
+		<ErrorType extends Throwable> AsyncTaskWithoutErrorHandler onError(Class<ErrorType> type, RunnableThrowingException<?> handler);
+
+	}
+
+	public interface AsyncTaskWithoutFinallyHandler extends AsyncTask {
+
+		AsyncTask andFinally(RunnableThrowingException<?> handler);
+
+	}
+
+	public interface AsyncTask extends Runnable {
+	}
+
+}