]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/commands/DaemonCommand.java
Fix repackage if building with multiple java versions
[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.exceptions.DBusException;
27 import org.slf4j.Logger;
28 import org.slf4j.LoggerFactory;
29
30 import java.io.File;
31 import java.io.IOException;
32 import java.net.UnixDomainSocketAddress;
33 import java.nio.channels.Channel;
34 import java.nio.channels.Channels;
35 import java.nio.channels.ServerSocketChannel;
36 import java.nio.channels.SocketChannel;
37 import java.nio.charset.StandardCharsets;
38 import java.util.List;
39 import java.util.concurrent.atomic.AtomicInteger;
40 import java.util.function.Consumer;
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.getManagers().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 static final AtomicInteger threadNumber = new AtomicInteger(0);
239
240 private void runSocket(final ServerSocketChannel serverChannel, Consumer<SocketChannel> socketHandler) {
241 final var thread = new Thread(() -> {
242 while (true) {
243 final var connectionId = threadNumber.getAndIncrement();
244 final SocketChannel channel;
245 final String clientString;
246 try {
247 channel = serverChannel.accept();
248 clientString = channel.getRemoteAddress() + " " + IOUtils.getUnixDomainPrincipal(channel);
249 logger.info("Accepted new client connection {}: {}", connectionId, clientString);
250 } catch (IOException e) {
251 logger.error("Failed to accept new socket connection", e);
252 synchronized (this) {
253 notifyAll();
254 }
255 break;
256 }
257 final var connectionThread = new Thread(() -> {
258 try (final var c = channel) {
259 socketHandler.accept(c);
260 } catch (IOException e) {
261 logger.warn("Failed to close channel", e);
262 } catch (Throwable e) {
263 logger.warn("Connection handler failed, closing connection", e);
264 }
265 logger.info("Connection {} closed: {}", connectionId, clientString);
266 });
267 connectionThread.setName("daemon-connection-" + connectionId);
268 connectionThread.start();
269 }
270 });
271 thread.setName("daemon-listener");
272 thread.start();
273 }
274
275 private SignalJsonRpcDispatcherHandler getSignalJsonRpcDispatcherHandler(
276 final SocketChannel c, final boolean noReceiveOnStart
277 ) {
278 final var lineSupplier = IOUtils.getLineSupplier(Channels.newReader(c, StandardCharsets.UTF_8));
279 final var jsonOutputWriter = new JsonWriterImpl(Channels.newWriter(c, StandardCharsets.UTF_8));
280
281 return new SignalJsonRpcDispatcherHandler(jsonOutputWriter, lineSupplier, noReceiveOnStart);
282 }
283
284 private void runDbusSingleAccount(
285 final Manager m, final boolean isDbusSystem, final boolean noReceiveOnStart
286 ) throws CommandException {
287 runDbus(isDbusSystem, (conn, objectPath) -> {
288 try {
289 exportDbusObject(conn, objectPath, m, noReceiveOnStart).join();
290 } catch (InterruptedException ignored) {
291 }
292 });
293 }
294
295 private void runDbusMultiAccount(
296 final MultiAccountManager c, final boolean noReceiveOnStart, final boolean isDbusSystem
297 ) throws CommandException {
298 runDbus(isDbusSystem, (connection, objectPath) -> {
299 final var signalControl = new DbusSignalControlImpl(c, objectPath);
300 connection.exportObject(signalControl);
301
302 c.addOnManagerAddedHandler(m -> {
303 final var thread = exportMultiAccountManager(connection, m, noReceiveOnStart);
304 try {
305 thread.join();
306 } catch (InterruptedException ignored) {
307 }
308 });
309 c.addOnManagerRemovedHandler(m -> {
310 final var path = DbusConfig.getObjectPath(m.getSelfNumber());
311 try {
312 final var object = connection.getExportedObject(null, path);
313 if (object instanceof DbusSignalImpl dbusSignal) {
314 dbusSignal.close();
315 }
316 } catch (DBusException ignored) {
317 }
318 });
319
320 final var initThreads = c.getManagers()
321 .stream()
322 .map(m -> exportMultiAccountManager(connection, m, noReceiveOnStart))
323 .toList();
324
325 for (var t : initThreads) {
326 try {
327 t.join();
328 } catch (InterruptedException ignored) {
329 }
330 }
331 });
332 }
333
334 private void runDbus(
335 final boolean isDbusSystem, DbusRunner dbusRunner
336 ) throws CommandException {
337 DBusConnection.DBusBusType busType;
338 if (isDbusSystem) {
339 busType = DBusConnection.DBusBusType.SYSTEM;
340 } else {
341 busType = DBusConnection.DBusBusType.SESSION;
342 }
343 DBusConnection conn;
344 try {
345 conn = DBusConnection.getConnection(busType);
346 dbusRunner.run(conn, DbusConfig.getObjectPath());
347 } catch (DBusException e) {
348 throw new UnexpectedErrorException("Dbus command failed: " + e.getMessage(), e);
349 } catch (UnsupportedOperationException e) {
350 throw new UserErrorException("Failed to connect to Dbus: " + e.getMessage(), e);
351 }
352
353 try {
354 conn.requestBusName(DbusConfig.getBusname());
355 } catch (DBusException e) {
356 throw new UnexpectedErrorException("Dbus command failed, maybe signal-cli dbus daemon is already running: "
357 + e.getMessage(), e);
358 }
359
360 logger.info("DBus daemon running on {} bus: {}", busType, DbusConfig.getBusname());
361 }
362
363 private Thread exportMultiAccountManager(
364 final DBusConnection conn, final Manager m, final boolean noReceiveOnStart
365 ) {
366 final var objectPath = DbusConfig.getObjectPath(m.getSelfNumber());
367 return exportDbusObject(conn, objectPath, m, noReceiveOnStart);
368 }
369
370 private Thread exportDbusObject(
371 final DBusConnection conn, final String objectPath, final Manager m, final boolean noReceiveOnStart
372 ) {
373 final var signal = new DbusSignalImpl(m, conn, objectPath, noReceiveOnStart);
374 final var initThread = new Thread(signal::initObjects);
375 initThread.setName("dbus-init");
376 initThread.start();
377
378 return initThread;
379 }
380
381 interface DbusRunner {
382
383 void run(DBusConnection connection, String objectPath) throws DBusException;
384 }
385 }