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
.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
;
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
;
41 public class DaemonCommand
implements MultiLocalCommand
, LocalCommand
{
43 private final static Logger logger
= LoggerFactory
.getLogger(DaemonCommand
.class);
46 public String
getName() {
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")
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")
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());
82 public List
<OutputType
> getSupportedOutputTypes() {
83 return List
.of(OutputType
.PLAIN_TEXT
, OutputType
.JSON
);
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"));
95 m
.setIgnoreAttachments(ignoreAttachments
);
96 addDefaultReceiveHandler(m
, noReceiveStdOut ?
null : outputWriter
, receiveMode
!= ReceiveMode
.ON_START
);
98 final Channel inheritedChannel
;
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
);
105 } catch (IOException e
) {
106 throw new IOErrorException("Failed to use inherited socket", e
);
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
);
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
);
120 final var isDbusSystem
= Boolean
.TRUE
.equals(ns
.getBoolean("dbus-system"));
122 runDbusSingleAccount(m
, true, receiveMode
!= ReceiveMode
.ON_START
);
124 final var isDbusSession
= Boolean
.TRUE
.equals(ns
.getBoolean("dbus"));
125 if (isDbusSession
|| (
127 && socketFile
== null
128 && tcpAddress
== null
129 && !(inheritedChannel
instanceof ServerSocketChannel
)
131 runDbusSingleAccount(m
, false, receiveMode
!= ReceiveMode
.ON_START
);
134 m
.addClosedListener(() -> {
135 synchronized (this) {
140 synchronized (this) {
143 } catch (InterruptedException ignored
) {
149 public void handleCommand(
150 final Namespace ns
, final MultiAccountManager c
, final OutputWriter outputWriter
151 ) throws CommandException
{
152 logger
.info("Starting daemon in multi-account mode");
153 final var noReceiveStdOut
= Boolean
.TRUE
.equals(ns
.getBoolean("no-receive-stdout"));
154 final var receiveMode
= ns
.<ReceiveMode
>get("receive-mode");
155 final var ignoreAttachments
= Boolean
.TRUE
.equals(ns
.getBoolean("ignore-attachments"));
157 c
.getAccountNumbers().stream().map(c
::getManager
).filter(Objects
::nonNull
).forEach(m
-> {
158 m
.setIgnoreAttachments(ignoreAttachments
);
159 addDefaultReceiveHandler(m
, noReceiveStdOut ?
null : outputWriter
, receiveMode
!= ReceiveMode
.ON_START
);
161 c
.addOnManagerAddedHandler(m
-> {
162 m
.setIgnoreAttachments(ignoreAttachments
);
163 addDefaultReceiveHandler(m
, noReceiveStdOut ?
null : outputWriter
, receiveMode
!= ReceiveMode
.ON_START
);
166 final Channel inheritedChannel
;
168 inheritedChannel
= System
.inheritedChannel();
169 if (inheritedChannel
instanceof ServerSocketChannel serverChannel
) {
170 logger
.info("Using inherited socket: " + serverChannel
.getLocalAddress());
171 runSocketMultiAccount(c
, serverChannel
, receiveMode
== ReceiveMode
.MANUAL
);
173 } catch (IOException e
) {
174 throw new IOErrorException("Failed to use inherited socket", e
);
176 final var socketFile
= ns
.<File
>get("socket");
177 if (socketFile
!= null) {
178 final var address
= UnixDomainSocketAddress
.of(socketFile
.toPath());
179 final var serverChannel
= IOUtils
.bindSocket(address
);
180 runSocketMultiAccount(c
, serverChannel
, receiveMode
== ReceiveMode
.MANUAL
);
182 final var tcpAddress
= ns
.getString("tcp");
183 if (tcpAddress
!= null) {
184 final var address
= IOUtils
.parseInetSocketAddress(tcpAddress
);
185 final var serverChannel
= IOUtils
.bindSocket(address
);
186 runSocketMultiAccount(c
, serverChannel
, receiveMode
== ReceiveMode
.MANUAL
);
188 final var isDbusSystem
= Boolean
.TRUE
.equals(ns
.getBoolean("dbus-system"));
190 runDbusMultiAccount(c
, receiveMode
!= ReceiveMode
.ON_START
, true);
192 final var isDbusSession
= Boolean
.TRUE
.equals(ns
.getBoolean("dbus"));
193 if (isDbusSession
|| (
195 && socketFile
== null
196 && tcpAddress
== null
197 && !(inheritedChannel
instanceof ServerSocketChannel
)
199 runDbusMultiAccount(c
, receiveMode
!= ReceiveMode
.ON_START
, false);
202 synchronized (this) {
205 } catch (InterruptedException ignored
) {
210 private void addDefaultReceiveHandler(Manager m
, OutputWriter outputWriter
, final boolean isWeakListener
) {
211 final var handler
= outputWriter
instanceof JsonWriter o
212 ?
new JsonReceiveMessageHandler(m
, o
)
213 : outputWriter
instanceof PlainTextWriter o
214 ?
new ReceiveMessageHandler(m
, o
)
215 : Manager
.ReceiveMessageHandler
.EMPTY
;
216 m
.addReceiveHandler(handler
, isWeakListener
);
219 private void runSocketSingleAccount(
220 final Manager m
, final ServerSocketChannel serverChannel
, final boolean noReceiveOnStart
222 runSocket(serverChannel
, channel
-> {
223 final var handler
= getSignalJsonRpcDispatcherHandler(channel
, noReceiveOnStart
);
224 handler
.handleConnection(m
);
228 private void runSocketMultiAccount(
229 final MultiAccountManager c
, final ServerSocketChannel serverChannel
, final boolean noReceiveOnStart
231 runSocket(serverChannel
, channel
-> {
232 final var handler
= getSignalJsonRpcDispatcherHandler(channel
, noReceiveOnStart
);
233 handler
.handleConnection(c
);
237 private void runSocket(final ServerSocketChannel serverChannel
, Consumer
<SocketChannel
> socketHandler
) {
240 final SocketChannel channel
;
241 final String clientString
;
243 channel
= serverChannel
.accept();
244 clientString
= channel
.getRemoteAddress() + " " + IOUtils
.getUnixDomainPrincipal(channel
);
245 logger
.info("Accepted new client: " + clientString
);
246 } catch (IOException e
) {
247 logger
.error("Failed to accept new socket connection", e
);
248 synchronized (this) {
254 try (final var c
= channel
) {
255 socketHandler
.accept(c
);
256 logger
.info("Connection closed: " + clientString
);
257 } catch (IOException e
) {
258 logger
.warn("Failed to close channel", e
);
265 private SignalJsonRpcDispatcherHandler
getSignalJsonRpcDispatcherHandler(
266 final SocketChannel c
, final boolean noReceiveOnStart
268 final var lineSupplier
= IOUtils
.getLineSupplier(Channels
.newReader(c
, StandardCharsets
.UTF_8
));
269 final var jsonOutputWriter
= new JsonWriterImpl(Channels
.newWriter(c
, StandardCharsets
.UTF_8
));
271 return new SignalJsonRpcDispatcherHandler(jsonOutputWriter
, lineSupplier
, noReceiveOnStart
);
274 private void runDbusSingleAccount(
275 final Manager m
, final boolean isDbusSystem
, final boolean noReceiveOnStart
276 ) throws UnexpectedErrorException
{
277 runDbus(isDbusSystem
, (conn
, objectPath
) -> {
279 exportDbusObject(conn
, objectPath
, m
, noReceiveOnStart
).join();
280 } catch (InterruptedException ignored
) {
285 private void runDbusMultiAccount(
286 final MultiAccountManager c
, final boolean noReceiveOnStart
, final boolean isDbusSystem
287 ) throws UnexpectedErrorException
{
288 runDbus(isDbusSystem
, (connection
, objectPath
) -> {
289 final var signalControl
= new DbusSignalControlImpl(c
, objectPath
);
290 connection
.exportObject(signalControl
);
292 c
.addOnManagerAddedHandler(m
-> {
293 final var thread
= exportMultiAccountManager(connection
, m
, noReceiveOnStart
);
294 if (thread
!= null) {
297 } catch (InterruptedException ignored
) {
301 c
.addOnManagerRemovedHandler(m
-> {
302 final var path
= DbusConfig
.getObjectPath(m
.getSelfNumber());
304 final var object
= connection
.getExportedObject(null, path
);
305 if (object
instanceof DbusSignalImpl dbusSignal
) {
308 } catch (DBusException ignored
) {
310 connection
.unExportObject(path
);
313 final var initThreads
= c
.getAccountNumbers()
316 .filter(Objects
::nonNull
)
317 .map(m
-> exportMultiAccountManager(connection
, m
, noReceiveOnStart
))
318 .filter(Objects
::nonNull
)
321 for (var t
: initThreads
) {
324 } catch (InterruptedException ignored
) {
330 private void runDbus(
331 final boolean isDbusSystem
, DbusRunner dbusRunner
332 ) throws UnexpectedErrorException
{
333 DBusConnection
.DBusBusType busType
;
335 busType
= DBusConnection
.DBusBusType
.SYSTEM
;
337 busType
= DBusConnection
.DBusBusType
.SESSION
;
340 var conn
= DBusConnection
.getConnection(busType
);
341 dbusRunner
.run(conn
, DbusConfig
.getObjectPath());
343 conn
.requestBusName(DbusConfig
.getBusname());
345 logger
.info("DBus daemon running on {} bus: {}", busType
, DbusConfig
.getBusname());
346 } catch (DBusException e
) {
347 logger
.error("Dbus command failed", e
);
348 throw new UnexpectedErrorException("Dbus command failed", e
);
352 private Thread
exportMultiAccountManager(
353 final DBusConnection conn
, final Manager m
, final boolean noReceiveOnStart
356 final var objectPath
= DbusConfig
.getObjectPath(m
.getSelfNumber());
357 return exportDbusObject(conn
, objectPath
, m
, noReceiveOnStart
);
358 } catch (DBusException e
) {
359 logger
.error("Failed to export object", e
);
364 private Thread
exportDbusObject(
365 final DBusConnection conn
, final String objectPath
, final Manager m
, final boolean noReceiveOnStart
366 ) throws DBusException
{
367 final var signal
= new DbusSignalImpl(m
, conn
, objectPath
, noReceiveOnStart
);
368 conn
.exportObject(signal
);
369 final var initThread
= new Thread(signal
::initObjects
);
372 logger
.debug("Exported dbus object: " + objectPath
);
377 interface DbusRunner
{
379 void run(DBusConnection connection
, String objectPath
) throws DBusException
;