]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/commands/DaemonCommand.java
e4a14f4ac975445694a5f67f236277d155b33daf
[signal-cli] / src / main / java / org / asamk / signal / commands / DaemonCommand.java
1 package org.asamk.signal.commands;
2
3 import net.sourceforge.argparse4j.impl.Arguments;
4 import net.sourceforge.argparse4j.inf.Namespace;
5 import net.sourceforge.argparse4j.inf.Subparser;
6
7 import org.asamk.signal.DbusConfig;
8 import org.asamk.signal.OutputType;
9 import org.asamk.signal.ReceiveMessageHandler;
10 import org.asamk.signal.Shutdown;
11 import org.asamk.signal.commands.exceptions.CommandException;
12 import org.asamk.signal.commands.exceptions.IOErrorException;
13 import org.asamk.signal.dbus.DbusHandler;
14 import org.asamk.signal.http.HttpServerHandler;
15 import org.asamk.signal.json.JsonReceiveMessageHandler;
16 import org.asamk.signal.jsonrpc.SocketHandler;
17 import org.asamk.signal.manager.Manager;
18 import org.asamk.signal.manager.MultiAccountManager;
19 import org.asamk.signal.output.JsonWriter;
20 import org.asamk.signal.output.OutputWriter;
21 import org.asamk.signal.output.PlainTextWriter;
22 import org.asamk.signal.util.IOUtils;
23 import org.slf4j.Logger;
24 import org.slf4j.LoggerFactory;
25
26 import java.io.File;
27 import java.io.IOException;
28 import java.net.InetSocketAddress;
29 import java.net.UnixDomainSocketAddress;
30 import java.nio.channels.Channel;
31 import java.nio.channels.ServerSocketChannel;
32 import java.util.ArrayList;
33 import java.util.List;
34
35 import static org.asamk.signal.util.CommandUtil.getReceiveConfig;
36
37 public class DaemonCommand implements MultiLocalCommand, LocalCommand {
38
39 private static final Logger logger = LoggerFactory.getLogger(DaemonCommand.class);
40
41 @Override
42 public String getName() {
43 return "daemon";
44 }
45
46 @Override
47 public void attachToSubparser(final Subparser subparser) {
48 final var defaultSocketPath = new File(new File(IOUtils.getRuntimeDir(), "signal-cli"), "socket");
49 subparser.help("Run in daemon mode and provide a JSON-RPC or an experimental dbus interface.");
50 subparser.addArgument("--dbus").action(Arguments.storeTrue()).help("Expose a DBus interface on the user bus.");
51 subparser.addArgument("--dbus-system", "--system")
52 .action(Arguments.storeTrue())
53 .help("Expose a DBus interface on the system bus.");
54 subparser.addArgument("--bus-name")
55 .setDefault(DbusConfig.getBusname())
56 .help("Specify the D-Bus bus name to connect to.");
57 subparser.addArgument("--socket")
58 .nargs("?")
59 .type(File.class)
60 .setConst(defaultSocketPath)
61 .help("Expose a JSON-RPC interface on a UNIX socket (default $XDG_RUNTIME_DIR/signal-cli/socket).");
62 subparser.addArgument("--tcp")
63 .nargs("?")
64 .setConst("localhost:7583")
65 .help("Expose a JSON-RPC interface on a TCP socket (default localhost:7583).");
66 subparser.addArgument("--http")
67 .nargs("?")
68 .setConst("localhost:8080")
69 .help("Expose a JSON-RPC interface as http endpoint (default localhost:8080).");
70 subparser.addArgument("--no-receive-stdout")
71 .help("Don’t print received messages to stdout.")
72 .action(Arguments.storeTrue());
73 subparser.addArgument("--receive-mode")
74 .help("Specify when to start receiving messages.")
75 .type(Arguments.enumStringType(ReceiveMode.class))
76 .setDefault(ReceiveMode.ON_START);
77 subparser.addArgument("--ignore-attachments")
78 .help("Don’t download attachments of received messages.")
79 .action(Arguments.storeTrue());
80 subparser.addArgument("--ignore-stories")
81 .help("Don’t receive story messages from the server.")
82 .action(Arguments.storeTrue());
83 subparser.addArgument("--send-read-receipts")
84 .help("Send read receipts for all incoming data messages (in addition to the default delivery receipts)")
85 .action(Arguments.storeTrue());
86 }
87
88 @Override
89 public List<OutputType> getSupportedOutputTypes() {
90 return List.of(OutputType.PLAIN_TEXT, OutputType.JSON);
91 }
92
93 @Override
94 public void handleCommand(
95 final Namespace ns, final Manager m, final OutputWriter outputWriter
96 ) throws CommandException {
97 Shutdown.installHandler();
98 logger.info("Starting daemon in single-account mode for " + m.getSelfNumber());
99 final var noReceiveStdOut = Boolean.TRUE.equals(ns.getBoolean("no-receive-stdout"));
100 final var receiveMode = ns.<ReceiveMode>get("receive-mode");
101 final var receiveConfig = getReceiveConfig(ns);
102
103 m.setReceiveConfig(receiveConfig);
104 addDefaultReceiveHandler(m, noReceiveStdOut ? null : outputWriter, receiveMode != ReceiveMode.ON_START);
105
106 try (final var daemonHandler = new SingleAccountDaemonHandler(m, receiveMode)) {
107 setup(ns, daemonHandler);
108
109 m.addClosedListener(Shutdown::triggerShutdown);
110
111 try {
112 Shutdown.waitForShutdown();
113 } catch (InterruptedException ignored) {
114 }
115 }
116 }
117
118 @Override
119 public void handleCommand(
120 final Namespace ns, final MultiAccountManager c, final OutputWriter outputWriter
121 ) throws CommandException {
122 Shutdown.installHandler();
123 logger.info("Starting daemon in multi-account mode");
124 final var noReceiveStdOut = Boolean.TRUE.equals(ns.getBoolean("no-receive-stdout"));
125 final var receiveMode = ns.<ReceiveMode>get("receive-mode");
126 final var receiveConfig = getReceiveConfig(ns);
127 c.getManagers().forEach(m -> {
128 m.setReceiveConfig(receiveConfig);
129 addDefaultReceiveHandler(m, noReceiveStdOut ? null : outputWriter, receiveMode != ReceiveMode.ON_START);
130 });
131 c.addOnManagerAddedHandler(m -> {
132 m.setReceiveConfig(receiveConfig);
133 addDefaultReceiveHandler(m, noReceiveStdOut ? null : outputWriter, receiveMode != ReceiveMode.ON_START);
134 });
135
136 try (final var daemonHandler = new MultiAccountDaemonHandler(c, receiveMode)) {
137 setup(ns, daemonHandler);
138
139 synchronized (this) {
140 try {
141 Shutdown.waitForShutdown();
142 } catch (InterruptedException ignored) {
143 }
144 }
145 }
146 }
147
148 private static void setup(final Namespace ns, final DaemonHandler daemonHandler) throws CommandException {
149 final Channel inheritedChannel;
150 try {
151 if (System.inheritedChannel() instanceof ServerSocketChannel serverChannel) {
152 inheritedChannel = serverChannel;
153 logger.info("Using inherited socket: " + serverChannel.getLocalAddress());
154 daemonHandler.runSocket(serverChannel);
155 } else {
156 inheritedChannel = null;
157 }
158 } catch (IOException e) {
159 throw new IOErrorException("Failed to use inherited socket", e);
160 }
161
162 final var socketFile = ns.<File>get("socket");
163 if (socketFile != null) {
164 final var address = UnixDomainSocketAddress.of(socketFile.toPath());
165 final var serverChannel = IOUtils.bindSocket(address);
166 daemonHandler.runSocket(serverChannel);
167 }
168
169 final var tcpAddress = ns.getString("tcp");
170 if (tcpAddress != null) {
171 final var address = IOUtils.parseInetSocketAddress(tcpAddress);
172 final var serverChannel = IOUtils.bindSocket(address);
173 daemonHandler.runSocket(serverChannel);
174 }
175
176 final var httpAddress = ns.getString("http");
177 if (httpAddress != null) {
178 final var address = IOUtils.parseInetSocketAddress(httpAddress);
179 daemonHandler.runHttp(address);
180 }
181
182 final var isDbusSystem = Boolean.TRUE.equals(ns.getBoolean("dbus-system"));
183 if (isDbusSystem) {
184 final var busName = ns.getString("bus-name");
185 daemonHandler.runDbus(true, busName);
186 }
187
188 final var isDbusSession = Boolean.TRUE.equals(ns.getBoolean("dbus"));
189 if (isDbusSession) {
190 final var busName = ns.getString("bus-name");
191 daemonHandler.runDbus(false, busName);
192 }
193
194 if (!isDbusSystem
195 && !isDbusSession
196 && socketFile == null
197 && tcpAddress == null
198 && httpAddress == null
199 && inheritedChannel == null) {
200 logger.warn(
201 "Running daemon command without explicit mode is deprecated. Use --dbus to use the dbus interface.");
202 daemonHandler.runDbus(false, DbusConfig.getBusname());
203 }
204 }
205
206 private void addDefaultReceiveHandler(Manager m, OutputWriter outputWriter, final boolean isWeakListener) {
207 final var handler = switch (outputWriter) {
208 case PlainTextWriter writer -> new ReceiveMessageHandler(m, writer);
209 case JsonWriter writer -> new JsonReceiveMessageHandler(m, writer);
210 case null -> Manager.ReceiveMessageHandler.EMPTY;
211 };
212 m.addReceiveHandler(handler, isWeakListener);
213 }
214
215 private static abstract class DaemonHandler implements AutoCloseable {
216
217 protected final ReceiveMode receiveMode;
218 protected final List<AutoCloseable> closeables = new ArrayList<>();
219
220 protected DaemonHandler(final ReceiveMode receiveMode) {
221 this.receiveMode = receiveMode;
222 }
223
224 public abstract void runSocket(ServerSocketChannel serverChannel) throws CommandException;
225
226 public abstract void runDbus(boolean isDbusSystem, final String busname) throws CommandException;
227
228 public abstract void runHttp(InetSocketAddress address) throws CommandException;
229
230 protected final void runSocket(final SocketHandler socketHandler) {
231 socketHandler.init();
232 this.closeables.add(socketHandler);
233 }
234
235 protected final void runDbus(
236 DbusHandler dbusHandler
237 ) throws CommandException {
238 dbusHandler.init();
239 this.closeables.add(dbusHandler);
240 }
241
242 protected final void runHttp(final HttpServerHandler handler) throws CommandException {
243 try {
244 handler.init();
245 } catch (IOException ex) {
246 throw new IOErrorException("Failed to initialize HTTP Server", ex);
247 }
248 this.closeables.add(handler);
249 }
250
251 @Override
252 public void close() {
253 for (final var closeable : new ArrayList<>(this.closeables)) {
254 try {
255 closeable.close();
256 } catch (Exception e) {
257 logger.warn("Failed to close daemon handler", e);
258 }
259 }
260 this.closeables.clear();
261 }
262 }
263
264 private static final class SingleAccountDaemonHandler extends DaemonHandler {
265
266 private final Manager m;
267
268 public SingleAccountDaemonHandler(final Manager m, final ReceiveMode receiveMode) {
269 super(receiveMode);
270 this.m = m;
271 }
272
273 @Override
274 public void runSocket(final ServerSocketChannel serverChannel) {
275 runSocket(new SocketHandler(serverChannel, m, receiveMode == ReceiveMode.MANUAL));
276 }
277
278 @Override
279 public void runDbus(final boolean isDbusSystem, final String busname) throws CommandException {
280 runDbus(new DbusHandler(isDbusSystem, busname, m, receiveMode != ReceiveMode.ON_START));
281 }
282
283 @Override
284 public void runHttp(InetSocketAddress address) throws CommandException {
285 runHttp(new HttpServerHandler(address, m));
286 }
287 }
288
289 private static final class MultiAccountDaemonHandler extends DaemonHandler {
290
291 private final MultiAccountManager c;
292
293 public MultiAccountDaemonHandler(final MultiAccountManager c, final ReceiveMode receiveMode) {
294 super(receiveMode);
295 this.c = c;
296 }
297
298 @Override
299 public void runSocket(final ServerSocketChannel serverChannel) {
300 runSocket(new SocketHandler(serverChannel, c, receiveMode == ReceiveMode.MANUAL));
301 }
302
303 @Override
304 public void runDbus(final boolean isDbusSystem, final String busname) throws CommandException {
305 runDbus(new DbusHandler(isDbusSystem, busname, c, receiveMode != ReceiveMode.ON_START));
306 }
307
308 @Override
309 public void runHttp(final InetSocketAddress address) throws CommandException {
310 runHttp(new HttpServerHandler(address, c));
311 }
312 }
313 }