signal-cli/src/main/java/org/asamk/signal/commands/DaemonCommand.java
2024-11-23 22:35:06 +01:00

317 lines
12 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package org.asamk.signal.commands;
import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.DbusConfig;
import org.asamk.signal.OutputType;
import org.asamk.signal.ReceiveMessageHandler;
import org.asamk.signal.Shutdown;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.dbus.DbusHandler;
import org.asamk.signal.http.HttpServerHandler;
import org.asamk.signal.json.JsonReceiveMessageHandler;
import org.asamk.signal.jsonrpc.SocketHandler;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.MultiAccountManager;
import org.asamk.signal.output.JsonWriter;
import org.asamk.signal.output.OutputWriter;
import org.asamk.signal.output.PlainTextWriter;
import org.asamk.signal.util.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.UnixDomainSocketAddress;
import java.nio.channels.Channel;
import java.nio.channels.ServerSocketChannel;
import java.util.ArrayList;
import java.util.List;
import static org.asamk.signal.util.CommandUtil.getReceiveConfig;
public class DaemonCommand implements MultiLocalCommand, LocalCommand {
private static final Logger logger = LoggerFactory.getLogger(DaemonCommand.class);
@Override
public String getName() {
return "daemon";
}
@Override
public void attachToSubparser(final Subparser subparser) {
final var defaultSocketPath = new File(new File(IOUtils.getRuntimeDir(), "signal-cli"), "socket");
subparser.help("Run in daemon mode and provide a JSON-RPC or an experimental dbus interface.");
subparser.addArgument("--dbus").action(Arguments.storeTrue()).help("Expose a DBus interface on the user bus.");
subparser.addArgument("--dbus-system", "--system")
.action(Arguments.storeTrue())
.help("Expose a DBus interface on the system bus.");
subparser.addArgument("--bus-name")
.setDefault(DbusConfig.getBusname())
.help("Specify the D-Bus bus name to connect to.");
subparser.addArgument("--socket")
.nargs("?")
.type(File.class)
.setConst(defaultSocketPath)
.help("Expose a JSON-RPC interface on a UNIX socket (default $XDG_RUNTIME_DIR/signal-cli/socket).");
subparser.addArgument("--tcp")
.nargs("?")
.setConst("localhost:7583")
.help("Expose a JSON-RPC interface on a TCP socket (default localhost:7583).");
subparser.addArgument("--http")
.nargs("?")
.setConst("localhost:8080")
.help("Expose a JSON-RPC interface as http endpoint (default localhost:8080).");
subparser.addArgument("--no-receive-stdout")
.help("Dont print received messages to stdout.")
.action(Arguments.storeTrue());
subparser.addArgument("--receive-mode")
.help("Specify when to start receiving messages.")
.type(Arguments.enumStringType(ReceiveMode.class))
.setDefault(ReceiveMode.ON_START);
subparser.addArgument("--ignore-attachments")
.help("Dont download attachments of received messages.")
.action(Arguments.storeTrue());
subparser.addArgument("--ignore-stories")
.help("Dont receive story messages from the server.")
.action(Arguments.storeTrue());
subparser.addArgument("--send-read-receipts")
.help("Send read receipts for all incoming data messages (in addition to the default delivery receipts)")
.action(Arguments.storeTrue());
}
@Override
public List<OutputType> getSupportedOutputTypes() {
return List.of(OutputType.PLAIN_TEXT, OutputType.JSON);
}
@Override
public void handleCommand(
final Namespace ns,
final Manager m,
final OutputWriter outputWriter
) throws CommandException {
Shutdown.installHandler();
logger.info("Starting daemon in single-account mode for " + m.getSelfNumber());
final var noReceiveStdOut = Boolean.TRUE.equals(ns.getBoolean("no-receive-stdout"));
final var receiveMode = ns.<ReceiveMode>get("receive-mode");
final var receiveConfig = getReceiveConfig(ns);
m.setReceiveConfig(receiveConfig);
addDefaultReceiveHandler(m, noReceiveStdOut ? null : outputWriter, receiveMode != ReceiveMode.ON_START);
try (final var daemonHandler = new SingleAccountDaemonHandler(m, receiveMode)) {
setup(ns, daemonHandler);
m.addClosedListener(Shutdown::triggerShutdown);
try {
Shutdown.waitForShutdown();
} catch (InterruptedException ignored) {
}
}
}
@Override
public void handleCommand(
final Namespace ns,
final MultiAccountManager c,
final OutputWriter outputWriter
) throws CommandException {
Shutdown.installHandler();
logger.info("Starting daemon in multi-account mode");
final var noReceiveStdOut = Boolean.TRUE.equals(ns.getBoolean("no-receive-stdout"));
final var receiveMode = ns.<ReceiveMode>get("receive-mode");
final var receiveConfig = getReceiveConfig(ns);
c.getManagers().forEach(m -> {
m.setReceiveConfig(receiveConfig);
addDefaultReceiveHandler(m, noReceiveStdOut ? null : outputWriter, receiveMode != ReceiveMode.ON_START);
});
c.addOnManagerAddedHandler(m -> {
m.setReceiveConfig(receiveConfig);
addDefaultReceiveHandler(m, noReceiveStdOut ? null : outputWriter, receiveMode != ReceiveMode.ON_START);
});
try (final var daemonHandler = new MultiAccountDaemonHandler(c, receiveMode)) {
setup(ns, daemonHandler);
synchronized (this) {
try {
Shutdown.waitForShutdown();
} catch (InterruptedException ignored) {
}
}
}
}
private static void setup(final Namespace ns, final DaemonHandler daemonHandler) throws CommandException {
final Channel inheritedChannel;
try {
if (System.inheritedChannel() instanceof ServerSocketChannel serverChannel) {
inheritedChannel = serverChannel;
logger.info("Using inherited socket: " + serverChannel.getLocalAddress());
daemonHandler.runSocket(serverChannel);
} else {
inheritedChannel = null;
}
} catch (IOException e) {
throw new IOErrorException("Failed to use inherited socket", e);
}
final var socketFile = ns.<File>get("socket");
if (socketFile != null) {
final var address = UnixDomainSocketAddress.of(socketFile.toPath());
final var serverChannel = IOUtils.bindSocket(address);
daemonHandler.runSocket(serverChannel);
}
final var tcpAddress = ns.getString("tcp");
if (tcpAddress != null) {
final var address = IOUtils.parseInetSocketAddress(tcpAddress);
final var serverChannel = IOUtils.bindSocket(address);
daemonHandler.runSocket(serverChannel);
}
final var httpAddress = ns.getString("http");
if (httpAddress != null) {
final var address = IOUtils.parseInetSocketAddress(httpAddress);
daemonHandler.runHttp(address);
}
final var isDbusSystem = Boolean.TRUE.equals(ns.getBoolean("dbus-system"));
if (isDbusSystem) {
final var busName = ns.getString("bus-name");
daemonHandler.runDbus(true, busName);
}
final var isDbusSession = Boolean.TRUE.equals(ns.getBoolean("dbus"));
if (isDbusSession) {
final var busName = ns.getString("bus-name");
daemonHandler.runDbus(false, busName);
}
if (!isDbusSystem
&& !isDbusSession
&& socketFile == null
&& tcpAddress == null
&& httpAddress == null
&& inheritedChannel == null) {
logger.warn(
"Running daemon command without explicit mode is deprecated. Use 'daemon --dbus' to use the dbus interface.");
daemonHandler.runDbus(false, DbusConfig.getBusname());
}
}
private void addDefaultReceiveHandler(Manager m, OutputWriter outputWriter, final boolean isWeakListener) {
final var handler = switch (outputWriter) {
case PlainTextWriter writer -> new ReceiveMessageHandler(m, writer);
case JsonWriter writer -> new JsonReceiveMessageHandler(m, writer);
case null -> Manager.ReceiveMessageHandler.EMPTY;
};
m.addReceiveHandler(handler, isWeakListener);
}
private static abstract class DaemonHandler implements AutoCloseable {
protected final ReceiveMode receiveMode;
protected final List<AutoCloseable> closeables = new ArrayList<>();
protected DaemonHandler(final ReceiveMode receiveMode) {
this.receiveMode = receiveMode;
}
public abstract void runSocket(ServerSocketChannel serverChannel) throws CommandException;
public abstract void runDbus(boolean isDbusSystem, final String busname) throws CommandException;
public abstract void runHttp(InetSocketAddress address) throws CommandException;
protected final void runSocket(final SocketHandler socketHandler) {
socketHandler.init();
this.closeables.add(socketHandler);
}
protected final void runDbus(
DbusHandler dbusHandler
) throws CommandException {
dbusHandler.init();
this.closeables.add(dbusHandler);
}
protected final void runHttp(final HttpServerHandler handler) throws CommandException {
try {
handler.init();
} catch (IOException ex) {
throw new IOErrorException("Failed to initialize HTTP Server", ex);
}
this.closeables.add(handler);
}
@Override
public void close() {
for (final var closeable : new ArrayList<>(this.closeables)) {
try {
closeable.close();
} catch (Exception e) {
logger.warn("Failed to close daemon handler", e);
}
}
this.closeables.clear();
}
}
private static final class SingleAccountDaemonHandler extends DaemonHandler {
private final Manager m;
public SingleAccountDaemonHandler(final Manager m, final ReceiveMode receiveMode) {
super(receiveMode);
this.m = m;
}
@Override
public void runSocket(final ServerSocketChannel serverChannel) {
runSocket(new SocketHandler(serverChannel, m, receiveMode == ReceiveMode.MANUAL));
}
@Override
public void runDbus(final boolean isDbusSystem, final String busname) throws CommandException {
runDbus(new DbusHandler(isDbusSystem, busname, m, receiveMode != ReceiveMode.ON_START));
}
@Override
public void runHttp(InetSocketAddress address) throws CommandException {
runHttp(new HttpServerHandler(address, m));
}
}
private static final class MultiAccountDaemonHandler extends DaemonHandler {
private final MultiAccountManager c;
public MultiAccountDaemonHandler(final MultiAccountManager c, final ReceiveMode receiveMode) {
super(receiveMode);
this.c = c;
}
@Override
public void runSocket(final ServerSocketChannel serverChannel) {
runSocket(new SocketHandler(serverChannel, c, receiveMode == ReceiveMode.MANUAL));
}
@Override
public void runDbus(final boolean isDbusSystem, final String busname) throws CommandException {
runDbus(new DbusHandler(isDbusSystem, busname, c, receiveMode != ReceiveMode.ON_START));
}
@Override
public void runHttp(final InetSocketAddress address) throws CommandException {
runHttp(new HttpServerHandler(address, c));
}
}
}