]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/commands/DaemonCommand.java
Refactor DaemonCommand
[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.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;
24
25 import java.io.File;
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;
33
34 import static org.asamk.signal.util.CommandUtil.getReceiveConfig;
35
36 public class DaemonCommand implements MultiLocalCommand, LocalCommand {
37
38 private static final Logger logger = LoggerFactory.getLogger(DaemonCommand.class);
39
40 @Override
41 public String getName() {
42 return "daemon";
43 }
44
45 @Override
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")
56 .nargs("?")
57 .type(File.class)
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")
61 .nargs("?")
62 .setConst("localhost:7583")
63 .help("Expose a JSON-RPC interface on a TCP socket (default localhost:7583).");
64 subparser.addArgument("--http")
65 .nargs("?")
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());
84 }
85
86 @Override
87 public List<OutputType> getSupportedOutputTypes() {
88 return List.of(OutputType.PLAIN_TEXT, OutputType.JSON);
89 }
90
91 @Override
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);
100
101 m.setReceiveConfig(receiveConfig);
102 addDefaultReceiveHandler(m, noReceiveStdOut ? null : outputWriter, receiveMode != ReceiveMode.ON_START);
103
104 try (final var daemonHandler = new SingleAccountDaemonHandler(m, receiveMode)) {
105 setup(ns, daemonHandler);
106
107 m.addClosedListener(Shutdown::triggerShutdown);
108
109 try {
110 Shutdown.waitForShutdown();
111 } catch (InterruptedException ignored) {
112 }
113 }
114 }
115
116 @Override
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);
128 });
129 c.addOnManagerAddedHandler(m -> {
130 m.setReceiveConfig(receiveConfig);
131 addDefaultReceiveHandler(m, noReceiveStdOut ? null : outputWriter, receiveMode != ReceiveMode.ON_START);
132 });
133
134 try (final var daemonHandler = new MultiAccountDaemonHandler(c, receiveMode)) {
135 setup(ns, daemonHandler);
136
137 synchronized (this) {
138 try {
139 Shutdown.waitForShutdown();
140 } catch (InterruptedException ignored) {
141 }
142 }
143 }
144 }
145
146 private static void setup(final Namespace ns, final DaemonHandler daemonHandler) throws CommandException {
147 final Channel inheritedChannel;
148 try {
149 if (System.inheritedChannel() instanceof ServerSocketChannel serverChannel) {
150 inheritedChannel = serverChannel;
151 logger.info("Using inherited socket: " + serverChannel.getLocalAddress());
152 daemonHandler.runSocket(serverChannel);
153 } else {
154 inheritedChannel = null;
155 }
156 } catch (IOException e) {
157 throw new IOErrorException("Failed to use inherited socket", e);
158 }
159
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);
165 }
166
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);
172 }
173
174 final var httpAddress = ns.getString("http");
175 if (httpAddress != null) {
176 final var address = IOUtils.parseInetSocketAddress(httpAddress);
177 daemonHandler.runHttp(address);
178 }
179
180 final var isDbusSystem = Boolean.TRUE.equals(ns.getBoolean("dbus-system"));
181 if (isDbusSystem) {
182 daemonHandler.runDbus(true);
183 }
184
185 final var isDbusSession = Boolean.TRUE.equals(ns.getBoolean("dbus"));
186 if (isDbusSession || (
187 !isDbusSystem
188 && socketFile == null
189 && tcpAddress == null
190 && httpAddress == null
191 && inheritedChannel == null
192 )) {
193 daemonHandler.runDbus(false);
194 }
195 }
196
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;
202 };
203 m.addReceiveHandler(handler, isWeakListener);
204 }
205
206 private static abstract class DaemonHandler implements AutoCloseable {
207
208 protected final ReceiveMode receiveMode;
209 protected final List<AutoCloseable> closeables = new ArrayList<>();
210
211 protected DaemonHandler(final ReceiveMode receiveMode) {
212 this.receiveMode = receiveMode;
213 }
214
215 public abstract void runSocket(ServerSocketChannel serverChannel) throws CommandException;
216
217 public abstract void runDbus(boolean isDbusSystem) throws CommandException;
218
219 public abstract void runHttp(InetSocketAddress address) throws CommandException;
220
221 protected final void runSocket(final SocketHandler socketHandler) {
222 socketHandler.init();
223 this.closeables.add(socketHandler);
224 }
225
226 protected final void runDbus(
227 DbusHandler dbusHandler
228 ) throws CommandException {
229 dbusHandler.init();
230 this.closeables.add(dbusHandler);
231 }
232
233 protected final void runHttp(final HttpServerHandler handler) throws CommandException {
234 try {
235 handler.init();
236 } catch (IOException ex) {
237 throw new IOErrorException("Failed to initialize HTTP Server", ex);
238 }
239 this.closeables.add(handler);
240 }
241
242 @Override
243 public void close() {
244 for (final var closeable : new ArrayList<>(this.closeables)) {
245 try {
246 closeable.close();
247 } catch (Exception e) {
248 logger.warn("Failed to close daemon handler", e);
249 }
250 }
251 this.closeables.clear();
252 }
253 }
254
255 private static final class SingleAccountDaemonHandler extends DaemonHandler {
256
257 private final Manager m;
258
259 public SingleAccountDaemonHandler(final Manager m, final ReceiveMode receiveMode) {
260 super(receiveMode);
261 this.m = m;
262 }
263
264 @Override
265 public void runSocket(final ServerSocketChannel serverChannel) {
266 runSocket(new SocketHandler(serverChannel, m, receiveMode == ReceiveMode.MANUAL));
267 }
268
269 @Override
270 public void runDbus(final boolean isDbusSystem) throws CommandException {
271 runDbus(new DbusHandler(isDbusSystem, m, receiveMode != ReceiveMode.ON_START));
272 }
273
274 @Override
275 public void runHttp(InetSocketAddress address) throws CommandException {
276 runHttp(new HttpServerHandler(address, m));
277 }
278 }
279
280 private static final class MultiAccountDaemonHandler extends DaemonHandler {
281
282 private final MultiAccountManager c;
283
284 public MultiAccountDaemonHandler(final MultiAccountManager c, final ReceiveMode receiveMode) {
285 super(receiveMode);
286 this.c = c;
287 }
288
289 @Override
290 public void runSocket(final ServerSocketChannel serverChannel) {
291 runSocket(new SocketHandler(serverChannel, c, receiveMode == ReceiveMode.MANUAL));
292 }
293
294 @Override
295 public void runDbus(final boolean isDbusSystem) throws CommandException {
296 runDbus(new DbusHandler(isDbusSystem, c, receiveMode != ReceiveMode.ON_START));
297 }
298
299 @Override
300 public void runHttp(final InetSocketAddress address) throws CommandException {
301 runHttp(new HttpServerHandler(address, c));
302 }
303 }
304 }