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