001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.eclipse.aether.named.ipc; 020 021import java.io.BufferedReader; 022import java.io.DataInputStream; 023import java.io.DataOutputStream; 024import java.io.IOException; 025import java.io.InputStreamReader; 026import java.net.SocketAddress; 027import java.nio.channels.ByteChannel; 028import java.nio.channels.Channels; 029import java.nio.channels.ServerSocketChannel; 030import java.nio.channels.SocketChannel; 031import java.nio.charset.StandardCharsets; 032import java.util.ArrayList; 033import java.util.Iterator; 034import java.util.List; 035import java.util.Map; 036import java.util.concurrent.CompletableFuture; 037import java.util.concurrent.ConcurrentHashMap; 038import java.util.concurrent.CopyOnWriteArrayList; 039import java.util.concurrent.TimeUnit; 040import java.util.concurrent.atomic.AtomicInteger; 041 042/** 043 * Implementation of the server side. 044 * The server instance is bound to a given maven repository. 045 * 046 * @since 2.0.1 047 */ 048public class IpcServer { 049 /** 050 * Should the IPC server not fork? (i.e. for testing purposes) 051 * 052 * @configurationSource {@link System#getProperty(String, String)} 053 * @configurationType {@link java.lang.Boolean} 054 * @configurationDefaultValue {@link #DEFAULT_NO_FORK} 055 */ 056 public static final String SYSTEM_PROP_NO_FORK = "aether.named.ipc.nofork"; 057 058 public static final boolean DEFAULT_NO_FORK = false; 059 060 /** 061 * IPC idle timeout in seconds. If there is no IPC request during idle time, it will stop. 062 * 063 * @configurationSource {@link System#getProperty(String, String)} 064 * @configurationType {@link java.lang.Integer} 065 * @configurationDefaultValue {@link #DEFAULT_IDLE_TIMEOUT} 066 */ 067 public static final String SYSTEM_PROP_IDLE_TIMEOUT = "aether.named.ipc.idleTimeout"; 068 069 public static final int DEFAULT_IDLE_TIMEOUT = 300; 070 071 /** 072 * IPC socket family to use. 073 * 074 * @configurationSource {@link System#getProperty(String, String)} 075 * @configurationType {@link java.lang.String} 076 * @configurationDefaultValue {@link #DEFAULT_FAMILY} 077 */ 078 public static final String SYSTEM_PROP_FAMILY = "aether.named.ipc.family"; 079 080 public static final String DEFAULT_FAMILY = "unix"; 081 082 /** 083 * Should the IPC server not use native executable? 084 * 085 * @configurationSource {@link System#getProperty(String, String)} 086 * @configurationType {@link java.lang.Boolean} 087 * @configurationDefaultValue {@link #DEFAULT_NO_NATIVE} 088 */ 089 public static final String SYSTEM_PROP_NO_NATIVE = "aether.named.ipc.nonative"; 090 091 public static final boolean DEFAULT_NO_NATIVE = true; 092 093 /** 094 * The name if the IPC server native executable (without file extension like ".exe") 095 * 096 * @configurationSource {@link System#getProperty(String, String)} 097 * @configurationType {@link java.lang.String} 098 * @configurationDefaultValue {@link #DEFAULT_NATIVE_NAME} 099 */ 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 * Should the IPC server log debug messages? (i.e. for testing purposes) 106 * 107 * @configurationSource {@link System#getProperty(String, String)} 108 * @configurationType {@link java.lang.Boolean} 109 * @configurationDefaultValue {@link #DEFAULT_DEBUG} 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 * @deprecated a server created without a bootstrap token refuses {@link IpcMessages#REQUEST_STOP} requests; 129 * use {@link #IpcServer(SocketFamily, String)} instead. 130 */ 131 @Deprecated 132 public IpcServer(SocketFamily family) throws IOException { 133 this(family, null); 134 } 135 136 /** 137 * Creates a server that honors a remote stop request only when it carries the given bootstrap token, which is 138 * shared exclusively with the client that spawned this server. The rest of the protocol is unauthenticated, 139 * but destructive cross-client operations (closing foreign contexts, stopping the daemon) are refused. 140 * 141 * @since 2.0.23 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 // When spawning a new process, the child process is create within 166 // the same process group. This means that a few signals are sent 167 // to the whole group. This is the case for SIGINT (Ctrl-C) and 168 // SIGTSTP (Ctrl-Z) which are both sent to all the processed in the 169 // group when initiated from the controlling terminal. 170 // This is only a problem when the client creates the daemon, but 171 // without ignoring those signals, a client being interrupted will 172 // also interrupt and kill the daemon. 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 // the bootstrap token is passed via stdin instead of argv: process arguments are commonly visible 187 // to other local users (e.g. /proc/<pid>/cmdline), and this token authorizes stopping the daemon 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); // this is one-off 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 // do not preallocate from an unauthenticated wire-supplied size; grow with actually received data 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 // contexts are scoped per connection: a client may only use contexts it created itself 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 // contexts are scoped per connection: a client may only close contexts it created itself 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 // the protocol is otherwise unauthenticated: only the client that spawned this 349 // server (and thus knows the bootstrap token) may stop it for everybody else 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 // ignore 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 // ignore 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}