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
.OutputType
;
8 import org
.asamk
.signal
.ReceiveMessageHandler
;
9 import org
.asamk
.signal
.Shutdown
;
10 import org
.asamk
.signal
.commands
.exceptions
.CommandException
;
11 import org
.asamk
.signal
.commands
.exceptions
.IOErrorException
;
12 import org
.asamk
.signal
.dbus
.DbusHandler
;
13 import org
.asamk
.signal
.http
.HttpServerHandler
;
14 import org
.asamk
.signal
.json
.JsonReceiveMessageHandler
;
15 import org
.asamk
.signal
.jsonrpc
.SocketHandler
;
16 import org
.asamk
.signal
.manager
.Manager
;
17 import org
.asamk
.signal
.manager
.MultiAccountManager
;
18 import org
.asamk
.signal
.output
.JsonWriter
;
19 import org
.asamk
.signal
.output
.OutputWriter
;
20 import org
.asamk
.signal
.output
.PlainTextWriter
;
21 import org
.asamk
.signal
.util
.IOUtils
;
22 import org
.slf4j
.Logger
;
23 import org
.slf4j
.LoggerFactory
;
26 import java
.io
.IOException
;
27 import java
.net
.InetSocketAddress
;
28 import java
.net
.UnixDomainSocketAddress
;
29 import java
.nio
.channels
.Channel
;
30 import java
.nio
.channels
.ServerSocketChannel
;
31 import java
.util
.ArrayList
;
32 import java
.util
.List
;
34 import static org
.asamk
.signal
.util
.CommandUtil
.getReceiveConfig
;
36 public class DaemonCommand
implements MultiLocalCommand
, LocalCommand
{
38 private static final Logger logger
= LoggerFactory
.getLogger(DaemonCommand
.class);
41 public String
getName() {
46 public void attachToSubparser(final Subparser subparser
) {
47 final var defaultSocketPath
= new File(new File(IOUtils
.getRuntimeDir(), "signal-cli"), "socket");
48 subparser
.help("Run in daemon mode and provide a JSON-RPC or an experimental dbus interface.");
49 subparser
.addArgument("--dbus")
50 .action(Arguments
.storeTrue())
51 .help("Expose a DBus interface on the user bus (the default, if no other options are given).");
52 subparser
.addArgument("--dbus-system", "--system")
53 .action(Arguments
.storeTrue())
54 .help("Expose a DBus interface on the system bus.");
55 subparser
.addArgument("--socket")
58 .setConst(defaultSocketPath
)
59 .help("Expose a JSON-RPC interface on a UNIX socket (default $XDG_RUNTIME_DIR/signal-cli/socket).");
60 subparser
.addArgument("--tcp")
62 .setConst("localhost:7583")
63 .help("Expose a JSON-RPC interface on a TCP socket (default localhost:7583).");
64 subparser
.addArgument("--http")
66 .setConst("localhost:8080")
67 .help("Expose a JSON-RPC interface as http endpoint (default localhost:8080).");
68 subparser
.addArgument("--no-receive-stdout")
69 .help("Don’t print received messages to stdout.")
70 .action(Arguments
.storeTrue());
71 subparser
.addArgument("--receive-mode")
72 .help("Specify when to start receiving messages.")
73 .type(Arguments
.enumStringType(ReceiveMode
.class))
74 .setDefault(ReceiveMode
.ON_START
);
75 subparser
.addArgument("--ignore-attachments")
76 .help("Don’t download attachments of received messages.")
77 .action(Arguments
.storeTrue());
78 subparser
.addArgument("--ignore-stories")
79 .help("Don’t receive story messages from the server.")
80 .action(Arguments
.storeTrue());
81 subparser
.addArgument("--send-read-receipts")
82 .help("Send read receipts for all incoming data messages (in addition to the default delivery receipts)")
83 .action(Arguments
.storeTrue());
87 public List
<OutputType
> getSupportedOutputTypes() {
88 return List
.of(OutputType
.PLAIN_TEXT
, OutputType
.JSON
);
92 public void handleCommand(
93 final Namespace ns
, final Manager m
, final OutputWriter outputWriter
94 ) throws CommandException
{
95 Shutdown
.installHandler();
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 receiveConfig
= getReceiveConfig(ns
);
101 m
.setReceiveConfig(receiveConfig
);
102 addDefaultReceiveHandler(m
, noReceiveStdOut ?
null : outputWriter
, receiveMode
!= ReceiveMode
.ON_START
);
104 try (final var daemonHandler
= new SingleAccountDaemonHandler(m
, receiveMode
)) {
105 setup(ns
, daemonHandler
);
107 m
.addClosedListener(Shutdown
::triggerShutdown
);
110 Shutdown
.waitForShutdown();
111 } catch (InterruptedException ignored
) {
117 public void handleCommand(
118 final Namespace ns
, final MultiAccountManager c
, final OutputWriter outputWriter
119 ) throws CommandException
{
120 Shutdown
.installHandler();
121 logger
.info("Starting daemon in multi-account mode");
122 final var noReceiveStdOut
= Boolean
.TRUE
.equals(ns
.getBoolean("no-receive-stdout"));
123 final var receiveMode
= ns
.<ReceiveMode
>get("receive-mode");
124 final var receiveConfig
= getReceiveConfig(ns
);
125 c
.getManagers().forEach(m
-> {
126 m
.setReceiveConfig(receiveConfig
);
127 addDefaultReceiveHandler(m
, noReceiveStdOut ?
null : outputWriter
, receiveMode
!= ReceiveMode
.ON_START
);
129 c
.addOnManagerAddedHandler(m
-> {
130 m
.setReceiveConfig(receiveConfig
);
131 addDefaultReceiveHandler(m
, noReceiveStdOut ?
null : outputWriter
, receiveMode
!= ReceiveMode
.ON_START
);
134 try (final var daemonHandler
= new MultiAccountDaemonHandler(c
, receiveMode
)) {
135 setup(ns
, daemonHandler
);
137 synchronized (this) {
139 Shutdown
.waitForShutdown();
140 } catch (InterruptedException ignored
) {
146 private static void setup(final Namespace ns
, final DaemonHandler daemonHandler
) throws CommandException
{
147 final Channel inheritedChannel
;
149 if (System
.inheritedChannel() instanceof ServerSocketChannel serverChannel
) {
150 inheritedChannel
= serverChannel
;
151 logger
.info("Using inherited socket: " + serverChannel
.getLocalAddress());
152 daemonHandler
.runSocket(serverChannel
);
154 inheritedChannel
= null;
156 } catch (IOException e
) {
157 throw new IOErrorException("Failed to use inherited socket", e
);
160 final var socketFile
= ns
.<File
>get("socket");
161 if (socketFile
!= null) {
162 final var address
= UnixDomainSocketAddress
.of(socketFile
.toPath());
163 final var serverChannel
= IOUtils
.bindSocket(address
);
164 daemonHandler
.runSocket(serverChannel
);
167 final var tcpAddress
= ns
.getString("tcp");
168 if (tcpAddress
!= null) {
169 final var address
= IOUtils
.parseInetSocketAddress(tcpAddress
);
170 final var serverChannel
= IOUtils
.bindSocket(address
);
171 daemonHandler
.runSocket(serverChannel
);
174 final var httpAddress
= ns
.getString("http");
175 if (httpAddress
!= null) {
176 final var address
= IOUtils
.parseInetSocketAddress(httpAddress
);
177 daemonHandler
.runHttp(address
);
180 final var isDbusSystem
= Boolean
.TRUE
.equals(ns
.getBoolean("dbus-system"));
182 daemonHandler
.runDbus(true);
185 final var isDbusSession
= Boolean
.TRUE
.equals(ns
.getBoolean("dbus"));
186 if (isDbusSession
|| (
188 && socketFile
== null
189 && tcpAddress
== null
190 && httpAddress
== null
191 && inheritedChannel
== null
193 daemonHandler
.runDbus(false);
197 private void addDefaultReceiveHandler(Manager m
, OutputWriter outputWriter
, final boolean isWeakListener
) {
198 final var handler
= switch (outputWriter
) {
199 case PlainTextWriter writer
-> new ReceiveMessageHandler(m
, writer
);
200 case JsonWriter writer
-> new JsonReceiveMessageHandler(m
, writer
);
201 case null -> Manager
.ReceiveMessageHandler
.EMPTY
;
203 m
.addReceiveHandler(handler
, isWeakListener
);
206 private static abstract class DaemonHandler
implements AutoCloseable
{
208 protected final ReceiveMode receiveMode
;
209 protected final List
<AutoCloseable
> closeables
= new ArrayList
<>();
211 protected DaemonHandler(final ReceiveMode receiveMode
) {
212 this.receiveMode
= receiveMode
;
215 public abstract void runSocket(ServerSocketChannel serverChannel
) throws CommandException
;
217 public abstract void runDbus(boolean isDbusSystem
) throws CommandException
;
219 public abstract void runHttp(InetSocketAddress address
) throws CommandException
;
221 protected final void runSocket(final SocketHandler socketHandler
) {
222 socketHandler
.init();
223 this.closeables
.add(socketHandler
);
226 protected final void runDbus(
227 DbusHandler dbusHandler
228 ) throws CommandException
{
230 this.closeables
.add(dbusHandler
);
233 protected final void runHttp(final HttpServerHandler handler
) throws CommandException
{
236 } catch (IOException ex
) {
237 throw new IOErrorException("Failed to initialize HTTP Server", ex
);
239 this.closeables
.add(handler
);
243 public void close() {
244 for (final var closeable
: new ArrayList
<>(this.closeables
)) {
247 } catch (Exception e
) {
248 logger
.warn("Failed to close daemon handler", e
);
251 this.closeables
.clear();
255 private static final class SingleAccountDaemonHandler
extends DaemonHandler
{
257 private final Manager m
;
259 public SingleAccountDaemonHandler(final Manager m
, final ReceiveMode receiveMode
) {
265 public void runSocket(final ServerSocketChannel serverChannel
) {
266 runSocket(new SocketHandler(serverChannel
, m
, receiveMode
== ReceiveMode
.MANUAL
));
270 public void runDbus(final boolean isDbusSystem
) throws CommandException
{
271 runDbus(new DbusHandler(isDbusSystem
, m
, receiveMode
!= ReceiveMode
.ON_START
));
275 public void runHttp(InetSocketAddress address
) throws CommandException
{
276 runHttp(new HttpServerHandler(address
, m
));
280 private static final class MultiAccountDaemonHandler
extends DaemonHandler
{
282 private final MultiAccountManager c
;
284 public MultiAccountDaemonHandler(final MultiAccountManager c
, final ReceiveMode receiveMode
) {
290 public void runSocket(final ServerSocketChannel serverChannel
) {
291 runSocket(new SocketHandler(serverChannel
, c
, receiveMode
== ReceiveMode
.MANUAL
));
295 public void runDbus(final boolean isDbusSystem
) throws CommandException
{
296 runDbus(new DbusHandler(isDbusSystem
, c
, receiveMode
!= ReceiveMode
.ON_START
));
300 public void runHttp(final InetSocketAddress address
) throws CommandException
{
301 runHttp(new HttpServerHandler(address
, c
));