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