mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-18 10:41:26 +00:00
Merge pull request #1719 from cryptomator/feature/1589-unix-sockets
Changed IPC to Unix domain sockets, fixes #1589, fixes #1716
This commit is contained in:
2
.idea/runConfigurations/Cryptomator_Linux.xml
generated
2
.idea/runConfigurations/Cryptomator_Linux.xml
generated
@@ -2,7 +2,7 @@
|
||||
<configuration default="false" name="Cryptomator Linux" type="Application" factoryName="Application">
|
||||
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
||||
<module name="cryptomator" />
|
||||
<option name="VM_PARAMETERS" value="-Djdk.gtk.version=2 -Duser.language=en -Dcryptomator.settingsPath="~/.config/Cryptomator/settings.json" -Dcryptomator.ipcPortPath="~/.config/Cryptomator/ipcPort.bin" -Dcryptomator.logDir="~/.local/share/Cryptomator/logs" -Dcryptomator.mountPointsDir="~/.local/share/Cryptomator/mnt" -Dcryptomator.showTrayIcon=true -Xss20m -Xmx512m" />
|
||||
<option name="VM_PARAMETERS" value="-Djdk.gtk.version=2 -Duser.language=en -Dcryptomator.settingsPath="~/.config/Cryptomator/settings.json" -Dcryptomator.ipcSocketPath="~/.config/Cryptomator/ipc.socket" -Dcryptomator.logDir="~/.local/share/Cryptomator/logs" -Dcryptomator.mountPointsDir="~/.local/share/Cryptomator/mnt" -Dcryptomator.showTrayIcon=true -Xss20m -Xmx512m" />
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<configuration default="false" name="Cryptomator Linux Dev" type="Application" factoryName="Application">
|
||||
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
||||
<module name="cryptomator" />
|
||||
<option name="VM_PARAMETERS" value="-Djdk.gtk.version=2 -Duser.language=en -Dcryptomator.settingsPath="~/.config/Cryptomator-Dev/settings.json" -Dcryptomator.ipcPortPath="~/.config/Cryptomator-Dev/ipcPort.bin" -Dcryptomator.logDir="~/.local/share/Cryptomator-Dev/logs" -Dcryptomator.mountPointsDir="~/.local/share/Cryptomator-Dev/mnt" -Dcryptomator.showTrayIcon=true -Dfuse.experimental="true" -Xss20m -Xmx512m" />
|
||||
<option name="VM_PARAMETERS" value="-Djdk.gtk.version=2 -Duser.language=en -Dcryptomator.settingsPath="~/.config/Cryptomator-Dev/settings.json" -Dcryptomator.ipcSocketPath="~/.config/Cryptomator-Dev/ipc.socket" -Dcryptomator.logDir="~/.local/share/Cryptomator-Dev/logs" -Dcryptomator.mountPointsDir="~/.local/share/Cryptomator-Dev/mnt" -Dcryptomator.showTrayIcon=true -Dfuse.experimental="true" -Xss20m -Xmx512m" />
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
|
||||
2
.idea/runConfigurations/Cryptomator_Windows.xml
generated
2
.idea/runConfigurations/Cryptomator_Windows.xml
generated
@@ -2,7 +2,7 @@
|
||||
<configuration default="false" name="Cryptomator Windows" type="Application" factoryName="Application">
|
||||
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
||||
<module name="cryptomator" />
|
||||
<option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath="~/AppData/Roaming/Cryptomator/settings.json" -Dcryptomator.ipcPortPath="~/AppData/Roaming/Cryptomator/ipcPort.bin" -Dcryptomator.logDir="~/AppData/Roaming/Cryptomator" -Dcryptomator.keychainPath="~/AppData/Roaming/Cryptomator/keychain.json" -Dcryptomator.mountPointsDir="~/Cryptomator" -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m" />
|
||||
<option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath="~/AppData/Roaming/Cryptomator/settings.json" -Dcryptomator.ipcSocketPath="~/AppData/Roaming/Cryptomator/ipc.socket" -Dcryptomator.logDir="~/AppData/Roaming/Cryptomator" -Dcryptomator.keychainPath="~/AppData/Roaming/Cryptomator/keychain.json" -Dcryptomator.mountPointsDir="~/Cryptomator" -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m" />
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<configuration default="false" name="Cryptomator Windows Dev" type="Application" factoryName="Application">
|
||||
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
||||
<module name="cryptomator" />
|
||||
<option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath="~/AppData/Roaming/Cryptomator-Dev/settings.json" -Dcryptomator.ipcPortPath="~/AppData/Roaming/Cryptomator-Dev/ipcPort.bin" -Dcryptomator.logDir="~/AppData/Roaming/Cryptomator-Dev" -Dcryptomator.keychainPath="~/AppData/Roaming/Cryptomator-Dev/keychain.json" -Dcryptomator.mountPointsDir="~/Cryptomator-Dev" -Dfuse.experimental="true" -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m" />
|
||||
<option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath="~/AppData/Roaming/Cryptomator-Dev/settings.json" -Dcryptomator.ipcSocketPath="~/AppData/Roaming/Cryptomator-Dev/ipc.socket" -Dcryptomator.logDir="~/AppData/Roaming/Cryptomator-Dev" -Dcryptomator.keychainPath="~/AppData/Roaming/Cryptomator-Dev/keychain.json" -Dcryptomator.mountPointsDir="~/Cryptomator-Dev" -Dfuse.experimental="true" -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m" />
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
|
||||
2
.idea/runConfigurations/Cryptomator_macOS.xml
generated
2
.idea/runConfigurations/Cryptomator_macOS.xml
generated
@@ -5,7 +5,7 @@
|
||||
</envs>
|
||||
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
||||
<module name="cryptomator" />
|
||||
<option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath="~/Library/Application Support/Cryptomator/settings.json" -Dcryptomator.ipcPortPath="~/Library/Application Support/Cryptomator/ipcPort.bin" -Dcryptomator.logDir="~/Library/Logs/Cryptomator" -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m -ea" />
|
||||
<option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath="~/Library/Application Support/Cryptomator/settings.json" -Dcryptomator.ipcSocketPath="~/Library/Application Support/Cryptomator/ipc.socket" -Dcryptomator.logDir="~/Library/Logs/Cryptomator" -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m -ea" />
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</envs>
|
||||
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
||||
<module name="cryptomator" />
|
||||
<option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath="~/Library/Application Support/Cryptomator-Dev/settings.json" -Dcryptomator.ipcPortPath="~/Library/Application Support/Cryptomator-Dev/ipcPort.bin" -Dcryptomator.logDir="~/Library/Logs/Cryptomator-Dev" -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m -ea" />
|
||||
<option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath="~/Library/Application Support/Cryptomator-Dev/settings.json" -Dcryptomator.ipcSocketPath="~/Library/Application Support/Cryptomator-Dev/ipc.socket" -Dcryptomator.logDir="~/Library/Logs/Cryptomator-Dev" -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m -ea" />
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
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,8 +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;
|
||||
@@ -17,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 {
|
||||
@@ -30,18 +36,22 @@ 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 ShutdownHook shutdownHook;
|
||||
private final Lazy<UiLauncher> uiLauncher;
|
||||
|
||||
@Inject
|
||||
Cryptomator(LoggerConfiguration logConfig, DebugMode debugMode, IpcFactory ipcFactory, @Named("applicationVersion") Optional<String> applicationVersion, @Named("shutdownLatch") CountDownLatch shutdownLatch, Lazy<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;
|
||||
}
|
||||
|
||||
@@ -66,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,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;
|
||||
|
||||
}
|
||||
@@ -3,7 +3,7 @@ cd $(dirname $0)
|
||||
java \
|
||||
-cp "libs/*" \
|
||||
-Dcryptomator.settingsPath="~/.config/Cryptomator/settings.json" \
|
||||
-Dcryptomator.ipcPortPath="~/.config/Cryptomator/ipcPort.bin" \
|
||||
-Dcryptomator.ipcSocketPath="~/.config/Cryptomator/ipc.socket" \
|
||||
-Dcryptomator.logDir="~/.local/share/Cryptomator/logs" \
|
||||
-Dcryptomator.mountPointsDir="~/.local/share/Cryptomator/mnt" \
|
||||
-Djdk.gtk.version=2 \
|
||||
|
||||
@@ -3,7 +3,7 @@ cd $(dirname $0)
|
||||
java \
|
||||
-cp "libs/*" \
|
||||
-Dcryptomator.settingsPath="~/Library/Application Support/Cryptomator/settings.json" \
|
||||
-Dcryptomator.ipcPortPath="~/Library/Application Support/Cryptomator/ipcPort.bin" \
|
||||
-Dcryptomator.ipcSocketPath="~/Library/Application Support/Cryptomator/ipc.socket" \
|
||||
-Dcryptomator.logDir="~/Library/Logs/Cryptomator" \
|
||||
-Dcryptomator.mountPointsDir="/Volumes" \
|
||||
-Xss20m \
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
java ^
|
||||
-cp "libs/*" ^
|
||||
-Dcryptomator.settingsPath="~/AppData/Roaming/Cryptomator/settings.json" ^
|
||||
-Dcryptomator.ipcPortPath="~/AppData/Roaming/Cryptomator/ipcPort.bin" ^
|
||||
-Dcryptomator.ipcSocketPath="~/AppData/Roaming/Cryptomator/ipc.socket" ^
|
||||
-Dcryptomator.logDir="~/AppData/Roaming/Cryptomator" ^
|
||||
-Dcryptomator.mountPointsDir="~/Cryptomator" ^
|
||||
-Dcryptomator.keychainPath="~/AppData/Roaming/Cryptomator/keychain.json" ^
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.cryptomator.common;
|
||||
import org.hamcrest.MatcherAssert;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
@@ -14,7 +13,6 @@ import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@DisplayName("Environment Variables Test")
|
||||
public class EnvironmentTest {
|
||||
@@ -39,14 +37,14 @@ public class EnvironmentTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("cryptomator.ipcPortPath=~/.config/Cryptomator/ipcPort.bin:~/.Cryptomator/ipcPort.bin")
|
||||
public void testIpcPortPath() {
|
||||
System.setProperty("cryptomator.ipcPortPath", "~/.config/Cryptomator/ipcPort.bin:~/.Cryptomator/ipcPort.bin");
|
||||
@DisplayName("cryptomator.ipcSocketPath=~/.config/Cryptomator/ipc.socket:~/.Cryptomator/ipc.socket")
|
||||
public void testIpcSocketPath() {
|
||||
System.setProperty("cryptomator.ipcSocketPath", "~/.config/Cryptomator/ipc.socket:~/.Cryptomator/ipc.socket");
|
||||
|
||||
List<Path> result = env.getIpcPortPath().toList();
|
||||
List<Path> result = env.ipcSocketPath().toList();
|
||||
MatcherAssert.assertThat(result, Matchers.hasSize(2));
|
||||
MatcherAssert.assertThat(result, Matchers.contains(Paths.get("/home/testuser/.config/Cryptomator/ipcPort.bin"), //
|
||||
Paths.get("/home/testuser/.Cryptomator/ipcPort.bin")));
|
||||
MatcherAssert.assertThat(result, Matchers.contains(Paths.get("/home/testuser/.config/Cryptomator/ipc.socket"), //
|
||||
Paths.get("/home/testuser/.Cryptomator/ipc.socket")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.cryptomator.ipc;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.List;
|
||||
|
||||
public class HandleLaunchArgsMessageTest {
|
||||
|
||||
@Test
|
||||
public void testSendAndReceive(@TempDir Path tmpDir) throws IOException {
|
||||
var message = new HandleLaunchArgsMessage(List.of("hello world", "foo bar"));
|
||||
|
||||
var file = tmpDir.resolve("tmp.file");
|
||||
try (var ch = FileChannel.open(file, StandardOpenOption.CREATE_NEW, StandardOpenOption.READ, StandardOpenOption.WRITE)) {
|
||||
message.send(ch);
|
||||
ch.position(0);
|
||||
if (IpcMessage.receive(ch) instanceof HandleLaunchArgsMessage received) {
|
||||
Assertions.assertArrayEquals(message.args().toArray(), received.args().toArray());
|
||||
} else {
|
||||
Assertions.fail("Received message of unexpected class");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendAndReceiveEmpty(@TempDir Path tmpDir) throws IOException {
|
||||
var message = new HandleLaunchArgsMessage(List.of());
|
||||
|
||||
var file = tmpDir.resolve("tmp.file");
|
||||
try (var ch = FileChannel.open(file, StandardOpenOption.CREATE_NEW, StandardOpenOption.READ, StandardOpenOption.WRITE)) {
|
||||
message.send(ch);
|
||||
ch.position(0);
|
||||
if (IpcMessage.receive(ch) instanceof HandleLaunchArgsMessage received) {
|
||||
Assertions.assertArrayEquals(message.args().toArray(), received.args().toArray());
|
||||
} else {
|
||||
Assertions.fail("Received message of unexpected class");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
44
src/test/java/org/cryptomator/ipc/IpcCommunicatorTest.java
Normal file
44
src/test/java/org/cryptomator/ipc/IpcCommunicatorTest.java
Normal file
@@ -0,0 +1,44 @@
|
||||
package org.cryptomator.ipc;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.function.Executable;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class IpcCommunicatorTest {
|
||||
|
||||
@Test
|
||||
public void testSendAndReceive(@TempDir Path tmpDir) throws IOException, InterruptedException {
|
||||
var socketPath = tmpDir.resolve("foo.sock");
|
||||
try (var server = IpcCommunicator.create(List.of(socketPath));
|
||||
var client = IpcCommunicator.create(List.of(socketPath))) {
|
||||
Assertions.assertNotSame(server, client);
|
||||
|
||||
var cdl = new CountDownLatch(1);
|
||||
var executor = Executors.newSingleThreadExecutor();
|
||||
server.listen(new IpcMessageListener() {
|
||||
@Override
|
||||
public void revealRunningApp() {
|
||||
cdl.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleLaunchArgs(List<String> args) {
|
||||
|
||||
}
|
||||
}, executor);
|
||||
client.sendRevealRunningApp();
|
||||
|
||||
Assertions.assertTimeoutPreemptively(Duration.ofMillis(300), (Executable) cdl::await);
|
||||
executor.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.cryptomator.ipc;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.function.Executable;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class LoopbackCommunicatorTest {
|
||||
|
||||
@Test
|
||||
public void testSendAndReceive() {
|
||||
try (var communicator = new LoopbackCommunicator()) {
|
||||
var cdl = new CountDownLatch(1);
|
||||
var executor = Executors.newSingleThreadExecutor();
|
||||
communicator.listen(new IpcMessageListener() {
|
||||
@Override
|
||||
public void revealRunningApp() {
|
||||
cdl.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleLaunchArgs(List<String> args) {
|
||||
|
||||
}
|
||||
}, executor);
|
||||
communicator.sendRevealRunningApp();
|
||||
|
||||
Assertions.assertTimeoutPreemptively(Duration.ofMillis(300), (Executable) cdl::await);
|
||||
executor.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.cryptomator.ipc;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.List;
|
||||
|
||||
public class RevealRunningAppMessageTest {
|
||||
|
||||
@Test
|
||||
public void testSendAndReceive(@TempDir Path tmpDir) throws IOException {
|
||||
var message = new RevealRunningAppMessage();
|
||||
|
||||
var file = tmpDir.resolve("tmp.file");
|
||||
try (var ch = FileChannel.open(file, StandardOpenOption.CREATE_NEW, StandardOpenOption.READ, StandardOpenOption.WRITE)) {
|
||||
message.send(ch);
|
||||
ch.position(0);
|
||||
if (IpcMessage.receive(ch) instanceof RevealRunningAppMessage received) {
|
||||
Assertions.assertNotNull(received);
|
||||
} else {
|
||||
Assertions.fail("Received message of unexpected class");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
|
||||
@@ -38,7 +39,7 @@ public class FileOpenRequestHandlerTest {
|
||||
@Test
|
||||
@DisplayName("./cryptomator.exe foo bar")
|
||||
public void testOpenArgsWithCorrectPaths() {
|
||||
inTest.handleLaunchArgs(new String[]{"foo", "bar"});
|
||||
inTest.handleLaunchArgs(List.of("foo", "bar"));
|
||||
|
||||
AppLaunchEvent evt = queue.poll();
|
||||
Assertions.assertNotNull(evt);
|
||||
@@ -51,7 +52,7 @@ public class FileOpenRequestHandlerTest {
|
||||
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"});
|
||||
inTest.handleLaunchArgs(fs, List.of("foo"));
|
||||
|
||||
AppLaunchEvent evt = queue.poll();
|
||||
Assertions.assertNull(evt);
|
||||
@@ -63,7 +64,7 @@ public class FileOpenRequestHandlerTest {
|
||||
queue.add(new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, Collections.emptyList()));
|
||||
Assumptions.assumeTrue(queue.remainingCapacity() == 0);
|
||||
|
||||
inTest.handleLaunchArgs(new String[]{"foo"});
|
||||
inTest.handleLaunchArgs(List.of("foo"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,71 +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 org.cryptomator.common.Environment;
|
||||
import org.cryptomator.launcher.IpcFactory.IpcEndpoint;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class IpcFactoryTest {
|
||||
|
||||
private Environment environment = Mockito.mock(Environment.class);
|
||||
private IpcProtocolImpl protocolHandler = Mockito.mock(IpcProtocolImpl.class);
|
||||
|
||||
@Test
|
||||
@DisplayName("Without IPC port files")
|
||||
public void testNoIpcWithoutPortFile() throws IOException {
|
||||
IpcFactory inTest = new IpcFactory(environment, protocolHandler);
|
||||
|
||||
Mockito.when(environment.getIpcPortPath()).thenReturn(Stream.empty());
|
||||
try (IpcEndpoint endpoint1 = inTest.create()) {
|
||||
Assertions.assertEquals(IpcFactory.SelfEndpoint.class, endpoint1.getClass());
|
||||
Assertions.assertFalse(endpoint1.isConnectedToRemote());
|
||||
Assertions.assertSame(protocolHandler, endpoint1.getRemote());
|
||||
try (IpcEndpoint endpoint2 = inTest.create()) {
|
||||
Assertions.assertEquals(IpcFactory.SelfEndpoint.class, endpoint2.getClass());
|
||||
Assertions.assertNotSame(endpoint1, endpoint2);
|
||||
Assertions.assertFalse(endpoint2.isConnectedToRemote());
|
||||
Assertions.assertSame(protocolHandler, endpoint2.getRemote());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Start server and client with port shared via file")
|
||||
public void testInterProcessCommunication(@TempDir Path tmpDir) throws IOException {
|
||||
Path portFile = tmpDir.resolve("testPortFile");
|
||||
Mockito.when(environment.getIpcPortPath()).thenReturn(Stream.of(portFile));
|
||||
IpcFactory inTest = new IpcFactory(environment, protocolHandler);
|
||||
|
||||
Assertions.assertFalse(Files.exists(portFile));
|
||||
try (IpcEndpoint endpoint1 = inTest.create()) {
|
||||
Assertions.assertEquals(IpcFactory.ServerEndpoint.class, endpoint1.getClass());
|
||||
Assertions.assertFalse(endpoint1.isConnectedToRemote());
|
||||
Assertions.assertTrue(Files.exists(portFile));
|
||||
Assertions.assertSame(protocolHandler, endpoint1.getRemote());
|
||||
Mockito.verifyZeroInteractions(protocolHandler);
|
||||
try (IpcEndpoint endpoint2 = inTest.create()) {
|
||||
Assertions.assertEquals(IpcFactory.ClientEndpoint.class, endpoint2.getClass());
|
||||
Assertions.assertNotSame(endpoint1, endpoint2);
|
||||
Assertions.assertTrue(endpoint2.isConnectedToRemote());
|
||||
Assertions.assertNotSame(protocolHandler, endpoint2.getRemote());
|
||||
Mockito.verifyZeroInteractions(protocolHandler);
|
||||
endpoint2.getRemote().handleLaunchArgs(new String[]{"foo"});
|
||||
Mockito.verify(protocolHandler).handleLaunchArgs(new String[]{"foo"});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user