1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.eclipse.aether.named.ipc;
20
21 import java.io.BufferedReader;
22 import java.io.DataInputStream;
23 import java.io.DataOutputStream;
24 import java.io.IOException;
25 import java.io.InputStreamReader;
26 import java.net.SocketAddress;
27 import java.nio.channels.ByteChannel;
28 import java.nio.channels.Channels;
29 import java.nio.channels.ServerSocketChannel;
30 import java.nio.channels.SocketChannel;
31 import java.nio.charset.StandardCharsets;
32 import java.util.ArrayList;
33 import java.util.Iterator;
34 import java.util.List;
35 import java.util.Map;
36 import java.util.concurrent.CompletableFuture;
37 import java.util.concurrent.ConcurrentHashMap;
38 import java.util.concurrent.CopyOnWriteArrayList;
39 import java.util.concurrent.TimeUnit;
40 import java.util.concurrent.atomic.AtomicInteger;
41
42
43
44
45
46
47
48 public class IpcServer {
49
50
51
52
53
54
55
56 public static final String SYSTEM_PROP_NO_FORK = "aether.named.ipc.nofork";
57
58 public static final boolean DEFAULT_NO_FORK = false;
59
60
61
62
63
64
65
66
67 public static final String SYSTEM_PROP_IDLE_TIMEOUT = "aether.named.ipc.idleTimeout";
68
69 public static final int DEFAULT_IDLE_TIMEOUT = 300;
70
71
72
73
74
75
76
77
78 public static final String SYSTEM_PROP_FAMILY = "aether.named.ipc.family";
79
80 public static final String DEFAULT_FAMILY = "unix";
81
82
83
84
85
86
87
88
89 public static final String SYSTEM_PROP_NO_NATIVE = "aether.named.ipc.nonative";
90
91 public static final boolean DEFAULT_NO_NATIVE = true;
92
93
94
95
96
97
98
99
100 public static final String SYSTEM_PROP_NATIVE_NAME = "aether.named.ipc.nativeName";
101
102 public static final String DEFAULT_NATIVE_NAME = "ipc-sync";
103
104
105
106
107
108
109
110
111 public static final String SYSTEM_PROP_DEBUG = "aether.named.ipc.debug";
112
113 public static final boolean DEFAULT_DEBUG = false;
114
115 private final ServerSocketChannel serverSocket;
116 private final Map<SocketChannel, Thread> clients = new ConcurrentHashMap<>();
117 private final AtomicInteger counter = new AtomicInteger();
118 private final Map<String, Lock> locks = new ConcurrentHashMap<>();
119 private final Map<String, Context> contexts = new ConcurrentHashMap<>();
120 private static final boolean DEBUG =
121 Boolean.parseBoolean(System.getProperty(SYSTEM_PROP_DEBUG, Boolean.toString(DEFAULT_DEBUG)));
122 private final long idleTimeout;
123 private final String bootstrapToken;
124 private volatile long lastUsed;
125 private volatile boolean closing;
126
127
128
129
130
131 @Deprecated
132 public IpcServer(SocketFamily family) throws IOException {
133 this(family, null);
134 }
135
136
137
138
139
140
141
142
143 public IpcServer(SocketFamily family, String bootstrapToken) throws IOException {
144 this.bootstrapToken = bootstrapToken;
145 serverSocket = family.openServerSocket();
146 long timeout = TimeUnit.SECONDS.toNanos(DEFAULT_IDLE_TIMEOUT);
147 String str = System.getProperty(SYSTEM_PROP_IDLE_TIMEOUT);
148 if (str != null) {
149 try {
150 TimeUnit unit = TimeUnit.SECONDS;
151 if (str.endsWith("ms")) {
152 unit = TimeUnit.MILLISECONDS;
153 str = str.substring(0, str.length() - 2);
154 }
155 long dur = Long.parseLong(str);
156 timeout = unit.toNanos(dur);
157 } catch (NumberFormatException e) {
158 error("Property " + SYSTEM_PROP_IDLE_TIMEOUT + " specified with invalid value: " + str, e);
159 }
160 }
161 idleTimeout = timeout;
162 }
163
164 public static void main(String[] args) throws Exception {
165
166
167
168
169
170
171
172
173 try {
174 sun.misc.Signal.handle(new sun.misc.Signal("INT"), sun.misc.SignalHandler.SIG_IGN);
175 if (!IpcClient.IS_WINDOWS) {
176 sun.misc.Signal.handle(new sun.misc.Signal("TSTP"), sun.misc.SignalHandler.SIG_IGN);
177 }
178 } catch (Throwable t) {
179 error("Unable to ignore INT and TSTP signals", t);
180 }
181
182 String family = args[0];
183 String tmpAddress = args[1];
184 String rand = args[2];
185 if ("-".equals(rand)) {
186
187
188 BufferedReader reader = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8));
189 rand = reader.readLine();
190 if (rand == null || rand.isEmpty()) {
191 throw new IOException("Expected the bootstrap token on standard input");
192 }
193 }
194
195 runServer(SocketFamily.valueOf(family), tmpAddress, rand);
196 }
197
198 static IpcServer runServer(SocketFamily family, String tmpAddress, String rand) throws IOException {
199 IpcServer server = new IpcServer(family, rand);
200 run(server::run, false);
201 String address = SocketFamily.toString(server.getLocalAddress());
202 SocketAddress socketAddress = SocketFamily.fromString(tmpAddress);
203 try (SocketChannel socket = SocketChannel.open(socketAddress)) {
204 try (DataOutputStream dos = new DataOutputStream(Channels.newOutputStream(socket))) {
205 dos.writeUTF(rand);
206 dos.writeUTF(address);
207 dos.flush();
208 }
209 }
210
211 return server;
212 }
213
214 private static void debug(String msg, Object... args) {
215 if (DEBUG) {
216 System.out.printf("[ipc] [debug] " + msg + "\n", args);
217 }
218 }
219
220 private static void info(String msg, Object... args) {
221 System.out.printf("[ipc] [info] " + msg + "\n", args);
222 }
223
224 private static void error(String msg, Throwable t) {
225 System.out.println("[ipc] [error] " + msg);
226 t.printStackTrace(System.out);
227 }
228
229 private static void run(Runnable runnable, boolean daemon) {
230 Thread thread = new Thread(runnable);
231 if (daemon) {
232 thread.setDaemon(true);
233 }
234 thread.start();
235 }
236
237 public SocketAddress getLocalAddress() throws IOException {
238 return serverSocket.getLocalAddress();
239 }
240
241 public void run() {
242 try {
243 info("IpcServer started at %s", getLocalAddress().toString());
244 use();
245 run(this::expirationCheck, true);
246 while (!closing) {
247 SocketChannel socket = this.serverSocket.accept();
248 run(() -> client(socket), false);
249 }
250 } catch (Throwable t) {
251 if (!closing) {
252 error("Error running sync server loop", t);
253 }
254 }
255 }
256
257 private void client(SocketChannel socket) {
258 int c;
259 synchronized (clients) {
260 clients.put(socket, Thread.currentThread());
261 c = clients.size();
262 }
263 info("New client connected (%d connected)", c);
264 use();
265 Map<String, Context> clientContexts = new ConcurrentHashMap<>();
266 try {
267 ByteChannel wrapper = new ByteChannelWrapper(socket);
268 DataInputStream input = new DataInputStream(Channels.newInputStream(wrapper));
269 DataOutputStream output = new DataOutputStream(Channels.newOutputStream(wrapper));
270 while (!closing) {
271 int requestId = input.readInt();
272 int sz = input.readInt();
273 if (sz < 0) {
274 throw new IOException("Received invalid request size: " + sz);
275 }
276
277 List<String> request = new ArrayList<>(Math.min(sz, 1024));
278 for (int i = 0; i < sz; i++) {
279 request.add(input.readUTF());
280 }
281 if (request.isEmpty()) {
282 throw new IOException("Received invalid request");
283 }
284 use();
285 String contextId;
286 Context context;
287 String command = request.remove(0);
288 switch (command) {
289 case IpcMessages.REQUEST_CONTEXT:
290 if (request.size() != 1) {
291 throw new IOException("Expected one argument for " + command + " but got " + request);
292 }
293 boolean shared = Boolean.parseBoolean(request.remove(0));
294 context = new Context(shared);
295 contexts.put(context.id, context);
296 clientContexts.put(context.id, context);
297 synchronized (output) {
298 debug("Created context %s", context.id);
299 output.writeInt(requestId);
300 output.writeInt(2);
301 output.writeUTF(IpcMessages.RESPONSE_CONTEXT);
302 output.writeUTF(context.id);
303 output.flush();
304 }
305 break;
306 case IpcMessages.REQUEST_ACQUIRE:
307 if (request.isEmpty()) {
308 throw new IOException(
309 "Expected at least one argument for " + command + " but got " + request);
310 }
311 contextId = request.remove(0);
312
313 context = clientContexts.get(contextId);
314 if (context == null) {
315 throw new IOException(
316 "Unknown context: " + contextId + ". Known contexts = " + clientContexts.keySet());
317 }
318 context.lock(request).thenRun(() -> sendAcquireResponse(output, socket, requestId, context));
319 break;
320 case IpcMessages.REQUEST_CLOSE:
321 if (request.size() != 1) {
322 throw new IOException("Expected one argument for " + command + " but got " + request);
323 }
324 contextId = request.remove(0);
325
326 context = clientContexts.remove(contextId);
327 if (context == null) {
328 throw new IOException(
329 "Unknown context: " + contextId + ". Known contexts = " + clientContexts.keySet());
330 }
331 contexts.remove(contextId);
332 context.unlock();
333 synchronized (output) {
334 debug("Closing context %s", context.id);
335 output.writeInt(requestId);
336 output.writeInt(1);
337 output.writeUTF(IpcMessages.RESPONSE_CLOSE);
338 output.flush();
339 }
340 break;
341 case IpcMessages.REQUEST_STOP:
342 if (request.size() > 1) {
343 throw new IOException(
344 "Expected at most one argument for " + command + " but got " + request);
345 }
346 String stopToken = request.isEmpty() ? null : request.remove(0);
347 if (bootstrapToken == null || !bootstrapToken.equals(stopToken)) {
348
349
350 throw new IOException("Stop request rejected: missing or invalid bootstrap token");
351 }
352 synchronized (output) {
353 debug("Stopping server");
354 output.writeInt(requestId);
355 output.writeInt(1);
356 output.writeUTF(IpcMessages.RESPONSE_STOP);
357 output.flush();
358 }
359 close();
360 break;
361 default:
362 throw new IOException("Unknown request: " + request.get(0));
363 }
364 }
365 } catch (Throwable t) {
366 if (!closing) {
367 error("Error processing request", t);
368 }
369 } finally {
370 if (!closing) {
371 info("Client disconnecting...");
372 }
373 clientContexts.values().forEach(context -> {
374 contexts.remove(context.id);
375 context.unlock();
376 });
377 try {
378 socket.close();
379 } catch (IOException ioException) {
380
381 }
382 synchronized (clients) {
383 clients.remove(socket);
384 c = clients.size();
385 }
386 if (!closing) {
387 info("%d clients remained", c);
388 }
389 }
390 }
391
392 private void sendAcquireResponse(DataOutputStream output, SocketChannel socket, int requestId, Context context) {
393 try {
394 synchronized (output) {
395 debug("Locking in context %s", context.id);
396 output.writeInt(requestId);
397 output.writeInt(1);
398 output.writeUTF(IpcMessages.RESPONSE_ACQUIRE);
399 output.flush();
400 }
401 } catch (IOException e) {
402 try {
403 socket.close();
404 } catch (IOException ioException) {
405 e.addSuppressed(ioException);
406 }
407 error("Error writing lock response", e);
408 }
409 }
410
411 private void use() {
412 lastUsed = System.nanoTime();
413 }
414
415 private void expirationCheck() {
416 while (true) {
417 long current = System.nanoTime();
418 long left = (lastUsed + idleTimeout) - current;
419 if (clients.isEmpty() && left < 0) {
420 info("IpcServer expired, closing");
421 close();
422 break;
423 } else {
424 try {
425 Thread.sleep(Math.max(1, TimeUnit.NANOSECONDS.toMillis(left)));
426 } catch (InterruptedException e) {
427 info("IpcServer expiration check interrupted, closing");
428 close();
429 break;
430 }
431 }
432 }
433 }
434
435 void close() {
436 closing = true;
437 try {
438 serverSocket.close();
439 } catch (IOException e) {
440 error("Error closing server socket", e);
441 }
442 clients.forEach((s, t) -> {
443 try {
444 s.close();
445 } catch (IOException e) {
446
447 }
448 t.interrupt();
449 });
450 }
451
452 static class Waiter {
453 final Context context;
454 final CompletableFuture<Void> future;
455
456 Waiter(Context context, CompletableFuture<Void> future) {
457 this.context = context;
458 this.future = future;
459 }
460 }
461
462 static class Lock {
463
464 final String key;
465
466 List<Context> holders;
467 List<Waiter> waiters;
468
469 Lock(String key) {
470 this.key = key;
471 }
472
473 public synchronized CompletableFuture<Void> lock(Context context) {
474 if (holders == null) {
475 holders = new ArrayList<>();
476 }
477 if (holders.isEmpty() || holders.get(0).shared && context.shared) {
478 holders.add(context);
479 return CompletableFuture.completedFuture(null);
480 }
481 if (waiters == null) {
482 waiters = new ArrayList<>();
483 }
484
485 CompletableFuture<Void> future = new CompletableFuture<>();
486 waiters.add(new Waiter(context, future));
487 return future;
488 }
489
490 public void unlock(Context context) {
491 List<CompletableFuture<Void>> toComplete;
492 synchronized (this) {
493 toComplete = new ArrayList<>();
494 if (holders.remove(context)) {
495 while (waiters != null
496 && !waiters.isEmpty()
497 && (holders.isEmpty() || holders.get(0).shared && waiters.get(0).context.shared)) {
498 Waiter waiter = waiters.remove(0);
499 holders.add(waiter.context);
500 toComplete.add(waiter.future);
501 }
502 } else if (waiters != null) {
503 for (Iterator<Waiter> it = waiters.iterator(); it.hasNext(); ) {
504 Waiter waiter = it.next();
505 if (waiter.context == context) {
506 it.remove();
507 waiter.future.cancel(false);
508 }
509 }
510 }
511 }
512 toComplete.forEach(f -> f.complete(null));
513 }
514
515 public synchronized boolean isEmpty() {
516 return (holders == null || holders.isEmpty()) && (waiters == null || waiters.isEmpty());
517 }
518 }
519
520 class Context {
521
522 final String id;
523 final boolean shared;
524 final List<String> locks = new CopyOnWriteArrayList<>();
525
526 Context(boolean shared) {
527 this.id = String.format("%08x", counter.incrementAndGet());
528 this.shared = shared;
529 }
530
531 public CompletableFuture<?> lock(List<String> keys) {
532 locks.addAll(keys);
533 CompletableFuture<?>[] futures = keys.stream()
534 .map(k -> IpcServer.this.locks.computeIfAbsent(k, Lock::new))
535 .map(l -> l.lock(this))
536 .toArray(CompletableFuture[]::new);
537 return CompletableFuture.allOf(futures);
538 }
539
540 public void unlock() {
541 locks.stream()
542 .map(k -> IpcServer.this.locks.computeIfAbsent(k, Lock::new))
543 .forEach(l -> {
544 l.unlock(this);
545 IpcServer.this.locks.compute(l.key, (k, v) -> (v == l && v.isEmpty()) ? null : v);
546 });
547 }
548 }
549 }