1 package org
.asamk
.signal
.commands
;
3 import net
.sourceforge
.argparse4j
.impl
.Arguments
;
4 import net
.sourceforge
.argparse4j
.inf
.Namespace
;
5 import net
.sourceforge
.argparse4j
.inf
.Subparser
;
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
.manager
.api
.ReceiveConfig
;
21 import org
.asamk
.signal
.output
.JsonWriter
;
22 import org
.asamk
.signal
.output
.JsonWriterImpl
;
23 import org
.asamk
.signal
.output
.OutputWriter
;
24 import org
.asamk
.signal
.output
.PlainTextWriter
;
25 import org
.asamk
.signal
.util
.IOUtils
;
26 import org
.freedesktop
.dbus
.connections
.impl
.DBusConnection
;
27 import org
.freedesktop
.dbus
.connections
.impl
.DBusConnectionBuilder
;
28 import org
.freedesktop
.dbus
.exceptions
.DBusException
;
29 import org
.slf4j
.Logger
;
30 import org
.slf4j
.LoggerFactory
;
33 import java
.io
.IOException
;
34 import java
.net
.UnixDomainSocketAddress
;
35 import java
.nio
.channels
.Channel
;
36 import java
.nio
.channels
.Channels
;
37 import java
.nio
.channels
.ServerSocketChannel
;
38 import java
.nio
.channels
.SocketChannel
;
39 import java
.nio
.charset
.StandardCharsets
;
40 import java
.util
.List
;
41 import java
.util
.concurrent
.atomic
.AtomicInteger
;
42 import java
.util
.function
.Consumer
;
44 public class DaemonCommand
implements MultiLocalCommand
, LocalCommand
{
46 private final static Logger logger
= LoggerFactory
.getLogger(DaemonCommand
.class);
49 public String
getName() {
54 public void attachToSubparser(final Subparser subparser
) {
55 final var defaultSocketPath
= new File(new File(IOUtils
.getRuntimeDir(), "signal-cli"), "socket");
56 subparser
.help("Run in daemon mode and provide an experimental dbus or JSON-RPC interface.");
57 subparser
.addArgument("--dbus")
58 .action(Arguments
.storeTrue())
59 .help("Expose a DBus interface on the user bus (the default, if no other options are given).");
60 subparser
.addArgument("--dbus-system", "--system")
61 .action(Arguments
.storeTrue())
62 .help("Expose a DBus interface on the system bus.");
63 subparser
.addArgument("--socket")
66 .setConst(defaultSocketPath
)
67 .help("Expose a JSON-RPC interface on a UNIX socket (default $XDG_RUNTIME_DIR/signal-cli/socket).");
68 subparser
.addArgument("--tcp")
70 .setConst("localhost:7583")
71 .help("Expose a JSON-RPC interface on a TCP socket (default localhost:7583).");
72 subparser
.addArgument("--no-receive-stdout")
73 .help("Don’t print received messages to stdout.")
74 .action(Arguments
.storeTrue());
75 subparser
.addArgument("--receive-mode")
76 .help("Specify when to start receiving messages.")
77 .type(Arguments
.enumStringType(ReceiveMode
.class))
78 .setDefault(ReceiveMode
.ON_START
);
79 subparser
.addArgument("--ignore-attachments")
80 .help("Don’t download attachments of received messages.")
81 .action(Arguments
.storeTrue());
82 subparser
.addArgument("--send-read-receipts")
83 .help("Send read receipts for all incoming data messages (in addition to the default delivery receipts)")
84 .action(Arguments
.storeTrue());
88 public List
<OutputType
> getSupportedOutputTypes() {
89 return List
.of(OutputType
.PLAIN_TEXT
, OutputType
.JSON
);
93 public void handleCommand(
94 final Namespace ns
, final Manager m
, final OutputWriter outputWriter
95 ) throws CommandException
{
96 logger
.info("Starting daemon in single-account mode for " + m
.getSelfNumber());
97 final var noReceiveStdOut
= Boolean
.TRUE
.equals(ns
.getBoolean("no-receive-stdout"));
98 final var receiveMode
= ns
.<ReceiveMode
>get("receive-mode");
99 final var ignoreAttachments
= Boolean
.TRUE
.equals(ns
.getBoolean("ignore-attachments"));
100 final boolean sendReadReceipts
= Boolean
.TRUE
.equals(ns
.getBoolean("send-read-receipts"));
102 m
.setReceiveConfig(new ReceiveConfig(ignoreAttachments
, sendReadReceipts
));
103 addDefaultReceiveHandler(m
, noReceiveStdOut ?
null : outputWriter
, receiveMode
!= ReceiveMode
.ON_START
);
105 final Channel inheritedChannel
;
107 inheritedChannel
= System
.inheritedChannel();
108 if (inheritedChannel
instanceof ServerSocketChannel serverChannel
) {
109 logger
.info("Using inherited socket: " + serverChannel
.getLocalAddress());
110 runSocketSingleAccount(m
, serverChannel
, receiveMode
== ReceiveMode
.MANUAL
);
112 } catch (IOException e
) {
113 throw new IOErrorException("Failed to use inherited socket", e
);
115 final var socketFile
= ns
.<File
>get("socket");
116 if (socketFile
!= null) {
117 final var address
= UnixDomainSocketAddress
.of(socketFile
.toPath());
118 final var serverChannel
= IOUtils
.bindSocket(address
);
119 runSocketSingleAccount(m
, serverChannel
, receiveMode
== ReceiveMode
.MANUAL
);
121 final var tcpAddress
= ns
.getString("tcp");
122 if (tcpAddress
!= null) {
123 final var address
= IOUtils
.parseInetSocketAddress(tcpAddress
);
124 final var serverChannel
= IOUtils
.bindSocket(address
);
125 runSocketSingleAccount(m
, serverChannel
, receiveMode
== ReceiveMode
.MANUAL
);
127 final var isDbusSystem
= Boolean
.TRUE
.equals(ns
.getBoolean("dbus-system"));
129 runDbusSingleAccount(m
, true, receiveMode
!= ReceiveMode
.ON_START
);
131 final var isDbusSession
= Boolean
.TRUE
.equals(ns
.getBoolean("dbus"));
132 if (isDbusSession
|| (
134 && socketFile
== null
135 && tcpAddress
== null
136 && !(inheritedChannel
instanceof ServerSocketChannel
)
138 runDbusSingleAccount(m
, false, receiveMode
!= ReceiveMode
.ON_START
);
141 m
.addClosedListener(() -> {
142 synchronized (this) {
147 synchronized (this) {
150 } catch (InterruptedException ignored
) {
156 public void handleCommand(
157 final Namespace ns
, final MultiAccountManager c
, final OutputWriter outputWriter
158 ) throws CommandException
{
159 logger
.info("Starting daemon in multi-account mode");
160 final var noReceiveStdOut
= Boolean
.TRUE
.equals(ns
.getBoolean("no-receive-stdout"));
161 final var receiveMode
= ns
.<ReceiveMode
>get("receive-mode");
162 final var ignoreAttachments
= Boolean
.TRUE
.equals(ns
.getBoolean("ignore-attachments"));
163 final boolean sendReadReceipts
= Boolean
.TRUE
.equals(ns
.getBoolean("send-read-receipts"));
165 final var receiveConfig
= new ReceiveConfig(ignoreAttachments
, sendReadReceipts
);
166 c
.getManagers().forEach(m
-> {
167 m
.setReceiveConfig(receiveConfig
);
168 addDefaultReceiveHandler(m
, noReceiveStdOut ?
null : outputWriter
, receiveMode
!= ReceiveMode
.ON_START
);
170 c
.addOnManagerAddedHandler(m
-> {
171 m
.setReceiveConfig(receiveConfig
);
172 addDefaultReceiveHandler(m
, noReceiveStdOut ?
null : outputWriter
, receiveMode
!= ReceiveMode
.ON_START
);
175 final Channel inheritedChannel
;
177 inheritedChannel
= System
.inheritedChannel();
178 if (inheritedChannel
instanceof ServerSocketChannel serverChannel
) {
179 logger
.info("Using inherited socket: " + serverChannel
.getLocalAddress());
180 runSocketMultiAccount(c
, serverChannel
, receiveMode
== ReceiveMode
.MANUAL
);
182 } catch (IOException e
) {
183 throw new IOErrorException("Failed to use inherited socket", e
);
185 final var socketFile
= ns
.<File
>get("socket");
186 if (socketFile
!= null) {
187 final var address
= UnixDomainSocketAddress
.of(socketFile
.toPath());
188 final var serverChannel
= IOUtils
.bindSocket(address
);
189 runSocketMultiAccount(c
, serverChannel
, receiveMode
== ReceiveMode
.MANUAL
);
191 final var tcpAddress
= ns
.getString("tcp");
192 if (tcpAddress
!= null) {
193 final var address
= IOUtils
.parseInetSocketAddress(tcpAddress
);
194 final var serverChannel
= IOUtils
.bindSocket(address
);
195 runSocketMultiAccount(c
, serverChannel
, receiveMode
== ReceiveMode
.MANUAL
);
197 final var isDbusSystem
= Boolean
.TRUE
.equals(ns
.getBoolean("dbus-system"));
199 runDbusMultiAccount(c
, receiveMode
!= ReceiveMode
.ON_START
, true);
201 final var isDbusSession
= Boolean
.TRUE
.equals(ns
.getBoolean("dbus"));
202 if (isDbusSession
|| (
204 && socketFile
== null
205 && tcpAddress
== null
206 && !(inheritedChannel
instanceof ServerSocketChannel
)
208 runDbusMultiAccount(c
, receiveMode
!= ReceiveMode
.ON_START
, false);
211 synchronized (this) {
214 } catch (InterruptedException ignored
) {
219 private void addDefaultReceiveHandler(Manager m
, OutputWriter outputWriter
, final boolean isWeakListener
) {
220 final var handler
= outputWriter
instanceof JsonWriter o
221 ?
new JsonReceiveMessageHandler(m
, o
)
222 : outputWriter
instanceof PlainTextWriter o
223 ?
new ReceiveMessageHandler(m
, o
)
224 : Manager
.ReceiveMessageHandler
.EMPTY
;
225 m
.addReceiveHandler(handler
, isWeakListener
);
228 private void runSocketSingleAccount(
229 final Manager m
, final ServerSocketChannel serverChannel
, final boolean noReceiveOnStart
231 runSocket(serverChannel
, channel
-> {
232 final var handler
= getSignalJsonRpcDispatcherHandler(channel
, noReceiveOnStart
);
233 handler
.handleConnection(m
);
237 private void runSocketMultiAccount(
238 final MultiAccountManager c
, final ServerSocketChannel serverChannel
, final boolean noReceiveOnStart
240 runSocket(serverChannel
, channel
-> {
241 final var handler
= getSignalJsonRpcDispatcherHandler(channel
, noReceiveOnStart
);
242 handler
.handleConnection(c
);
246 private static final AtomicInteger threadNumber
= new AtomicInteger(0);
248 private void runSocket(final ServerSocketChannel serverChannel
, Consumer
<SocketChannel
> socketHandler
) {
249 final var thread
= new Thread(() -> {
251 final var connectionId
= threadNumber
.getAndIncrement();
252 final SocketChannel channel
;
253 final String clientString
;
255 channel
= serverChannel
.accept();
256 clientString
= channel
.getRemoteAddress() + " " + IOUtils
.getUnixDomainPrincipal(channel
);
257 logger
.info("Accepted new client connection {}: {}", connectionId
, clientString
);
258 } catch (IOException e
) {
259 logger
.error("Failed to accept new socket connection", e
);
260 synchronized (this) {
265 final var connectionThread
= new Thread(() -> {
266 try (final var c
= channel
) {
267 socketHandler
.accept(c
);
268 } catch (IOException e
) {
269 logger
.warn("Failed to close channel", e
);
270 } catch (Throwable e
) {
271 logger
.warn("Connection handler failed, closing connection", e
);
273 logger
.info("Connection {} closed: {}", connectionId
, clientString
);
275 connectionThread
.setName("daemon-connection-" + connectionId
);
276 connectionThread
.start();
279 thread
.setName("daemon-listener");
283 private SignalJsonRpcDispatcherHandler
getSignalJsonRpcDispatcherHandler(
284 final SocketChannel c
, final boolean noReceiveOnStart
286 final var lineSupplier
= IOUtils
.getLineSupplier(Channels
.newReader(c
, StandardCharsets
.UTF_8
));
287 final var jsonOutputWriter
= new JsonWriterImpl(Channels
.newWriter(c
, StandardCharsets
.UTF_8
));
289 return new SignalJsonRpcDispatcherHandler(jsonOutputWriter
, lineSupplier
, noReceiveOnStart
);
292 private void runDbusSingleAccount(
293 final Manager m
, final boolean isDbusSystem
, final boolean noReceiveOnStart
294 ) throws CommandException
{
295 runDbus(isDbusSystem
, (conn
, objectPath
) -> {
297 exportDbusObject(conn
, objectPath
, m
, noReceiveOnStart
).join();
298 } catch (InterruptedException ignored
) {
303 private void runDbusMultiAccount(
304 final MultiAccountManager c
, final boolean noReceiveOnStart
, final boolean isDbusSystem
305 ) throws CommandException
{
306 runDbus(isDbusSystem
, (connection
, objectPath
) -> {
307 final var signalControl
= new DbusSignalControlImpl(c
, objectPath
);
308 connection
.exportObject(signalControl
);
310 c
.addOnManagerAddedHandler(m
-> {
311 final var thread
= exportMultiAccountManager(connection
, m
, noReceiveOnStart
);
314 } catch (InterruptedException ignored
) {
317 c
.addOnManagerRemovedHandler(m
-> {
318 final var path
= DbusConfig
.getObjectPath(m
.getSelfNumber());
320 final var object
= connection
.getExportedObject(null, path
);
321 if (object
instanceof DbusSignalImpl dbusSignal
) {
324 } catch (DBusException ignored
) {
328 final var initThreads
= c
.getManagers()
330 .map(m
-> exportMultiAccountManager(connection
, m
, noReceiveOnStart
))
333 for (var t
: initThreads
) {
336 } catch (InterruptedException ignored
) {
342 private void runDbus(
343 final boolean isDbusSystem
, DbusRunner dbusRunner
344 ) throws CommandException
{
345 DBusConnection
.DBusBusType busType
;
347 busType
= DBusConnection
.DBusBusType
.SYSTEM
;
349 busType
= DBusConnection
.DBusBusType
.SESSION
;
353 conn
= DBusConnectionBuilder
.forType(busType
).build();
354 dbusRunner
.run(conn
, DbusConfig
.getObjectPath());
355 } catch (DBusException e
) {
356 throw new UnexpectedErrorException("Dbus command failed: " + e
.getMessage(), e
);
357 } catch (UnsupportedOperationException e
) {
358 throw new UserErrorException("Failed to connect to Dbus: " + e
.getMessage(), e
);
362 conn
.requestBusName(DbusConfig
.getBusname());
363 } catch (DBusException e
) {
364 throw new UnexpectedErrorException("Dbus command failed, maybe signal-cli dbus daemon is already running: "
365 + e
.getMessage(), e
);
368 logger
.info("DBus daemon running on {} bus: {}", busType
, DbusConfig
.getBusname());
371 private Thread
exportMultiAccountManager(
372 final DBusConnection conn
, final Manager m
, final boolean noReceiveOnStart
374 final var objectPath
= DbusConfig
.getObjectPath(m
.getSelfNumber());
375 return exportDbusObject(conn
, objectPath
, m
, noReceiveOnStart
);
378 private Thread
exportDbusObject(
379 final DBusConnection conn
, final String objectPath
, final Manager m
, final boolean noReceiveOnStart
381 final var signal
= new DbusSignalImpl(m
, conn
, objectPath
, noReceiveOnStart
);
382 final var initThread
= new Thread(signal
::initObjects
);
383 initThread
.setName("dbus-init");
389 interface DbusRunner
{
391 void run(DBusConnection connection
, String objectPath
) throws DBusException
;