mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-04-22 18:46:53 -04:00
Merge branch 'develop' into feature/improve-health-check
# Conflicts: # src/main/java/org/cryptomator/ui/health/HealthCheckTask.java # src/main/java/org/cryptomator/ui/health/ReportWriter.java # src/main/resources/fxml/health_check_list.fxml # src/main/resources/fxml/vault_options.fxml
This commit is contained in:
62
src/main/java/module-info.java
Normal file
62
src/main/java/module-info.java
Normal file
@@ -0,0 +1,62 @@
|
||||
import org.cryptomator.integrations.autostart.AutoStartProvider;
|
||||
import org.cryptomator.integrations.keychain.KeychainAccessProvider;
|
||||
import org.cryptomator.integrations.tray.TrayIntegrationProvider;
|
||||
import org.cryptomator.integrations.uiappearance.UiAppearanceProvider;
|
||||
|
||||
module org.cryptomator.desktop {
|
||||
requires org.cryptomator.cryptofs;
|
||||
requires org.cryptomator.frontend.dokany;
|
||||
requires org.cryptomator.frontend.fuse;
|
||||
requires org.cryptomator.frontend.webdav;
|
||||
requires org.cryptomator.integrations.api;
|
||||
requires java.rmi;
|
||||
requires java.desktop;
|
||||
requires java.net.http;
|
||||
requires javafx.base;
|
||||
requires javafx.graphics;
|
||||
requires javafx.controls;
|
||||
requires javafx.fxml;
|
||||
requires com.tobiasdiez.easybind;
|
||||
requires com.google.common;
|
||||
requires com.google.gson;
|
||||
requires com.nulabinc.zxcvbn;
|
||||
requires org.slf4j;
|
||||
requires org.apache.commons.lang3;
|
||||
requires dagger;
|
||||
requires com.auth0.jwt;
|
||||
|
||||
/* TODO: filename-based modules: */
|
||||
requires static javax.inject; /* ugly dagger/guava crap */
|
||||
requires logback.classic;
|
||||
requires logback.core;
|
||||
|
||||
uses AutoStartProvider;
|
||||
uses KeychainAccessProvider;
|
||||
uses TrayIntegrationProvider;
|
||||
uses UiAppearanceProvider;
|
||||
|
||||
opens org.cryptomator.common.settings to com.google.gson;
|
||||
|
||||
opens org.cryptomator.launcher to java.rmi;
|
||||
|
||||
opens org.cryptomator.common to javafx.fxml;
|
||||
opens org.cryptomator.common.vaults to javafx.fxml;
|
||||
opens org.cryptomator.ui.addvaultwizard to javafx.fxml;
|
||||
opens org.cryptomator.ui.changepassword to javafx.fxml;
|
||||
opens org.cryptomator.ui.common to javafx.fxml;
|
||||
opens org.cryptomator.ui.controls to javafx.fxml;
|
||||
opens org.cryptomator.ui.forgetPassword to javafx.fxml;
|
||||
opens org.cryptomator.ui.fxapp to javafx.fxml;
|
||||
opens org.cryptomator.ui.health to javafx.fxml;
|
||||
opens org.cryptomator.ui.keyloading.masterkeyfile to javafx.fxml;
|
||||
opens org.cryptomator.ui.mainwindow to javafx.fxml;
|
||||
opens org.cryptomator.ui.migration to javafx.fxml;
|
||||
opens org.cryptomator.ui.preferences to javafx.fxml;
|
||||
opens org.cryptomator.ui.quit to javafx.fxml;
|
||||
opens org.cryptomator.ui.recoverykey to javafx.fxml;
|
||||
opens org.cryptomator.ui.removevault to javafx.fxml;
|
||||
opens org.cryptomator.ui.stats to javafx.fxml;
|
||||
opens org.cryptomator.ui.unlock to javafx.fxml;
|
||||
opens org.cryptomator.ui.vaultoptions to javafx.fxml;
|
||||
opens org.cryptomator.ui.wrongfilealert to javafx.fxml;
|
||||
}
|
||||
@@ -33,7 +33,7 @@ public class Environment {
|
||||
LOG.debug("user.region: {}", System.getProperty("user.region"));
|
||||
LOG.debug("logback.configurationFile: {}", System.getProperty("logback.configurationFile"));
|
||||
LOG.debug("cryptomator.settingsPath: {}", System.getProperty("cryptomator.settingsPath"));
|
||||
LOG.debug("cryptomator.ipcPortPath: {}", System.getProperty("cryptomator.ipcPortPath"));
|
||||
LOG.debug("cryptomator.ipcSocketPath: {}", System.getProperty("cryptomator.ipcSocketPath"));
|
||||
LOG.debug("cryptomator.keychainPath: {}", System.getProperty("cryptomator.keychainPath"));
|
||||
LOG.debug("cryptomator.logDir: {}", System.getProperty("cryptomator.logDir"));
|
||||
LOG.debug("cryptomator.mountPointsDir: {}", System.getProperty("cryptomator.mountPointsDir"));
|
||||
@@ -51,8 +51,8 @@ public class Environment {
|
||||
return getPaths("cryptomator.settingsPath");
|
||||
}
|
||||
|
||||
public Stream<Path> getIpcPortPath() {
|
||||
return getPaths("cryptomator.ipcPortPath");
|
||||
public Stream<Path> ipcSocketPath() {
|
||||
return getPaths("cryptomator.ipcSocketPath");
|
||||
}
|
||||
|
||||
public Stream<Path> getKeychainPath() {
|
||||
|
||||
8
src/main/java/org/cryptomator/common/Nullable.java
Normal file
8
src/main/java/org/cryptomator/common/Nullable.java
Normal file
@@ -0,0 +1,8 @@
|
||||
package org.cryptomator.common;
|
||||
|
||||
/**
|
||||
* Replacement for JSR-305 to avoid runtime dependencies. Used in Dagger components.
|
||||
*/
|
||||
public @interface Nullable {
|
||||
|
||||
}
|
||||
@@ -43,7 +43,7 @@ public class VaultSettings {
|
||||
private static final Random RNG = new Random();
|
||||
|
||||
private final String id;
|
||||
private final ObjectProperty<Path> path = new SimpleObjectProperty();
|
||||
private final ObjectProperty<Path> path = new SimpleObjectProperty<>();
|
||||
private final StringProperty displayName = new SimpleStringProperty();
|
||||
private final StringProperty winDriveLetter = new SimpleStringProperty();
|
||||
private final BooleanProperty unlockAfterStartup = new SimpleBooleanProperty(DEFAULT_UNLOCK_AFTER_STARTUP);
|
||||
|
||||
@@ -7,10 +7,10 @@ package org.cryptomator.common.vaults;
|
||||
|
||||
import dagger.BindsInstance;
|
||||
import dagger.Subcomponent;
|
||||
import org.cryptomator.common.Nullable;
|
||||
import org.cryptomator.common.mountpoint.MountPointChooserModule;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Named;
|
||||
|
||||
@PerVault
|
||||
|
||||
@@ -8,6 +8,7 @@ package org.cryptomator.common.vaults;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.Nullable;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.common.settings.VolumeImpl;
|
||||
@@ -15,7 +16,6 @@ import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Named;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
|
||||
65
src/main/java/org/cryptomator/ipc/Client.java
Normal file
65
src/main/java/org/cryptomator/ipc/Client.java
Normal file
@@ -0,0 +1,65 @@
|
||||
package org.cryptomator.ipc;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.UnixDomainSocketAddress;
|
||||
import java.nio.channels.SocketChannel;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
class Client implements IpcCommunicator {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Client.class);
|
||||
|
||||
private final SocketChannel socketChannel;
|
||||
|
||||
private Client(SocketChannel socketChannel) {
|
||||
this.socketChannel = socketChannel;
|
||||
}
|
||||
|
||||
public static Client create(Path socketPath) throws IOException {
|
||||
var address = UnixDomainSocketAddress.of(socketPath);
|
||||
var socketChannel = SocketChannel.open(address);
|
||||
LOG.info("Connected to IPC server on socket {}", socketPath);
|
||||
return new Client(socketChannel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isClient() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void listen(IpcMessageListener listener, Executor executor) {
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
while (socketChannel.isConnected()) {
|
||||
var msg = IpcMessage.receive(socketChannel);
|
||||
listener.handleMessage(msg);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to read IPC message", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(IpcMessage message, Executor executor) {
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
message.send(socketChannel);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to send IPC message", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
socketChannel.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.cryptomator.ipc;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Splitter;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
record HandleLaunchArgsMessage(List<String> args) implements IpcMessage {
|
||||
|
||||
private static final char DELIMITER = '\n';
|
||||
|
||||
public static HandleLaunchArgsMessage decode(ByteBuffer encoded) {
|
||||
var str = StandardCharsets.UTF_8.decode(encoded).toString();
|
||||
var args = Splitter.on(DELIMITER).omitEmptyStrings().splitToList(str);
|
||||
return new HandleLaunchArgsMessage(args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageType getMessageType() {
|
||||
return MessageType.HANDLE_LAUNCH_ARGS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer encodePayload() {
|
||||
var str = Joiner.on(DELIMITER).join(args);
|
||||
return StandardCharsets.UTF_8.encode(str);
|
||||
}
|
||||
}
|
||||
96
src/main/java/org/cryptomator/ipc/IpcCommunicator.java
Normal file
96
src/main/java/org/cryptomator/ipc/IpcCommunicator.java
Normal file
@@ -0,0 +1,96 @@
|
||||
package org.cryptomator.ipc;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
public interface IpcCommunicator extends Closeable {
|
||||
|
||||
Logger LOG = LoggerFactory.getLogger(IpcCommunicator.class);
|
||||
|
||||
/**
|
||||
* Attempts to establish a socket connection via one of the given paths.
|
||||
* <p>
|
||||
* If no connection to an existing sockets can be established, a new socket is created for the first given path.
|
||||
* <p>
|
||||
* If this fails as well, a fallback communicator is returned that allows process-internal communication mocking the API
|
||||
* that would have been used for IPC.
|
||||
*
|
||||
* @param socketPaths The socket path(s)
|
||||
* @return A communicator object that allows sending and receiving messages
|
||||
*/
|
||||
static IpcCommunicator create(Iterable<Path> socketPaths) {
|
||||
Preconditions.checkArgument(socketPaths.iterator().hasNext(), "socketPaths must contain at least one element");
|
||||
for (var p : socketPaths) {
|
||||
try {
|
||||
var attr = Files.readAttributes(p, BasicFileAttributes.class);
|
||||
if (attr.isOther()) {
|
||||
return Client.create(p);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// attempt next socket path
|
||||
}
|
||||
}
|
||||
// Didn't get any connection yet? I.e. we're the first app instance, so let's launch a server:
|
||||
try {
|
||||
return Server.create(socketPaths.iterator().next());
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Failed to create IPC server", e);
|
||||
return new LoopbackCommunicator();
|
||||
}
|
||||
}
|
||||
|
||||
boolean isClient();
|
||||
|
||||
/**
|
||||
* Listens to incoming messages until the connection gets closed.
|
||||
* @param listener The listener that should be notified of incoming messages
|
||||
* @param executor An executor on which to listen. Listening will block, so you might want to use a background thread.
|
||||
* @return
|
||||
*/
|
||||
void listen(IpcMessageListener listener, Executor executor);
|
||||
|
||||
/**
|
||||
* Sends the given message.
|
||||
*
|
||||
* @param message The message to send
|
||||
* @param executor An executor used to send the message. Sending will block, so you might want to use a background thread.
|
||||
*/
|
||||
void send(IpcMessage message, Executor executor);
|
||||
|
||||
default void sendRevealRunningApp() {
|
||||
send(new RevealRunningAppMessage(), MoreExecutors.directExecutor());
|
||||
}
|
||||
|
||||
default void sendHandleLaunchargs(List<String> args) {
|
||||
send(new HandleLaunchArgsMessage(args), MoreExecutors.directExecutor());
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources.
|
||||
*
|
||||
* @implSpec Must be idempotent
|
||||
* @throws IOException In case of I/O errors.
|
||||
*/
|
||||
@Override
|
||||
void close() throws IOException;
|
||||
|
||||
default void closeUnchecked() throws UncheckedIOException {
|
||||
try {
|
||||
close();
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
68
src/main/java/org/cryptomator/ipc/IpcMessage.java
Normal file
68
src/main/java/org/cryptomator/ipc/IpcMessage.java
Normal file
@@ -0,0 +1,68 @@
|
||||
package org.cryptomator.ipc;
|
||||
|
||||
import org.cryptomator.cryptolib.common.ByteBuffers;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
import java.nio.channels.WritableByteChannel;
|
||||
import java.util.function.Function;
|
||||
|
||||
// TODO make sealed, remove enum
|
||||
interface IpcMessage {
|
||||
|
||||
enum MessageType {
|
||||
REVEAL_RUNNING_APP(RevealRunningAppMessage::decode),
|
||||
HANDLE_LAUNCH_ARGS(HandleLaunchArgsMessage::decode);
|
||||
|
||||
private final Function<ByteBuffer, IpcMessage> decoder;
|
||||
|
||||
MessageType(Function<ByteBuffer, IpcMessage> decoder) {
|
||||
this.decoder = decoder;
|
||||
}
|
||||
|
||||
static MessageType forOrdinal(int ordinal) {
|
||||
try {
|
||||
return values()[ordinal];
|
||||
} catch (IndexOutOfBoundsException e) {
|
||||
throw new IllegalArgumentException("No such message type: " + ordinal, e);
|
||||
}
|
||||
}
|
||||
|
||||
IpcMessage decodePayload(ByteBuffer payload) {
|
||||
return decoder.apply(payload);
|
||||
}
|
||||
}
|
||||
|
||||
MessageType getMessageType();
|
||||
|
||||
ByteBuffer encodePayload();
|
||||
|
||||
static IpcMessage receive(ReadableByteChannel channel) throws IOException {
|
||||
var header = ByteBuffer.allocate(2 * Integer.BYTES);
|
||||
if (ByteBuffers.fill(channel, header) < header.capacity()) {
|
||||
throw new EOFException();
|
||||
}
|
||||
header.flip();
|
||||
int typeNo = header.getInt();
|
||||
int length = header.getInt();
|
||||
MessageType type = MessageType.forOrdinal(typeNo);
|
||||
var payload = ByteBuffer.allocate(length);
|
||||
ByteBuffers.fill(channel, payload);
|
||||
payload.flip();
|
||||
return type.decodePayload(payload);
|
||||
}
|
||||
|
||||
default void send(WritableByteChannel channel) throws IOException {
|
||||
var payload = encodePayload();
|
||||
var buf = ByteBuffer.allocate(2 * Integer.BYTES + payload.remaining());
|
||||
buf.putInt(getMessageType().ordinal()); // message type
|
||||
buf.putInt(payload.remaining()); // message length
|
||||
buf.put(payload); // message
|
||||
buf.flip();
|
||||
while (buf.hasRemaining()) {
|
||||
channel.write(buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/main/java/org/cryptomator/ipc/IpcMessageListener.java
Normal file
19
src/main/java/org/cryptomator/ipc/IpcMessageListener.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package org.cryptomator.ipc;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface IpcMessageListener {
|
||||
|
||||
default void handleMessage(IpcMessage message) {
|
||||
if (message instanceof RevealRunningAppMessage) {
|
||||
revealRunningApp();
|
||||
} else if (message instanceof HandleLaunchArgsMessage m) {
|
||||
handleLaunchArgs(m.args());
|
||||
}
|
||||
}
|
||||
|
||||
void revealRunningApp();
|
||||
|
||||
void handleLaunchArgs(List<String> args);
|
||||
|
||||
}
|
||||
50
src/main/java/org/cryptomator/ipc/LoopbackCommunicator.java
Normal file
50
src/main/java/org/cryptomator/ipc/LoopbackCommunicator.java
Normal file
@@ -0,0 +1,50 @@
|
||||
package org.cryptomator.ipc;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.TransferQueue;
|
||||
|
||||
class LoopbackCommunicator implements IpcCommunicator {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(LoopbackCommunicator.class);
|
||||
|
||||
private final TransferQueue<IpcMessage> transferQueue = new LinkedTransferQueue<>();
|
||||
|
||||
@Override
|
||||
public boolean isClient() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void listen(IpcMessageListener listener, Executor executor) {
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
var msg = transferQueue.take();
|
||||
listener.handleMessage(msg);
|
||||
} catch (InterruptedException e) {
|
||||
LOG.error("Failed to read IPC message", e);
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(IpcMessage message, Executor executor) {
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
transferQueue.put(message);
|
||||
} catch (InterruptedException e) {
|
||||
LOG.error("Failed to send IPC message", e);
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.cryptomator.ipc;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public record RevealRunningAppMessage() implements IpcMessage {
|
||||
|
||||
static RevealRunningAppMessage decode(ByteBuffer ignored) {
|
||||
return new RevealRunningAppMessage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageType getMessageType() {
|
||||
return MessageType.REVEAL_RUNNING_APP;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer encodePayload() {
|
||||
return ByteBuffer.allocate(0);
|
||||
}
|
||||
}
|
||||
82
src/main/java/org/cryptomator/ipc/Server.java
Normal file
82
src/main/java/org/cryptomator/ipc/Server.java
Normal file
@@ -0,0 +1,82 @@
|
||||
package org.cryptomator.ipc;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.net.StandardProtocolFamily;
|
||||
import java.net.UnixDomainSocketAddress;
|
||||
import java.nio.channels.AsynchronousCloseException;
|
||||
import java.nio.channels.ClosedChannelException;
|
||||
import java.nio.channels.ServerSocketChannel;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
class Server implements IpcCommunicator {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Server.class);
|
||||
|
||||
private final ServerSocketChannel serverSocketChannel;
|
||||
private final Path socketPath;
|
||||
|
||||
private Server(ServerSocketChannel serverSocketChannel, Path socketPath) {
|
||||
this.serverSocketChannel = serverSocketChannel;
|
||||
this.socketPath = socketPath;
|
||||
}
|
||||
|
||||
public static Server create(Path socketPath) throws IOException {
|
||||
var address = UnixDomainSocketAddress.of(socketPath);
|
||||
var serverSocketChannel = ServerSocketChannel.open(StandardProtocolFamily.UNIX);
|
||||
serverSocketChannel.bind(address);
|
||||
LOG.info("Spawning IPC server listening on socket {}", socketPath);
|
||||
return new Server(serverSocketChannel, socketPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isClient() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void listen(IpcMessageListener listener, Executor executor) {
|
||||
executor.execute(() -> {
|
||||
while (serverSocketChannel.isOpen()) {
|
||||
try (var ch = serverSocketChannel.accept()) {
|
||||
while (ch.isConnected()) {
|
||||
var msg = IpcMessage.receive(ch);
|
||||
listener.handleMessage(msg);
|
||||
}
|
||||
} catch (AsynchronousCloseException e) {
|
||||
return; // serverSocketChannel closed or listener interrupted
|
||||
} catch (EOFException | ClosedChannelException e) {
|
||||
// continue with next connected client
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to read IPC message", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(IpcMessage message, Executor executor) {
|
||||
executor.execute(() -> {
|
||||
try (var ch = serverSocketChannel.accept()) {
|
||||
message.send(ch);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to send IPC message", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
try {
|
||||
serverSocketChannel.close();
|
||||
} finally {
|
||||
Files.deleteIfExists(socketPath);
|
||||
LOG.debug("IPC server closed");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,12 @@
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.launcher;
|
||||
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
import dagger.Lazy;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.common.ShutdownHook;
|
||||
import org.cryptomator.ipc.IpcCommunicator;
|
||||
import org.cryptomator.logging.DebugMode;
|
||||
import org.cryptomator.logging.LoggerConfiguration;
|
||||
import org.cryptomator.ui.launcher.UiLauncher;
|
||||
@@ -16,8 +21,10 @@ import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
@Singleton
|
||||
public class Cryptomator {
|
||||
@@ -29,23 +36,28 @@ public class Cryptomator {
|
||||
|
||||
private final LoggerConfiguration logConfig;
|
||||
private final DebugMode debugMode;
|
||||
private final IpcFactory ipcFactory;
|
||||
private final Environment env;
|
||||
private final Lazy<IpcMessageHandler> ipcMessageHandler;
|
||||
private final Optional<String> applicationVersion;
|
||||
private final CountDownLatch shutdownLatch;
|
||||
private final UiLauncher uiLauncher;
|
||||
private final ShutdownHook shutdownHook;
|
||||
private final Lazy<UiLauncher> uiLauncher;
|
||||
|
||||
@Inject
|
||||
Cryptomator(LoggerConfiguration logConfig, DebugMode debugMode, IpcFactory ipcFactory, @Named("applicationVersion") Optional<String> applicationVersion, @Named("shutdownLatch") CountDownLatch shutdownLatch, UiLauncher uiLauncher) {
|
||||
Cryptomator(LoggerConfiguration logConfig, DebugMode debugMode, Environment env, Lazy<IpcMessageHandler> ipcMessageHandler, @Named("applicationVersion") Optional<String> applicationVersion, @Named("shutdownLatch") CountDownLatch shutdownLatch, ShutdownHook shutdownHook, Lazy<UiLauncher> uiLauncher) {
|
||||
this.logConfig = logConfig;
|
||||
this.debugMode = debugMode;
|
||||
this.ipcFactory = ipcFactory;
|
||||
this.env = env;
|
||||
this.ipcMessageHandler = ipcMessageHandler;
|
||||
this.applicationVersion = applicationVersion;
|
||||
this.shutdownLatch = shutdownLatch;
|
||||
this.shutdownHook = shutdownHook;
|
||||
this.uiLauncher = uiLauncher;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
int exitCode = CRYPTOMATOR_COMPONENT.application().run(args);
|
||||
LOG.info("Exit {}", exitCode);
|
||||
System.exit(exitCode); // end remaining non-daemon threads.
|
||||
}
|
||||
|
||||
@@ -64,19 +76,24 @@ public class Cryptomator {
|
||||
* Attempts to create an IPC connection to a running Cryptomator instance and sends it the given args.
|
||||
* If no external process could be reached, the args will be handled by the loopback IPC endpoint.
|
||||
*/
|
||||
try (IpcFactory.IpcEndpoint endpoint = ipcFactory.create()) {
|
||||
endpoint.getRemote().handleLaunchArgs(args); // if we are the server, getRemote() returns self.
|
||||
if (endpoint.isConnectedToRemote()) {
|
||||
endpoint.getRemote().revealRunningApp();
|
||||
try (var communicator = IpcCommunicator.create(env.ipcSocketPath().toList())) {
|
||||
if (communicator.isClient()) {
|
||||
communicator.sendHandleLaunchargs(List.of(args));
|
||||
communicator.sendRevealRunningApp();
|
||||
LOG.info("Found running application instance. Shutting down...");
|
||||
return 2;
|
||||
} else {
|
||||
shutdownHook.runOnShutdown(communicator::closeUnchecked);
|
||||
var executor = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("IPC-%d").build());
|
||||
var msgHandler = ipcMessageHandler.get();
|
||||
msgHandler.handleLaunchArgs(List.of(args));
|
||||
communicator.listen(msgHandler, executor);
|
||||
LOG.debug("Did not find running application instance. Launching GUI...");
|
||||
return runGuiApplication();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to initiate inter-process communication.", e);
|
||||
return runGuiApplication();
|
||||
} catch (Throwable e) {
|
||||
LOG.error("Running application failed", e);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +105,7 @@ public class Cryptomator {
|
||||
*/
|
||||
private int runGuiApplication() {
|
||||
try {
|
||||
uiLauncher.launch();
|
||||
uiLauncher.get().launch();
|
||||
shutdownLatch.await();
|
||||
LOG.info("UI shut down");
|
||||
return 0;
|
||||
@@ -98,5 +115,4 @@ public class Cryptomator {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -46,13 +47,13 @@ class FileOpenRequestHandler {
|
||||
tryToEnqueueFileOpenRequest(launchEvent);
|
||||
}
|
||||
|
||||
public void handleLaunchArgs(String[] args) {
|
||||
public void handleLaunchArgs(List<String> args) {
|
||||
handleLaunchArgs(FileSystems.getDefault(), args);
|
||||
}
|
||||
|
||||
// visible for testing
|
||||
void handleLaunchArgs(FileSystem fs, String[] args) {
|
||||
Collection<Path> pathsToOpen = Arrays.stream(args).map(str -> {
|
||||
void handleLaunchArgs(FileSystem fs, List<String> args) {
|
||||
Collection<Path> pathsToOpen = args.stream().map(str -> {
|
||||
try {
|
||||
return fs.getPath(str);
|
||||
} catch (InvalidPathException e) {
|
||||
|
||||
@@ -1,258 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the accompanying LICENSE file.
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.launcher;
|
||||
|
||||
import com.google.common.io.MoreFiles;
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
import java.nio.channels.WritableByteChannel;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.rmi.NotBoundException;
|
||||
import java.rmi.registry.LocateRegistry;
|
||||
import java.rmi.registry.Registry;
|
||||
import java.rmi.server.RMIClientSocketFactory;
|
||||
import java.rmi.server.RMIServerSocketFactory;
|
||||
import java.rmi.server.RMISocketFactory;
|
||||
import java.rmi.server.UnicastRemoteObject;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* First running application on a machine opens a server socket. Further processes will connect as clients.
|
||||
*/
|
||||
@Singleton
|
||||
class IpcFactory {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(IpcFactory.class);
|
||||
private static final String RMI_NAME = "Cryptomator";
|
||||
|
||||
private final List<Path> portFilePaths;
|
||||
private final IpcProtocolImpl ipcHandler;
|
||||
|
||||
@Inject
|
||||
public IpcFactory(Environment env, IpcProtocolImpl ipcHandler) {
|
||||
this.portFilePaths = env.getIpcPortPath().collect(Collectors.toUnmodifiableList());
|
||||
this.ipcHandler = ipcHandler;
|
||||
}
|
||||
|
||||
public IpcEndpoint create() {
|
||||
if (portFilePaths.isEmpty()) {
|
||||
LOG.warn("No IPC port file path specified.");
|
||||
return new SelfEndpoint(ipcHandler);
|
||||
} else {
|
||||
System.setProperty("java.rmi.server.hostname", "localhost");
|
||||
return attemptClientConnection().or(this::createServerEndpoint).orElseGet(() -> new SelfEndpoint(ipcHandler));
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<IpcEndpoint> attemptClientConnection() {
|
||||
for (Path portFilePath : portFilePaths) {
|
||||
try {
|
||||
int port = readPort(portFilePath);
|
||||
LOG.debug("[Client] Connecting to port {}...", port);
|
||||
Registry registry = LocateRegistry.getRegistry("localhost", port, new ClientSocketFactory());
|
||||
IpcProtocol remoteInterface = (IpcProtocol) registry.lookup(RMI_NAME);
|
||||
return Optional.of(new ClientEndpoint(remoteInterface));
|
||||
} catch (NotBoundException | IOException e) {
|
||||
LOG.debug("[Client] Failed to connect.");
|
||||
// continue with next portFilePath...
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private int readPort(Path portFilePath) throws IOException {
|
||||
try (ReadableByteChannel ch = Files.newByteChannel(portFilePath, StandardOpenOption.READ)) {
|
||||
LOG.debug("[Client] Reading IPC port from {}", portFilePath);
|
||||
ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES);
|
||||
if (ch.read(buf) == Integer.BYTES) {
|
||||
buf.flip();
|
||||
return buf.getInt();
|
||||
} else {
|
||||
throw new IOException("Invalid IPC port file.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<IpcEndpoint> createServerEndpoint() {
|
||||
assert !portFilePaths.isEmpty();
|
||||
Path portFilePath = portFilePaths.get(0);
|
||||
try {
|
||||
ServerSocket socket = new ServerSocket(0, Byte.MAX_VALUE, InetAddress.getByName("localhost"));
|
||||
RMIClientSocketFactory csf = RMISocketFactory.getDefaultSocketFactory();
|
||||
SingletonServerSocketFactory ssf = new SingletonServerSocketFactory(socket);
|
||||
Registry registry = LocateRegistry.createRegistry(0, csf, ssf);
|
||||
UnicastRemoteObject.exportObject(ipcHandler, 0);
|
||||
registry.rebind(RMI_NAME, ipcHandler);
|
||||
writePort(portFilePath, socket.getLocalPort());
|
||||
return Optional.of(new ServerEndpoint(ipcHandler, socket, registry, portFilePath));
|
||||
} catch (IOException e) {
|
||||
LOG.warn("[Server] Failed to create IPC server.", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private void writePort(Path portFilePath, int port) throws IOException {
|
||||
ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES);
|
||||
buf.putInt(port);
|
||||
buf.flip();
|
||||
MoreFiles.createParentDirectories(portFilePath);
|
||||
try (WritableByteChannel ch = Files.newByteChannel(portFilePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
|
||||
if (ch.write(buf) != Integer.BYTES) {
|
||||
throw new IOException("Did not write expected number of bytes.");
|
||||
}
|
||||
}
|
||||
LOG.debug("[Server] Wrote IPC port {} to {}", port, portFilePath);
|
||||
}
|
||||
|
||||
interface IpcEndpoint extends Closeable {
|
||||
|
||||
boolean isConnectedToRemote();
|
||||
|
||||
IpcProtocol getRemote();
|
||||
|
||||
}
|
||||
|
||||
static class SelfEndpoint implements IpcEndpoint {
|
||||
|
||||
protected final IpcProtocol remoteObject;
|
||||
|
||||
SelfEndpoint(IpcProtocol remoteObject) {
|
||||
this.remoteObject = remoteObject;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConnectedToRemote() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IpcProtocol getRemote() {
|
||||
return remoteObject;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
static class ClientEndpoint implements IpcEndpoint {
|
||||
|
||||
private final IpcProtocol remoteInterface;
|
||||
|
||||
public ClientEndpoint(IpcProtocol remoteInterface) {
|
||||
this.remoteInterface = remoteInterface;
|
||||
}
|
||||
|
||||
public IpcProtocol getRemote() {
|
||||
return remoteInterface;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConnectedToRemote() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ServerEndpoint extends SelfEndpoint {
|
||||
|
||||
private final ServerSocket socket;
|
||||
private final Registry registry;
|
||||
private final Path portFilePath;
|
||||
|
||||
private ServerEndpoint(IpcProtocol remoteObject, ServerSocket socket, Registry registry, Path portFilePath) {
|
||||
super(remoteObject);
|
||||
this.socket = socket;
|
||||
this.registry = registry;
|
||||
this.portFilePath = portFilePath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
registry.unbind(RMI_NAME);
|
||||
UnicastRemoteObject.unexportObject(remoteObject, true);
|
||||
socket.close();
|
||||
Files.deleteIfExists(portFilePath);
|
||||
LOG.debug("[Server] Shut down");
|
||||
} catch (NotBoundException | IOException e) {
|
||||
LOG.warn("[Server] Error shutting down:", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Always returns the same pre-constructed server socket.
|
||||
*/
|
||||
private static class SingletonServerSocketFactory implements RMIServerSocketFactory {
|
||||
|
||||
private final ServerSocket socket;
|
||||
|
||||
public SingletonServerSocketFactory(ServerSocket socket) {
|
||||
this.socket = socket;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized ServerSocket createServerSocket(int port) throws IOException {
|
||||
if (port != 0) {
|
||||
throw new IllegalArgumentException("This factory doesn't support specific ports.");
|
||||
}
|
||||
return this.socket;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates client sockets with short timeouts.
|
||||
*/
|
||||
private static class ClientSocketFactory implements RMIClientSocketFactory {
|
||||
|
||||
@Override
|
||||
public Socket createSocket(String host, int port) throws IOException {
|
||||
return new SocketWithFixedTimeout(host, port, 1000);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class SocketWithFixedTimeout extends Socket {
|
||||
|
||||
public SocketWithFixedTimeout(String host, int port, int timeoutInMs) throws UnknownHostException, IOException {
|
||||
super(host, port);
|
||||
super.setSoTimeout(timeoutInMs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void setSoTimeout(int timeout) throws SocketException {
|
||||
// do nothing, timeout is fixed
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.cryptomator.launcher;
|
||||
|
||||
import org.cryptomator.ipc.IpcMessageListener;
|
||||
import org.cryptomator.ui.launcher.AppLaunchEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -7,20 +8,20 @@ import org.slf4j.LoggerFactory;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
|
||||
@Singleton
|
||||
class IpcProtocolImpl implements IpcProtocol {
|
||||
class IpcMessageHandler implements IpcMessageListener {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(IpcProtocolImpl.class);
|
||||
private static final Logger LOG = LoggerFactory.getLogger(IpcMessageHandler.class);
|
||||
|
||||
private final FileOpenRequestHandler fileOpenRequestHandler;
|
||||
private final BlockingQueue<AppLaunchEvent> launchEventQueue;
|
||||
|
||||
@Inject
|
||||
public IpcProtocolImpl(FileOpenRequestHandler fileOpenRequestHandler, @Named("launchEventQueue") BlockingQueue<AppLaunchEvent> launchEventQueue) {
|
||||
public IpcMessageHandler(FileOpenRequestHandler fileOpenRequestHandler, @Named("launchEventQueue") BlockingQueue<AppLaunchEvent> launchEventQueue) {
|
||||
this.fileOpenRequestHandler = fileOpenRequestHandler;
|
||||
this.launchEventQueue = launchEventQueue;
|
||||
}
|
||||
@@ -31,8 +32,8 @@ class IpcProtocolImpl implements IpcProtocol {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleLaunchArgs(String... args) {
|
||||
LOG.debug("Received launch args: {}", Arrays.stream(args).reduce((a, b) -> a + ", " + b).orElse(""));
|
||||
public void handleLaunchArgs(List<String> args) {
|
||||
LOG.debug("Received launch args: {}", args.stream().reduce((a, b) -> a + ", " + b).orElse(""));
|
||||
fileOpenRequestHandler.handleLaunchArgs(args);
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the accompanying LICENSE file.
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.launcher;
|
||||
|
||||
import java.rmi.Remote;
|
||||
import java.rmi.RemoteException;
|
||||
|
||||
interface IpcProtocol extends Remote {
|
||||
|
||||
void revealRunningApp() throws RemoteException;
|
||||
|
||||
void handleLaunchArgs(String... args) throws RemoteException;
|
||||
|
||||
}
|
||||
@@ -2,8 +2,8 @@ package org.cryptomator.ui.common;
|
||||
|
||||
import dagger.BindsInstance;
|
||||
import dagger.Subcomponent;
|
||||
import org.cryptomator.common.Nullable;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.cryptomator.ui.common;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import org.cryptomator.common.Nullable;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javafx.fxml.FXML;
|
||||
|
||||
@@ -5,7 +5,6 @@ import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.Map;
|
||||
import java.util.ResourceBundle;
|
||||
@@ -26,11 +25,9 @@ public class FxmlLoaderFactory {
|
||||
/**
|
||||
* @return A new FXMLLoader instance
|
||||
*/
|
||||
public FXMLLoader construct() {
|
||||
FXMLLoader loader = new FXMLLoader();
|
||||
loader.setControllerFactory(this::constructController);
|
||||
loader.setResources(resourceBundle);
|
||||
return loader;
|
||||
private FXMLLoader construct(String fxmlResourceName) {
|
||||
var url = getClass().getResource(fxmlResourceName);
|
||||
return new FXMLLoader(url, resourceBundle, null, this::constructController);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,10 +38,8 @@ public class FxmlLoaderFactory {
|
||||
* @throws IOException if an error occurs while loading the FXML file
|
||||
*/
|
||||
public FXMLLoader load(String fxmlResourceName) throws IOException {
|
||||
FXMLLoader loader = construct();
|
||||
try (InputStream in = getClass().getResourceAsStream(fxmlResourceName)) {
|
||||
loader.load(in);
|
||||
}
|
||||
FXMLLoader loader = construct(fxmlResourceName);
|
||||
loader.load();
|
||||
return loader;
|
||||
}
|
||||
|
||||
|
||||
@@ -35,9 +35,9 @@ public class Check {
|
||||
|
||||
String getLocalizedName() {
|
||||
try {
|
||||
return resourceBundle.getString(LOCALIZE_PREFIX+check.identifier());
|
||||
return resourceBundle.getString(LOCALIZE_PREFIX+check.name());
|
||||
} catch (MissingResourceException e){
|
||||
return check.identifier();
|
||||
return check.name();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ public class ReportWriter {
|
||||
var writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8))) {
|
||||
writer.write(REPORT_HEADER.formatted(vaultConfig.getId(), vault.getDisplayName(), vault.getPath()));
|
||||
for (var check : performedChecks) {
|
||||
writer.write(REPORT_CHECK_HEADER.formatted(check.getHealthCheck().identifier()));
|
||||
writer.write(REPORT_CHECK_HEADER.formatted(check.getHealthCheck().name()));
|
||||
switch (check.getState()) {
|
||||
case SUCCEEDED -> {
|
||||
writer.write("STATUS: SUCCESS\nRESULTS:\n");
|
||||
|
||||
@@ -19,6 +19,7 @@ import java.awt.Desktop;
|
||||
import java.awt.EventQueue;
|
||||
import java.awt.desktop.AboutEvent;
|
||||
import java.awt.desktop.QuitResponse;
|
||||
import java.awt.desktop.QuitStrategy;
|
||||
import java.util.EnumSet;
|
||||
import java.util.EventObject;
|
||||
import java.util.Set;
|
||||
@@ -61,6 +62,11 @@ public class AppLifecycleListener {
|
||||
Desktop.getDesktop().setQuitHandler(this::handleQuitRequest);
|
||||
}
|
||||
|
||||
// set quit strategy (cmd+q would call `System.exit(0)` otherwise)
|
||||
if (Desktop.getDesktop().isSupported(Desktop.Action.APP_QUIT_STRATEGY)) {
|
||||
Desktop.getDesktop().setQuitStrategy(QuitStrategy.CLOSE_ALL_WINDOWS);
|
||||
}
|
||||
|
||||
shutdownHook.runOnShutdown(this::forceUnmountRemainingVaults);
|
||||
}
|
||||
|
||||
@@ -71,7 +77,7 @@ public class AppLifecycleListener {
|
||||
handleQuitRequest(null, new QuitResponse() {
|
||||
@Override
|
||||
public void performQuit() {
|
||||
System.exit(0);
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -96,7 +102,7 @@ public class AppLifecycleListener {
|
||||
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)
|
||||
originalQuitResponse.performQuit();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Reference in New Issue
Block a user