]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/commands/DaemonCommand.java
Bump version to 0.12.8
[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").action(Arguments.storeTrue()).help("Expose a DBus interface on the user bus.");
50 subparser.addArgument("--dbus-system", "--system")
51 .action(Arguments.storeTrue())
52 .help("Expose a DBus interface on the system bus.");
53 subparser.addArgument("--socket")
54 .nargs("?")
55 .type(File.class)
56 .setConst(defaultSocketPath)
57 .help("Expose a JSON-RPC interface on a UNIX socket (default $XDG_RUNTIME_DIR/signal-cli/socket).");
58 subparser.addArgument("--tcp")
59 .nargs("?")
60 .setConst("localhost:7583")
61 .help("Expose a JSON-RPC interface on a TCP socket (default localhost:7583).");
62 subparser.addArgument("--http")
63 .nargs("?")
64 .setConst("localhost:8080")
65 .help("Expose a JSON-RPC interface as http endpoint (default localhost:8080).");
66 subparser.addArgument("--no-receive-stdout")
67 .help("Don’t print received messages to stdout.")
68 .action(Arguments.storeTrue());
69 subparser.addArgument("--receive-mode")
70 .help("Specify when to start receiving messages.")
71 .type(Arguments.enumStringType(ReceiveMode.class))
72 .setDefault(ReceiveMode.ON_START);
73 subparser.addArgument("--ignore-attachments")
74 .help("Don’t download attachments of received messages.")
75 .action(Arguments.storeTrue());
76 subparser.addArgument("--ignore-stories")
77 .help("Don’t receive story messages from the server.")
78 .action(Arguments.storeTrue());
79 subparser.addArgument("--send-read-receipts")
80 .help("Send read receipts for all incoming data messages (in addition to the default delivery receipts)")
81 .action(Arguments.storeTrue());
82 }
83
84 @Override
85 public List<OutputType> getSupportedOutputTypes() {
86 return List.of(OutputType.PLAIN_TEXT, OutputType.JSON);
87 }
88
89 @Override
90 public void handleCommand(
91 final Namespace ns, final Manager m, final OutputWriter outputWriter
92 ) throws CommandException {
93 Shutdown.installHandler();
94 logger.info("Starting daemon in single-account mode for " + m.getSelfNumber());
95 final var noReceiveStdOut = Boolean.TRUE.equals(ns.getBoolean("no-receive-stdout"));
96 final var receiveMode = ns.<ReceiveMode>get("receive-mode");
97 final var receiveConfig = getReceiveConfig(ns);
98
99 m.setReceiveConfig(receiveConfig);
100 addDefaultReceiveHandler(m, noReceiveStdOut ? null : outputWriter, receiveMode != ReceiveMode.ON_START);
101
102 try (final var daemonHandler = new SingleAccountDaemonHandler(m, receiveMode)) {
103 setup(ns, daemonHandler);
104
105 m.addClosedListener(Shutdown::triggerShutdown);
106
107 try {
108 Shutdown.waitForShutdown();
109 } catch (InterruptedException ignored) {
110 }
111 }
112 }
113
114 @Override
115 public void handleCommand(
116 final Namespace ns, final MultiAccountManager c, final OutputWriter outputWriter
117 ) throws CommandException {
118 Shutdown.installHandler();
119 logger.info("Starting daemon in multi-account mode");
120 final var noReceiveStdOut = Boolean.TRUE.equals(ns.getBoolean("no-receive-stdout"));
121 final var receiveMode = ns.<ReceiveMode>get("receive-mode");
122 final var receiveConfig = getReceiveConfig(ns);
123 c.getManagers().forEach(m -> {
124 m.setReceiveConfig(receiveConfig);
125 addDefaultReceiveHandler(m, noReceiveStdOut ? null : outputWriter, receiveMode != ReceiveMode.ON_START);
126 });
127 c.addOnManagerAddedHandler(m -> {
128 m.setReceiveConfig(receiveConfig);
129 addDefaultReceiveHandler(m, noReceiveStdOut ? null : outputWriter, receiveMode != ReceiveMode.ON_START);
130 });
131
132 try (final var daemonHandler = new MultiAccountDaemonHandler(c, receiveMode)) {
133 setup(ns, daemonHandler);
134
135 synchronized (this) {
136 try {
137 Shutdown.waitForShutdown();
138 } catch (InterruptedException ignored) {
139 }
140 }
141 }
142 }
143
144 private static void setup(final Namespace ns, final DaemonHandler daemonHandler) throws CommandException {
145 final Channel inheritedChannel;
146 try {
147 if (System.inheritedChannel() instanceof ServerSocketChannel serverChannel) {
148 inheritedChannel = serverChannel;
149 logger.info("Using inherited socket: " + serverChannel.getLocalAddress());
150 daemonHandler.runSocket(serverChannel);
151 } else {
152 inheritedChannel = null;
153 }
154 } catch (IOException e) {
155 throw new IOErrorException("Failed to use inherited socket", e);
156 }
157
158 final var socketFile = ns.<File>get("socket");
159 if (socketFile != null) {
160 final var address = UnixDomainSocketAddress.of(socketFile.toPath());
161 final var serverChannel = IOUtils.bindSocket(address);
162 daemonHandler.runSocket(serverChannel);
163 }
164
165 final var tcpAddress = ns.getString("tcp");
166 if (tcpAddress != null) {
167 final var address = IOUtils.parseInetSocketAddress(tcpAddress);
168 final var serverChannel = IOUtils.bindSocket(address);
169 daemonHandler.runSocket(serverChannel);
170 }
171
172 final var httpAddress = ns.getString("http");
173 if (httpAddress != null) {
174 final var address = IOUtils.parseInetSocketAddress(httpAddress);
175 daemonHandler.runHttp(address);
176 }
177
178 final var isDbusSystem = Boolean.TRUE.equals(ns.getBoolean("dbus-system"));
179 if (isDbusSystem) {
180 daemonHandler.runDbus(true);
181 }
182
183 final var isDbusSession = Boolean.TRUE.equals(ns.getBoolean("dbus"));
184 if (isDbusSession || (
185 !isDbusSystem
186 && socketFile == null
187 && tcpAddress == null
188 && httpAddress == null
189 && inheritedChannel == null
190 )) {
191 logger.warn(
192 "Running daemon command without explicit mode is deprecated. Use --dbus to use the dbus interface.");
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 }