mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-17 10:11:27 +00:00
App lifecycle fixes
This commit is contained in:
@@ -5,7 +5,6 @@
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.launcher;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.logging.DebugMode;
|
||||
import org.cryptomator.logging.LoggerConfiguration;
|
||||
@@ -91,7 +90,6 @@ public class Cryptomator {
|
||||
try {
|
||||
uiLauncher.launch();
|
||||
shutdownLatch.await();
|
||||
Platform.exit();
|
||||
LOG.info("UI shut down");
|
||||
return 0;
|
||||
} catch (InterruptedException e) {
|
||||
|
||||
@@ -21,8 +21,10 @@ import java.nio.file.FileSystems;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@Singleton
|
||||
@@ -40,7 +42,7 @@ class FileOpenRequestHandler {
|
||||
}
|
||||
|
||||
private void openFiles(OpenFilesEvent evt) {
|
||||
Stream<Path> pathsToOpen = evt.getFiles().stream().map(File::toPath);
|
||||
Collection<Path> pathsToOpen = evt.getFiles().stream().map(File::toPath).collect(Collectors.toList());
|
||||
AppLaunchEvent launchEvent = new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, pathsToOpen);
|
||||
tryToEnqueueFileOpenRequest(launchEvent);
|
||||
}
|
||||
@@ -51,16 +53,18 @@ class FileOpenRequestHandler {
|
||||
|
||||
// visible for testing
|
||||
void handleLaunchArgs(FileSystem fs, String[] args) {
|
||||
Stream<Path> pathsToOpen = Arrays.stream(args).map(str -> {
|
||||
Collection<Path> pathsToOpen = Arrays.stream(args).map(str -> {
|
||||
try {
|
||||
return fs.getPath(str);
|
||||
} catch (InvalidPathException e) {
|
||||
LOG.trace("Argument not a valid path: {}", str);
|
||||
return null;
|
||||
}
|
||||
}).filter(Objects::nonNull);
|
||||
AppLaunchEvent launchEvent = new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, pathsToOpen);
|
||||
tryToEnqueueFileOpenRequest(launchEvent);
|
||||
}).filter(Objects::nonNull).collect(Collectors.toList());
|
||||
if (!pathsToOpen.isEmpty()) {
|
||||
AppLaunchEvent launchEvent = new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, pathsToOpen);
|
||||
tryToEnqueueFileOpenRequest(launchEvent);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -27,7 +28,7 @@ class IpcProtocolImpl implements IpcProtocol {
|
||||
|
||||
@Override
|
||||
public void revealRunningApp() {
|
||||
launchEventQueue.add(new AppLaunchEvent(AppLaunchEvent.EventType.REVEAL_APP, Stream.empty()));
|
||||
launchEventQueue.add(new AppLaunchEvent(AppLaunchEvent.EventType.REVEAL_APP, Collections.emptyList()));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -15,16 +15,14 @@ import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class FileOpenRequestHandlerTest {
|
||||
|
||||
@@ -39,32 +37,30 @@ public class FileOpenRequestHandlerTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("./cryptomator.exe foo bar")
|
||||
public void testOpenArgsWithCorrectPaths() throws IOException {
|
||||
public void testOpenArgsWithCorrectPaths() {
|
||||
inTest.handleLaunchArgs(new String[]{"foo", "bar"});
|
||||
|
||||
AppLaunchEvent evt = queue.poll();
|
||||
Assertions.assertNotNull(evt);
|
||||
List<Path> paths = evt.getPathsToOpen().collect(Collectors.toList());
|
||||
Collection<Path> paths = evt.getPathsToOpen();
|
||||
MatcherAssert.assertThat(paths, CoreMatchers.hasItems(Paths.get("foo"), Paths.get("bar")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("./cryptomator.exe foo (with 'foo' being an invalid path)")
|
||||
public void testOpenArgsWithIncorrectPaths() throws IOException {
|
||||
public void testOpenArgsWithIncorrectPaths() {
|
||||
FileSystem fs = Mockito.mock(FileSystem.class);
|
||||
Mockito.when(fs.getPath("foo")).thenThrow(new InvalidPathException("foo", "foo is not a path"));
|
||||
inTest.handleLaunchArgs(fs, new String[]{"foo"});
|
||||
|
||||
AppLaunchEvent evt = queue.poll();
|
||||
Assertions.assertNotNull(evt);
|
||||
List<Path> paths = evt.getPathsToOpen().collect(Collectors.toList());
|
||||
Assertions.assertTrue(paths.isEmpty());
|
||||
Assertions.assertNull(evt);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("./cryptomator.exe foo (with full event queue)")
|
||||
public void testOpenArgsWithFullQueue() throws IOException {
|
||||
queue.add(new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, Stream.empty()));
|
||||
public void testOpenArgsWithFullQueue() {
|
||||
queue.add(new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, Collections.emptyList()));
|
||||
Assumptions.assumeTrue(queue.remainingCapacity() == 0);
|
||||
|
||||
inTest.handleLaunchArgs(new String[]{"foo"});
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
package org.cryptomator.ui.launcher;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class AppLaunchEvent {
|
||||
|
||||
private final Stream<Path> pathsToOpen;
|
||||
private final EventType type;
|
||||
private final Collection<Path> pathsToOpen;
|
||||
|
||||
public enum EventType {REVEAL_APP, OPEN_FILE}
|
||||
|
||||
public AppLaunchEvent(EventType type, Stream<Path> pathsToOpen) {
|
||||
public AppLaunchEvent(EventType type, Collection<Path> pathsToOpen) {
|
||||
this.type = type;
|
||||
this.pathsToOpen = pathsToOpen;
|
||||
}
|
||||
@@ -19,7 +20,7 @@ public class AppLaunchEvent {
|
||||
return type;
|
||||
}
|
||||
|
||||
public Stream<Path> getPathsToOpen() {
|
||||
public Collection<Path> getPathsToOpen() {
|
||||
return pathsToOpen;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
import java.awt.Desktop;
|
||||
import java.awt.EventQueue;
|
||||
import java.awt.desktop.QuitResponse;
|
||||
import java.util.EnumSet;
|
||||
import java.util.EventObject;
|
||||
@@ -31,14 +32,14 @@ public class AppLifecycleListener {
|
||||
private final FxApplicationStarter fxApplicationStarter;
|
||||
private final CountDownLatch shutdownLatch;
|
||||
private final ObservableList<Vault> vaults;
|
||||
private final AtomicBoolean allowSuddenTermination;
|
||||
private final AtomicBoolean allowQuitWithoutPrompt;
|
||||
|
||||
@Inject
|
||||
AppLifecycleListener(FxApplicationStarter fxApplicationStarter, @Named("shutdownLatch") CountDownLatch shutdownLatch, ShutdownHook shutdownHook, ObservableList<Vault> vaults) {
|
||||
this.fxApplicationStarter = fxApplicationStarter;
|
||||
this.shutdownLatch = shutdownLatch;
|
||||
this.vaults = vaults;
|
||||
this.allowSuddenTermination = new AtomicBoolean(true);
|
||||
this.allowQuitWithoutPrompt = new AtomicBoolean(true);
|
||||
vaults.addListener(this::vaultListChanged);
|
||||
|
||||
// register preferences shortcut
|
||||
@@ -51,11 +52,6 @@ public class AppLifecycleListener {
|
||||
Desktop.getDesktop().setQuitHandler(this::handleQuitRequest);
|
||||
}
|
||||
|
||||
// allow sudden termination
|
||||
if (Desktop.getDesktop().isSupported(Desktop.Action.APP_SUDDEN_TERMINATION)) {
|
||||
Desktop.getDesktop().enableSuddenTermination();
|
||||
}
|
||||
|
||||
shutdownHook.runOnShutdown(this::forceUnmountRemainingVaults);
|
||||
}
|
||||
|
||||
@@ -66,7 +62,7 @@ public class AppLifecycleListener {
|
||||
handleQuitRequest(null, new QuitResponse() {
|
||||
@Override
|
||||
public void performQuit() {
|
||||
shutdownLatch.countDown();
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -76,18 +72,37 @@ public class AppLifecycleListener {
|
||||
});
|
||||
}
|
||||
|
||||
private void handleQuitRequest(@SuppressWarnings("unused") EventObject e, QuitResponse response) {
|
||||
QuitResponse decoratedQuitResponse = decorateQuitResponse(response);
|
||||
if (allowQuitWithoutPrompt.get()) {
|
||||
decoratedQuitResponse.performQuit();
|
||||
} else {
|
||||
fxApplicationStarter.get(true).thenAccept(app -> app.showQuitWindow(decoratedQuitResponse));
|
||||
}
|
||||
}
|
||||
|
||||
private QuitResponse decorateQuitResponse(QuitResponse originalQuitResponse) {
|
||||
return new QuitResponse() {
|
||||
@Override
|
||||
public void performQuit() {
|
||||
Platform.exit(); // will be no-op, if JavaFX never started.
|
||||
shutdownLatch.countDown(); // main thread is waiting for this latch
|
||||
EventQueue.invokeLater(originalQuitResponse::performQuit); // this will eventually call System.exit(0)
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelQuit() {
|
||||
originalQuitResponse.cancelQuit();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void vaultListChanged(@SuppressWarnings("unused") Observable observable) {
|
||||
assert Platform.isFxApplicationThread();
|
||||
boolean allVaultsAllowTermination = vaults.stream().map(Vault::getState).allMatch(STATES_ALLOWING_TERMINATION::contains);
|
||||
boolean suddenTerminationChanged = allowSuddenTermination.compareAndSet(!allVaultsAllowTermination, allVaultsAllowTermination);
|
||||
if (suddenTerminationChanged && Desktop.getDesktop().isSupported(Desktop.Action.APP_SUDDEN_TERMINATION)) {
|
||||
if (allVaultsAllowTermination) {
|
||||
Desktop.getDesktop().enableSuddenTermination();
|
||||
LOG.debug("sudden termination enabled");
|
||||
} else {
|
||||
Desktop.getDesktop().disableSuddenTermination();
|
||||
LOG.debug("sudden termination disabled");
|
||||
}
|
||||
boolean suddenTerminationChanged = allowQuitWithoutPrompt.compareAndSet(!allVaultsAllowTermination, allVaultsAllowTermination);
|
||||
if (suddenTerminationChanged) {
|
||||
LOG.debug("Allow quitting without prompt: {}", allVaultsAllowTermination);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,14 +110,6 @@ public class AppLifecycleListener {
|
||||
fxApplicationStarter.get(true).thenAccept(app -> app.showPreferencesWindow(SelectedPreferencesTab.ANY));
|
||||
}
|
||||
|
||||
private void handleQuitRequest(@SuppressWarnings("unused") EventObject e, QuitResponse response) {
|
||||
if (allowSuddenTermination.get()) {
|
||||
response.performQuit(); // really?
|
||||
} else {
|
||||
fxApplicationStarter.get(true).thenAccept(app -> app.showQuitWindow(response));
|
||||
}
|
||||
}
|
||||
|
||||
private void forceUnmountRemainingVaults() {
|
||||
for (Vault vault : vaults) {
|
||||
if (vault.isUnlocked()) {
|
||||
|
||||
Reference in New Issue
Block a user