]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/commands/DaemonCommand.java
Extend shutdown request with optional error
[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.DbusConfig;
8 import org.asamk.signal.OutputType;
9 import org.asamk.signal.ReceiveMessageHandler;
10 import org.asamk.signal.Shutdown;
11 import org.asamk.signal.commands.exceptions.CommandException;
12 import org.asamk.signal.commands.exceptions.IOErrorException;
13 import org.asamk.signal.dbus.DbusHandler;
14 import org.asamk.signal.http.HttpServerHandler;
15 import org.asamk.signal.json.JsonReceiveMessageHandler;
16 import org.asamk.signal.jsonrpc.SocketHandler;
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.OutputWriter;
21 import org.asamk.signal.output.PlainTextWriter;
22 import org.asamk.signal.util.IOUtils;
23 import org.slf4j.Logger;
24 import org.slf4j.LoggerFactory;
25
26 import java.io.File;
27 import java.io.IOException;
28 import java.net.InetSocketAddress;
29 import java.net.UnixDomainSocketAddress;
30 import java.nio.channels.Channel;
31 import java.nio.channels.ServerSocketChannel;
32 import java.util.ArrayList;
33 import java.util.List;
34
35 import static org.asamk.signal.util.CommandUtil.getReceiveConfig;
36
37 public class DaemonCommand implements MultiLocalCommand, LocalCommand {
38
39 private static final Logger logger = LoggerFactory.getLogger(DaemonCommand.class);
40
41 @Override
42 public String getName() {
43 return "daemon";
44 }
45
46 @Override
47 public void attachToSubparser(final Subparser subparser) {
48 final var defaultSocketPath = new File(new File(IOUtils.getRuntimeDir(), "signal-cli"), "socket");
49 subparser.help("Run in daemon mode and provide a JSON-RPC or an experimental dbus interface.");
50 subparser.addArgument("--dbus").action(Arguments.storeTrue()).help("Expose a DBus interface on the user bus.");
51 subparser.addArgument("--dbus-system", "--system")
52 .action(Arguments.storeTrue())
53 .help("Expose a DBus interface on the system bus.");
54 subparser.addArgument("--bus-name")
55 .setDefault(DbusConfig.getBusname())
56 .help("Specify the D-Bus bus name to connect to.");
57 subparser.addArgument("--socket")
58 .nargs("?")
59 .type(File.class)
60 .setConst(defaultSocketPath)
61 .help("Expose a JSON-RPC interface on a UNIX socket (default $XDG_RUNTIME_DIR/signal-cli/socket).");
62 subparser.addArgument("--tcp")
63 .nargs("?")
64 .setConst("localhost:7583")
65 .help("Expose a JSON-RPC interface on a TCP socket (default localhost:7583).");
66 subparser.addArgument("--http")
67 .nargs("?")
68 .setConst("localhost:8080")
69 .help("Expose a JSON-RPC interface as http endpoint (default localhost:8080).");
70 subparser.addArgument("--no-receive-stdout")
71 .help("Don’t print received messages to stdout.")
72 .action(Arguments.storeTrue());
73 subparser.addArgument("--receive-mode")
74 .help("Specify when to start receiving messages.")
75 .type(Arguments.enumStringType(ReceiveMode.class))
76 .setDefault(ReceiveMode.ON_START);
77 subparser.addArgument("--ignore-attachments")
78 .help("Don’t download attachments of received messages.")
79 .action(Arguments.storeTrue());
80 subparser.addArgument("--ignore-stories")
81 .help("Don’t receive story messages from the server.")
82 .action(Arguments.storeTrue());
83 subparser.addArgument("--send-read-receipts")
84 .help("Send read receipts for all incoming data messages (in addition to the default delivery receipts)")
85 .action(Arguments.storeTrue());
86 }
87
88 @Override
89 public List<OutputType> getSupportedOutputTypes() {
90 return List.of(OutputType.PLAIN_TEXT, OutputType.JSON);
91 }
92
93 @Override
94 public void handleCommand(
95 final Namespace ns,
96 final Manager m,
97 final OutputWriter outputWriter
98 ) throws CommandException {
99 Shutdown.installHandler();
100 logger.info("Starting daemon in single-account mode for {}", m.getSelfNumber());
101 final var noReceiveStdOut = Boolean.TRUE.equals(ns.getBoolean("no-receive-stdout"));
102 final var receiveMode = ns.<ReceiveMode>get("receive-mode");
103 final var receiveConfig = getReceiveConfig(ns);
104
105 m.setReceiveConfig(receiveConfig);
106 addDefaultReceiveHandler(m, noReceiveStdOut ? null : outputWriter, receiveMode != ReceiveMode.ON_START);
107
108 try (final var daemonHandler = new SingleAccountDaemonHandler(m, receiveMode)) {
109 setup(ns, daemonHandler);
110
111 m.addClosedListener(Shutdown::triggerShutdown);
112
113 try {
114 Shutdown.waitForShutdown();
115 } catch (InterruptedException ignored) {
116 }
117 }
118 }
119
120 @Override
121 public void handleCommand(
122 final Namespace ns,
123 final MultiAccountManager c,
124 final OutputWriter outputWriter
125 ) throws CommandException {
126 Shutdown.installHandler();
127 logger.info("Starting daemon in multi-account mode");
128 final var noReceiveStdOut = Boolean.TRUE.equals(ns.getBoolean("no-receive-stdout"));
129 final var receiveMode = ns.<ReceiveMode>get("receive-mode");
130 final var receiveConfig = getReceiveConfig(ns);
131 c.getManagers().forEach(m -> {
132 m.setReceiveConfig(receiveConfig);
133 addDefaultReceiveHandler(m, noReceiveStdOut ? null : outputWriter, receiveMode != ReceiveMode.ON_START);
134 });
135 c.addOnManagerAddedHandler(m -> {
136 m.setReceiveConfig(receiveConfig);
137 addDefaultReceiveHandler(m, noReceiveStdOut ? null : outputWriter, receiveMode != ReceiveMode.ON_START);
138 });
139
140 try (final var daemonHandler = new MultiAccountDaemonHandler(c, receiveMode)) {
141 setup(ns, daemonHandler);
142
143 synchronized (this) {
144 try {
145 Shutdown.waitForShutdown();
146 } catch (InterruptedException ignored) {
147 }
148 }
149 }
150 }
151
152 private static void setup(final Namespace ns, final DaemonHandler daemonHandler) throws CommandException {
153 final Channel inheritedChannel;
154 try {
155 if (System.inheritedChannel() instanceof ServerSocketChannel serverChannel) {
156 inheritedChannel = serverChannel;
157 logger.info("Using inherited socket: " + serverChannel.getLocalAddress());
158 daemonHandler.runSocket(serverChannel);
159 } else {
160 inheritedChannel = null;
161 }
162 } catch (IOException e) {
163 throw new IOErrorException("Failed to use inherited socket", e);
164 }
165
166 final var socketFile = ns.<File>get("socket");
167 if (socketFile != null) {
168 final var address = UnixDomainSocketAddress.of(socketFile.toPath());
169 final var serverChannel = IOUtils.bindSocket(address);
170 daemonHandler.runSocket(serverChannel);
171 }
172
173 final var tcpAddress = ns.getString("tcp");
174 if (tcpAddress != null) {
175 final var address = IOUtils.parseInetSocketAddress(tcpAddress);
176 final var serverChannel = IOUtils.bindSocket(address);
177 daemonHandler.runSocket(serverChannel);
178 }
179
180 final var httpAddress = ns.getString("http");
181 if (httpAddress != null) {
182 final var address = IOUtils.parseInetSocketAddress(httpAddress);
183 daemonHandler.runHttp(address);
184 }
185
186 final var isDbusSystem = Boolean.TRUE.equals(ns.getBoolean("dbus-system"));
187 if (isDbusSystem) {
188 final var busName = ns.getString("bus-name");
189 daemonHandler.runDbus(true, busName);
190 }
191
192 final var isDbusSession = Boolean.TRUE.equals(ns.getBoolean("dbus"));
193 if (isDbusSession) {
194 final var busName = ns.getString("bus-name");
195 daemonHandler.runDbus(false, busName);
196 }
197
198 if (!isDbusSystem
199 && !isDbusSession
200 && socketFile == null
201 && tcpAddress == null
202 && httpAddress == null
203 && inheritedChannel == null) {
204 logger.warn(
205 "Running daemon command without explicit mode is deprecated. Use 'daemon --dbus' to use the dbus interface.");
206 daemonHandler.runDbus(false, DbusConfig.getBusname());
207 }
208 }
209
210 private void addDefaultReceiveHandler(Manager m, OutputWriter outputWriter, final boolean isWeakListener) {
211 final var handler = switch (outputWriter) {
212 case PlainTextWriter writer -> new ReceiveMessageHandler(m, writer);
213 case JsonWriter writer -> new JsonReceiveMessageHandler(m, writer);
214 case null -> Manager.ReceiveMessageHandler.EMPTY;
215 };
216 m.addReceiveHandler(handler, isWeakListener);
217 }
218
219 private static abstract class DaemonHandler implements AutoCloseable {
220
221 protected final ReceiveMode receiveMode;
222 protected final List<AutoCloseable> closeables = new ArrayList<>();
223
224 protected DaemonHandler(final ReceiveMode receiveMode) {
225 this.receiveMode = receiveMode;
226 }
227
228 public abstract void runSocket(ServerSocketChannel serverChannel) throws CommandException;
229
230 public abstract void runDbus(boolean isDbusSystem, final String busname) throws CommandException;
231
232 public abstract void runHttp(InetSocketAddress address) throws CommandException;
233
234 protected final void runSocket(final SocketHandler socketHandler) {
235 socketHandler.init();
236 this.closeables.add(socketHandler);
237 }
238
239 protected final void runDbus(
240 DbusHandler dbusHandler
241 ) throws CommandException {
242 dbusHandler.init();
243 this.closeables.add(dbusHandler);
244 }
245
246 protected final void runHttp(final HttpServerHandler handler) throws CommandException {
247 try {
248 handler.init();
249 } catch (IOException ex) {
250 throw new IOErrorException("Failed to initialize HTTP Server", ex);
251 }
252 this.closeables.add(handler);
253 }
254
255 @Override
256 public void close() {
257 for (final var closeable : new ArrayList<>(this.closeables)) {
258 try {
259 closeable.close();
260 } catch (Exception e) {
261 logger.warn("Failed to close daemon handler", e);
262 }
263 }
264 this.closeables.clear();
265 }
266 }
267
268 private static final class SingleAccountDaemonHandler extends DaemonHandler {
269
270 private final Manager m;
271
272 public SingleAccountDaemonHandler(final Manager m, final ReceiveMode receiveMode) {
273 super(receiveMode);
274 this.m = m;
275 }
276
277 @Override
278 public void runSocket(final ServerSocketChannel serverChannel) {
279 runSocket(new SocketHandler(serverChannel, m, receiveMode == ReceiveMode.MANUAL));
280 }
281
282 @Override
283 public void runDbus(final boolean isDbusSystem, final String busname) throws CommandException {
284 runDbus(new DbusHandler(isDbusSystem, busname, m, receiveMode != ReceiveMode.ON_START));
285 }
286
287 @Override
288 public void runHttp(InetSocketAddress address) throws CommandException {
289 runHttp(new HttpServerHandler(address, m));
290 }
291 }
292
293 private static final class MultiAccountDaemonHandler extends DaemonHandler {
294
295 private final MultiAccountManager c;
296
297 public MultiAccountDaemonHandler(final MultiAccountManager c, final ReceiveMode receiveMode) {
298 super(receiveMode);
299 this.c = c;
300 }
301
302 @Override
303 public void runSocket(final ServerSocketChannel serverChannel) {
304 runSocket(new SocketHandler(serverChannel, c, receiveMode == ReceiveMode.MANUAL));
305 }
306
307 @Override
308 public void runDbus(final boolean isDbusSystem, final String busname) throws CommandException {
309 runDbus(new DbusHandler(isDbusSystem, busname, c, receiveMode != ReceiveMode.ON_START));
310 }
311
312 @Override
313 public void runHttp(final InetSocketAddress address) throws CommandException {
314 runHttp(new HttpServerHandler(address, c));
315 }
316 }
317 }