]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/commands/DaemonCommand.java
Handle send failures as non fatal and return detailed results in json output
[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.commands.exceptions.CommandException;
11 import org.asamk.signal.commands.exceptions.IOErrorException;
12 import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
13 import org.asamk.signal.dbus.DbusSignalControlImpl;
14 import org.asamk.signal.dbus.DbusSignalImpl;
15 import org.asamk.signal.json.JsonReceiveMessageHandler;
16 import org.asamk.signal.jsonrpc.SignalJsonRpcDispatcherHandler;
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.JsonWriterImpl;
21 import org.asamk.signal.output.OutputWriter;
22 import org.asamk.signal.output.PlainTextWriter;
23 import org.asamk.signal.util.IOUtils;
24 import org.freedesktop.dbus.connections.impl.DBusConnection;
25 import org.freedesktop.dbus.exceptions.DBusException;
26 import org.slf4j.Logger;
27 import org.slf4j.LoggerFactory;
28
29 import java.io.File;
30 import java.io.IOException;
31 import java.net.UnixDomainSocketAddress;
32 import java.nio.channels.Channel;
33 import java.nio.channels.Channels;
34 import java.nio.channels.ServerSocketChannel;
35 import java.nio.channels.SocketChannel;
36 import java.nio.charset.StandardCharsets;
37 import java.util.List;
38 import java.util.Objects;
39 import java.util.function.Consumer;
40 import java.util.stream.Collectors;
41
42 public class DaemonCommand implements MultiLocalCommand, LocalCommand {
43
44 private final static Logger logger = LoggerFactory.getLogger(DaemonCommand.class);
45
46 @Override
47 public String getName() {
48 return "daemon";
49 }
50
51 @Override
52 public void attachToSubparser(final Subparser subparser) {
53 final var defaultSocketPath = new File(new File(IOUtils.getRuntimeDir(), "signal-cli"), "socket");
54 subparser.help("Run in daemon mode and provide an experimental dbus or JSON-RPC interface.");
55 subparser.addArgument("--dbus")
56 .action(Arguments.storeTrue())
57 .help("Expose a DBus interface on the user bus (the default, if no other options are given).");
58 subparser.addArgument("--dbus-system", "--system")
59 .action(Arguments.storeTrue())
60 .help("Expose a DBus interface on the system bus.");
61 subparser.addArgument("--socket")
62 .nargs("?")
63 .type(File.class)
64 .setConst(defaultSocketPath)
65 .help("Expose a JSON-RPC interface on a UNIX socket (default $XDG_RUNTIME_DIR/signal-cli/socket).");
66 subparser.addArgument("--tcp")
67 .nargs("?")
68 .setConst("localhost:7583")
69 .help("Expose a JSON-RPC interface on a TCP socket (default localhost:7583).");
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 }
81
82 @Override
83 public List<OutputType> getSupportedOutputTypes() {
84 return List.of(OutputType.PLAIN_TEXT, OutputType.JSON);
85 }
86
87 @Override
88 public void handleCommand(
89 final Namespace ns, final Manager m, final OutputWriter outputWriter
90 ) throws CommandException {
91 logger.info("Starting daemon in single-account mode for " + m.getSelfNumber());
92 final var noReceiveStdOut = Boolean.TRUE.equals(ns.getBoolean("no-receive-stdout"));
93 final var receiveMode = ns.<ReceiveMode>get("receive-mode");
94 final var ignoreAttachments = Boolean.TRUE.equals(ns.getBoolean("ignore-attachments"));
95
96 m.setIgnoreAttachments(ignoreAttachments);
97 addDefaultReceiveHandler(m, noReceiveStdOut ? null : outputWriter, receiveMode != ReceiveMode.ON_START);
98
99 final Channel inheritedChannel;
100 try {
101 inheritedChannel = System.inheritedChannel();
102 if (inheritedChannel instanceof ServerSocketChannel serverChannel) {
103 logger.info("Using inherited socket: " + serverChannel.getLocalAddress());
104 runSocketSingleAccount(m, serverChannel, receiveMode == ReceiveMode.MANUAL);
105 }
106 } catch (IOException e) {
107 throw new IOErrorException("Failed to use inherited socket", e);
108 }
109 final var socketFile = ns.<File>get("socket");
110 if (socketFile != null) {
111 final var address = UnixDomainSocketAddress.of(socketFile.toPath());
112 final var serverChannel = IOUtils.bindSocket(address);
113 runSocketSingleAccount(m, serverChannel, receiveMode == ReceiveMode.MANUAL);
114 }
115 final var tcpAddress = ns.getString("tcp");
116 if (tcpAddress != null) {
117 final var address = IOUtils.parseInetSocketAddress(tcpAddress);
118 final var serverChannel = IOUtils.bindSocket(address);
119 runSocketSingleAccount(m, serverChannel, receiveMode == ReceiveMode.MANUAL);
120 }
121 final var isDbusSystem = Boolean.TRUE.equals(ns.getBoolean("dbus-system"));
122 if (isDbusSystem) {
123 runDbusSingleAccount(m, true, receiveMode != ReceiveMode.ON_START);
124 }
125 final var isDbusSession = Boolean.TRUE.equals(ns.getBoolean("dbus"));
126 if (isDbusSession || (
127 !isDbusSystem
128 && socketFile == null
129 && tcpAddress == null
130 && !(inheritedChannel instanceof ServerSocketChannel)
131 )) {
132 runDbusSingleAccount(m, false, receiveMode != ReceiveMode.ON_START);
133 }
134
135 m.addClosedListener(() -> {
136 synchronized (this) {
137 notifyAll();
138 }
139 });
140
141 synchronized (this) {
142 try {
143 wait();
144 } catch (InterruptedException ignored) {
145 }
146 }
147 }
148
149 @Override
150 public void handleCommand(
151 final Namespace ns, final MultiAccountManager c, final OutputWriter outputWriter
152 ) throws CommandException {
153 logger.info("Starting daemon in multi-account mode");
154 final var noReceiveStdOut = Boolean.TRUE.equals(ns.getBoolean("no-receive-stdout"));
155 final var receiveMode = ns.<ReceiveMode>get("receive-mode");
156 final var ignoreAttachments = Boolean.TRUE.equals(ns.getBoolean("ignore-attachments"));
157
158 c.getAccountNumbers().stream().map(c::getManager).filter(Objects::nonNull).forEach(m -> {
159 m.setIgnoreAttachments(ignoreAttachments);
160 addDefaultReceiveHandler(m, noReceiveStdOut ? null : outputWriter, receiveMode != ReceiveMode.ON_START);
161 });
162 c.addOnManagerAddedHandler(m -> {
163 m.setIgnoreAttachments(ignoreAttachments);
164 addDefaultReceiveHandler(m, noReceiveStdOut ? null : outputWriter, receiveMode != ReceiveMode.ON_START);
165 });
166
167 final Channel inheritedChannel;
168 try {
169 inheritedChannel = System.inheritedChannel();
170 if (inheritedChannel instanceof ServerSocketChannel serverChannel) {
171 logger.info("Using inherited socket: " + serverChannel.getLocalAddress());
172 runSocketMultiAccount(c, serverChannel, receiveMode == ReceiveMode.MANUAL);
173 }
174 } catch (IOException e) {
175 throw new IOErrorException("Failed to use inherited socket", e);
176 }
177 final var socketFile = ns.<File>get("socket");
178 if (socketFile != null) {
179 final var address = UnixDomainSocketAddress.of(socketFile.toPath());
180 final var serverChannel = IOUtils.bindSocket(address);
181 runSocketMultiAccount(c, serverChannel, receiveMode == ReceiveMode.MANUAL);
182 }
183 final var tcpAddress = ns.getString("tcp");
184 if (tcpAddress != null) {
185 final var address = IOUtils.parseInetSocketAddress(tcpAddress);
186 final var serverChannel = IOUtils.bindSocket(address);
187 runSocketMultiAccount(c, serverChannel, receiveMode == ReceiveMode.MANUAL);
188 }
189 final var isDbusSystem = Boolean.TRUE.equals(ns.getBoolean("dbus-system"));
190 if (isDbusSystem) {
191 runDbusMultiAccount(c, receiveMode != ReceiveMode.ON_START, true);
192 }
193 final var isDbusSession = Boolean.TRUE.equals(ns.getBoolean("dbus"));
194 if (isDbusSession || (
195 !isDbusSystem
196 && socketFile == null
197 && tcpAddress == null
198 && !(inheritedChannel instanceof ServerSocketChannel)
199 )) {
200 runDbusMultiAccount(c, receiveMode != ReceiveMode.ON_START, false);
201 }
202
203 synchronized (this) {
204 try {
205 wait();
206 } catch (InterruptedException ignored) {
207 }
208 }
209 }
210
211 private void addDefaultReceiveHandler(Manager m, OutputWriter outputWriter, final boolean isWeakListener) {
212 final var handler = outputWriter instanceof JsonWriter o
213 ? new JsonReceiveMessageHandler(m, o)
214 : outputWriter instanceof PlainTextWriter o
215 ? new ReceiveMessageHandler(m, o)
216 : Manager.ReceiveMessageHandler.EMPTY;
217 m.addReceiveHandler(handler, isWeakListener);
218 }
219
220 private void runSocketSingleAccount(
221 final Manager m, final ServerSocketChannel serverChannel, final boolean noReceiveOnStart
222 ) {
223 runSocket(serverChannel, channel -> {
224 final var handler = getSignalJsonRpcDispatcherHandler(channel, noReceiveOnStart);
225 handler.handleConnection(m);
226 });
227 }
228
229 private void runSocketMultiAccount(
230 final MultiAccountManager c, final ServerSocketChannel serverChannel, final boolean noReceiveOnStart
231 ) {
232 runSocket(serverChannel, channel -> {
233 final var handler = getSignalJsonRpcDispatcherHandler(channel, noReceiveOnStart);
234 handler.handleConnection(c);
235 });
236 }
237
238 private void runSocket(final ServerSocketChannel serverChannel, Consumer<SocketChannel> socketHandler) {
239 new Thread(() -> {
240 while (true) {
241 final SocketChannel channel;
242 final String clientString;
243 try {
244 channel = serverChannel.accept();
245 clientString = channel.getRemoteAddress() + " " + IOUtils.getUnixDomainPrincipal(channel);
246 logger.info("Accepted new client: " + clientString);
247 } catch (IOException e) {
248 logger.error("Failed to accept new socket connection", e);
249 synchronized (this) {
250 notifyAll();
251 }
252 break;
253 }
254 new Thread(() -> {
255 try (final var c = channel) {
256 socketHandler.accept(c);
257 logger.info("Connection closed: " + clientString);
258 } catch (IOException e) {
259 logger.warn("Failed to close channel", e);
260 }
261 }).start();
262 }
263 }).start();
264 }
265
266 private SignalJsonRpcDispatcherHandler getSignalJsonRpcDispatcherHandler(
267 final SocketChannel c, final boolean noReceiveOnStart
268 ) {
269 final var lineSupplier = IOUtils.getLineSupplier(Channels.newReader(c, StandardCharsets.UTF_8));
270 final var jsonOutputWriter = new JsonWriterImpl(Channels.newWriter(c, StandardCharsets.UTF_8));
271
272 return new SignalJsonRpcDispatcherHandler(jsonOutputWriter, lineSupplier, noReceiveOnStart);
273 }
274
275 private void runDbusSingleAccount(
276 final Manager m, final boolean isDbusSystem, final boolean noReceiveOnStart
277 ) throws UnexpectedErrorException {
278 runDbus(isDbusSystem, (conn, objectPath) -> {
279 try {
280 exportDbusObject(conn, objectPath, m, noReceiveOnStart).join();
281 } catch (InterruptedException ignored) {
282 }
283 });
284 }
285
286 private void runDbusMultiAccount(
287 final MultiAccountManager c, final boolean noReceiveOnStart, final boolean isDbusSystem
288 ) throws UnexpectedErrorException {
289 runDbus(isDbusSystem, (connection, objectPath) -> {
290 final var signalControl = new DbusSignalControlImpl(c, objectPath);
291 connection.exportObject(signalControl);
292
293 c.addOnManagerAddedHandler(m -> {
294 final var thread = exportMultiAccountManager(connection, m, noReceiveOnStart);
295 if (thread != null) {
296 try {
297 thread.join();
298 } catch (InterruptedException ignored) {
299 }
300 }
301 });
302 c.addOnManagerRemovedHandler(m -> {
303 final var path = DbusConfig.getObjectPath(m.getSelfNumber());
304 try {
305 final var object = connection.getExportedObject(null, path);
306 if (object instanceof DbusSignalImpl dbusSignal) {
307 dbusSignal.close();
308 }
309 } catch (DBusException ignored) {
310 }
311 connection.unExportObject(path);
312 });
313
314 final var initThreads = c.getAccountNumbers()
315 .stream()
316 .map(c::getManager)
317 .filter(Objects::nonNull)
318 .map(m -> exportMultiAccountManager(connection, m, noReceiveOnStart))
319 .filter(Objects::nonNull)
320 .collect(Collectors.toList());
321
322 for (var t : initThreads) {
323 try {
324 t.join();
325 } catch (InterruptedException ignored) {
326 }
327 }
328 });
329 }
330
331 private void runDbus(
332 final boolean isDbusSystem, DbusRunner dbusRunner
333 ) throws UnexpectedErrorException {
334 DBusConnection.DBusBusType busType;
335 if (isDbusSystem) {
336 busType = DBusConnection.DBusBusType.SYSTEM;
337 } else {
338 busType = DBusConnection.DBusBusType.SESSION;
339 }
340 try {
341 var conn = DBusConnection.getConnection(busType);
342 dbusRunner.run(conn, DbusConfig.getObjectPath());
343
344 conn.requestBusName(DbusConfig.getBusname());
345
346 logger.info("DBus daemon running on {} bus: {}", busType, DbusConfig.getBusname());
347 } catch (DBusException e) {
348 logger.error("Dbus command failed", e);
349 throw new UnexpectedErrorException("Dbus command failed", e);
350 }
351 }
352
353 private Thread exportMultiAccountManager(
354 final DBusConnection conn, final Manager m, final boolean noReceiveOnStart
355 ) {
356 try {
357 final var objectPath = DbusConfig.getObjectPath(m.getSelfNumber());
358 return exportDbusObject(conn, objectPath, m, noReceiveOnStart);
359 } catch (DBusException e) {
360 logger.error("Failed to export object", e);
361 return null;
362 }
363 }
364
365 private Thread exportDbusObject(
366 final DBusConnection conn, final String objectPath, final Manager m, final boolean noReceiveOnStart
367 ) throws DBusException {
368 final var signal = new DbusSignalImpl(m, conn, objectPath, noReceiveOnStart);
369 conn.exportObject(signal);
370 final var initThread = new Thread(signal::initObjects);
371 initThread.start();
372
373 logger.debug("Exported dbus object: " + objectPath);
374
375 return initThread;
376 }
377
378 interface DbusRunner {
379
380 void run(DBusConnection connection, String objectPath) throws DBusException;
381 }
382 }