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 042import org.slf4j.LoggerFactory; 043 044/** 045 * Implementation of the server side. 046 * The server instance is bound to a given maven repository. 047 * 048 * @since 2.0.1 049 */ 050public class IpcServer { 051 static volatile boolean forkedProcess; 052 053 /** 054 * Should the IPC server not fork? (i.e. for testing purposes) 055 * 056 * @configurationSource {@link System#getProperty(String, String)} 057 * @configurationType {@link java.lang.Boolean} 058 * @configurationDefaultValue {@link #DEFAULT_NO_FORK} 059 */ 060 public static final String SYSTEM_PROP_NO_FORK = "aether.named.ipc.nofork"; 061 062 public static final boolean DEFAULT_NO_FORK = false; 063 064 /** 065 * IPC idle timeout in seconds. If there is no IPC request during idle time, it will stop. 066 * 067 * @configurationSource {@link System#getProperty(String, String)} 068 * @configurationType {@link java.lang.Integer} 069 * @configurationDefaultValue {@link #DEFAULT_IDLE_TIMEOUT} 070 */ 071 public static final String SYSTEM_PROP_IDLE_TIMEOUT = "aether.named.ipc.idleTimeout"; 072 073 public static final int DEFAULT_IDLE_TIMEOUT = 300; 074 075 /** 076 * IPC socket family to use. 077 * 078 * @configurationSource {@link System#getProperty(String, String)} 079 * @configurationType {@link java.lang.String} 080 * @configurationDefaultValue {@link #DEFAULT_FAMILY} 081 */ 082 public static final String SYSTEM_PROP_FAMILY = "aether.named.ipc.family"; 083 084 public static final String DEFAULT_FAMILY = "unix"; 085 086 /** 087 * Should the IPC server not use native executable? 088 * 089 * @configurationSource {@link System#getProperty(String, String)} 090 * @configurationType {@link java.lang.Boolean} 091 * @configurationDefaultValue {@link #DEFAULT_NO_NATIVE} 092 */ 093 public static final String SYSTEM_PROP_NO_NATIVE = "aether.named.ipc.nonative"; 094 095 public static final boolean DEFAULT_NO_NATIVE = true; 096 097 /** 098 * The name if the IPC server native executable (without file extension like ".exe") 099 * 100 * @configurationSource {@link System#getProperty(String, String)} 101 * @configurationType {@link java.lang.String} 102 * @configurationDefaultValue {@link #DEFAULT_NATIVE_NAME} 103 */ 104 public static final String SYSTEM_PROP_NATIVE_NAME = "aether.named.ipc.nativeName"; 105 106 public static final String DEFAULT_NATIVE_NAME = "ipc-sync"; 107 108 /** 109 * Should the IPC server log debug messages? (i.e. for testing purposes) 110 * 111 * @configurationSource {@link System#getProperty(String, String)} 112 * @configurationType {@link java.lang.Boolean} 113 * @configurationDefaultValue {@link #DEFAULT_DEBUG} 114 */ 115 public static final String SYSTEM_PROP_DEBUG = "aether.named.ipc.debug"; 116 117 public static final boolean DEFAULT_DEBUG = false; 118 119 private final ServerSocketChannel serverSocket; 120 private final Map<SocketChannel, Thread> clients = new ConcurrentHashMap<>(); 121 private final AtomicInteger counter = new AtomicInteger(); 122 private final Map<String, Lock> locks = new ConcurrentHashMap<>(); 123 private final Map<String, Context> contexts = new ConcurrentHashMap<>(); 124 private static final boolean DEBUG = 125 Boolean.parseBoolean(System.getProperty(SYSTEM_PROP_DEBUG, Boolean.toString(DEFAULT_DEBUG))); 126 private final long idleTimeout; 127 private final String bootstrapToken; 128 private volatile long lastUsed; 129 private volatile boolean closing; 130 131 /** 132 * @deprecated a server created without a bootstrap token refuses {@link IpcMessages#REQUEST_STOP} requests; 133 * use {@link #IpcServer(SocketFamily, String)} instead. 134 */ 135 @Deprecated 136 public IpcServer(SocketFamily family) throws IOException { 137 this(family, null); 138 } 139 140 /** 141 * Creates a server that honors a remote stop request only when it carries the given bootstrap token, which is 142 * shared exclusively with the client that spawned this server. The rest of the protocol is unauthenticated, 143 * but destructive cross-client operations (closing foreign contexts, stopping the daemon) are refused. 144 * 145 * @since 2.0.23 146 */ 147 public IpcServer(SocketFamily family, String bootstrapToken) throws IOException { 148 this.bootstrapToken = bootstrapToken; 149 serverSocket = family.openServerSocket(); 150 long timeout = TimeUnit.SECONDS.toNanos(DEFAULT_IDLE_TIMEOUT); 151 String str = System.getProperty(SYSTEM_PROP_IDLE_TIMEOUT); 152 if (str != null) { 153 try { 154 TimeUnit unit = TimeUnit.SECONDS; 155 if (str.endsWith("ms")) { 156 unit = TimeUnit.MILLISECONDS; 157 str = str.substring(0, str.length() - 2); 158 } 159 long dur = Long.parseLong(str); 160 timeout = unit.toNanos(dur); 161 } catch (NumberFormatException e) { 162 error("Property " + SYSTEM_PROP_IDLE_TIMEOUT + " specified with invalid value: " + str, e); 163 } 164 } 165 idleTimeout = timeout; 166 } 167 168 public static void main(String[] args) throws Exception { 169 forkedProcess = true; 170 // When spawning a new process, the child process is create within 171 // the same process group. This means that a few signals are sent 172 // to the whole group. This is the case for SIGINT (Ctrl-C) and 173 // SIGTSTP (Ctrl-Z) which are both sent to all the processed in the 174 // group when initiated from the controlling terminal. 175 // This is only a problem when the client creates the daemon, but 176 // without ignoring those signals, a client being interrupted will 177 // also interrupt and kill the daemon. 178 try { 179 sun.misc.Signal.handle(new sun.misc.Signal("INT"), sun.misc.SignalHandler.SIG_IGN); 180 if (!IpcClient.IS_WINDOWS) { 181 sun.misc.Signal.handle(new sun.misc.Signal("TSTP"), sun.misc.SignalHandler.SIG_IGN); 182 } 183 } catch (Throwable t) { 184 error("Unable to ignore INT and TSTP signals", t); 185 } 186 187 String family = args[0]; 188 String tmpAddress = args[1]; 189 String rand = args[2]; 190 if ("-".equals(rand)) { 191 // the bootstrap token is passed via stdin instead of argv: process arguments are commonly visible 192 // to other local users (e.g. /proc/<pid>/cmdline), and this token authorizes stopping the daemon 193 BufferedReader reader = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8)); 194 rand = reader.readLine(); 195 if (rand == null || rand.isEmpty()) { 196 throw new IOException("Expected the bootstrap token on standard input"); 197 } 198 } 199 200 runServer(SocketFamily.valueOf(family), tmpAddress, rand); 201 } 202 203 static IpcServer runServer(SocketFamily family, String tmpAddress, String rand) throws IOException { 204 IpcServer server = new IpcServer(family, rand); 205 run(server::run, false); // this is one-off 206 String address = SocketFamily.toString(server.getLocalAddress()); 207 SocketAddress socketAddress = SocketFamily.fromString(tmpAddress); 208 try (SocketChannel socket = SocketChannel.open(socketAddress)) { 209 try (DataOutputStream dos = new DataOutputStream(Channels.newOutputStream(socket))) { 210 dos.writeUTF(rand); 211 dos.writeUTF(address); 212 dos.flush(); 213 } 214 } 215 216 return server; 217 } 218 219 private static void debug(String msg, Object... args) { 220 if (DEBUG) { 221 if (forkedProcess) { 222 System.out.println("[ipc] [debug] " + format(msg, args)); 223 } else { 224 LoggerFactory.getLogger(IpcServer.class).debug(msg, args); 225 } 226 } 227 } 228 229 private static void info(String msg, Object... args) { 230 if (forkedProcess) { 231 System.out.println("[ipc] [info] " + format(msg, args)); 232 } else { 233 LoggerFactory.getLogger(IpcServer.class).info(msg, args); 234 } 235 } 236 237 private static void error(String msg, Throwable t) { 238 if (forkedProcess) { 239 System.out.println("[ipc] [error] " + msg); 240 t.printStackTrace(System.out); 241 } else { 242 LoggerFactory.getLogger(IpcServer.class).error(msg, t); 243 } 244 } 245 246 private static String format(String msg, Object... args) { 247 return String.format(msg.replace("{}", "%s"), args); 248 } 249 250 private static void run(Runnable runnable, boolean daemon) { 251 Thread thread = new Thread(runnable); 252 if (daemon) { 253 thread.setDaemon(true); 254 } 255 thread.start(); 256 } 257 258 public SocketAddress getLocalAddress() throws IOException { 259 return serverSocket.getLocalAddress(); 260 } 261 262 public void run() { 263 try { 264 info("IpcServer started at {}", getLocalAddress().toString()); 265 use(); 266 run(this::expirationCheck, true); 267 while (!closing) { 268 SocketChannel socket = this.serverSocket.accept(); 269 run(() -> client(socket), false); 270 } 271 } catch (Throwable t) { 272 if (!closing) { 273 error("Error running sync server loop", t); 274 } 275 } finally { 276 forkedProcess = false; 277 } 278 } 279 280 private void client(SocketChannel socket) { 281 int c; 282 synchronized (clients) { 283 clients.put(socket, Thread.currentThread()); 284 c = clients.size(); 285 } 286 info("New client connected ({} connected)", c); 287 use(); 288 Map<String, Context> clientContexts = new ConcurrentHashMap<>(); 289 try { 290 ByteChannel wrapper = new ByteChannelWrapper(socket); 291 DataInputStream input = new DataInputStream(Channels.newInputStream(wrapper)); 292 DataOutputStream output = new DataOutputStream(Channels.newOutputStream(wrapper)); 293 while (!closing) { 294 int requestId = input.readInt(); 295 int sz = input.readInt(); 296 if (sz < 0) { 297 throw new IOException("Received invalid request size: " + sz); 298 } 299 // do not preallocate from an unauthenticated wire-supplied size; grow with actually received data 300 List<String> request = new ArrayList<>(Math.min(sz, 1024)); 301 for (int i = 0; i < sz; i++) { 302 request.add(input.readUTF()); 303 } 304 if (request.isEmpty()) { 305 throw new IOException("Received invalid request"); 306 } 307 use(); 308 String contextId; 309 Context context; 310 String command = request.remove(0); 311 switch (command) { 312 case IpcMessages.REQUEST_CONTEXT: 313 if (request.size() != 1) { 314 throw new IOException("Expected one argument for " + command + " but got " + request); 315 } 316 boolean shared = Boolean.parseBoolean(request.remove(0)); 317 context = new Context(shared); 318 contexts.put(context.id, context); 319 clientContexts.put(context.id, context); 320 synchronized (output) { 321 debug("Created context {}", context.id); 322 output.writeInt(requestId); 323 output.writeInt(2); 324 output.writeUTF(IpcMessages.RESPONSE_CONTEXT); 325 output.writeUTF(context.id); 326 output.flush(); 327 } 328 break; 329 case IpcMessages.REQUEST_ACQUIRE: 330 if (request.isEmpty()) { 331 throw new IOException( 332 "Expected at least one argument for " + command + " but got " + request); 333 } 334 contextId = request.remove(0); 335 // contexts are scoped per connection: a client may only use contexts it created itself 336 context = clientContexts.get(contextId); 337 if (context == null) { 338 throw new IOException( 339 "Unknown context: " + contextId + ". Known contexts = " + clientContexts.keySet()); 340 } 341 context.lock(request).thenRun(() -> sendAcquireResponse(output, socket, requestId, context)); 342 break; 343 case IpcMessages.REQUEST_CLOSE: 344 if (request.size() != 1) { 345 throw new IOException("Expected one argument for " + command + " but got " + request); 346 } 347 contextId = request.remove(0); 348 // contexts are scoped per connection: a client may only close contexts it created itself 349 context = clientContexts.remove(contextId); 350 if (context == null) { 351 throw new IOException( 352 "Unknown context: " + contextId + ". Known contexts = " + clientContexts.keySet()); 353 } 354 contexts.remove(contextId); 355 context.unlock(); 356 synchronized (output) { 357 debug("Closing context {}", context.id); 358 output.writeInt(requestId); 359 output.writeInt(1); 360 output.writeUTF(IpcMessages.RESPONSE_CLOSE); 361 output.flush(); 362 } 363 break; 364 case IpcMessages.REQUEST_STOP: 365 if (request.size() > 1) { 366 throw new IOException( 367 "Expected at most one argument for " + command + " but got " + request); 368 } 369 String stopToken = request.isEmpty() ? null : request.remove(0); 370 if (bootstrapToken == null || !bootstrapToken.equals(stopToken)) { 371 // the protocol is otherwise unauthenticated: only the client that spawned this 372 // server (and thus knows the bootstrap token) may stop it for everybody else 373 throw new IOException("Stop request rejected: missing or invalid bootstrap token"); 374 } 375 synchronized (output) { 376 debug("Stopping server"); 377 output.writeInt(requestId); 378 output.writeInt(1); 379 output.writeUTF(IpcMessages.RESPONSE_STOP); 380 output.flush(); 381 } 382 close(); 383 break; 384 default: 385 throw new IOException("Unknown request: " + request.get(0)); 386 } 387 } 388 } catch (Throwable t) { 389 if (!closing) { 390 error("Error processing request", t); 391 } 392 } finally { 393 if (!closing) { 394 info("Client disconnecting..."); 395 } 396 clientContexts.values().forEach(context -> { 397 contexts.remove(context.id); 398 context.unlock(); 399 }); 400 try { 401 socket.close(); 402 } catch (IOException ioException) { 403 // ignore 404 } 405 synchronized (clients) { 406 clients.remove(socket); 407 c = clients.size(); 408 } 409 if (!closing) { 410 info("{} clients remained", c); 411 } 412 } 413 } 414 415 private void sendAcquireResponse(DataOutputStream output, SocketChannel socket, int requestId, Context context) { 416 try { 417 synchronized (output) { 418 debug("Locking in context {}", context.id); 419 output.writeInt(requestId); 420 output.writeInt(1); 421 output.writeUTF(IpcMessages.RESPONSE_ACQUIRE); 422 output.flush(); 423 } 424 } catch (IOException e) { 425 try { 426 socket.close(); 427 } catch (IOException ioException) { 428 e.addSuppressed(ioException); 429 } 430 error("Error writing lock response", e); 431 } 432 } 433 434 private void use() { 435 lastUsed = System.nanoTime(); 436 } 437 438 private void expirationCheck() { 439 while (true) { 440 long current = System.nanoTime(); 441 long left = (lastUsed + idleTimeout) - current; 442 if (clients.isEmpty() && left < 0) { 443 info("IpcServer expired, closing"); 444 close(); 445 break; 446 } else { 447 try { 448 Thread.sleep(Math.max(1, TimeUnit.NANOSECONDS.toMillis(left))); 449 } catch (InterruptedException e) { 450 info("IpcServer expiration check interrupted, closing"); 451 close(); 452 break; 453 } 454 } 455 } 456 } 457 458 void close() { 459 closing = true; 460 try { 461 serverSocket.close(); 462 } catch (IOException e) { 463 error("Error closing server socket", e); 464 } 465 clients.forEach((s, t) -> { 466 try { 467 s.close(); 468 } catch (IOException e) { 469 // ignore 470 } 471 t.interrupt(); 472 }); 473 } 474 475 static class Waiter { 476 final Context context; 477 final CompletableFuture<Void> future; 478 479 Waiter(Context context, CompletableFuture<Void> future) { 480 this.context = context; 481 this.future = future; 482 } 483 } 484 485 static class Lock { 486 487 final String key; 488 489 List<Context> holders; 490 List<Waiter> waiters; 491 492 Lock(String key) { 493 this.key = key; 494 } 495 496 public synchronized CompletableFuture<Void> lock(Context context) { 497 if (holders == null) { 498 holders = new ArrayList<>(); 499 } 500 if (holders.isEmpty() || holders.get(0).shared && context.shared) { 501 holders.add(context); 502 return CompletableFuture.completedFuture(null); 503 } 504 if (waiters == null) { 505 waiters = new ArrayList<>(); 506 } 507 508 CompletableFuture<Void> future = new CompletableFuture<>(); 509 waiters.add(new Waiter(context, future)); 510 return future; 511 } 512 513 public void unlock(Context context) { 514 List<CompletableFuture<Void>> toComplete; 515 synchronized (this) { 516 toComplete = new ArrayList<>(); 517 if (holders.remove(context)) { 518 while (waiters != null 519 && !waiters.isEmpty() 520 && (holders.isEmpty() || holders.get(0).shared && waiters.get(0).context.shared)) { 521 Waiter waiter = waiters.remove(0); 522 holders.add(waiter.context); 523 toComplete.add(waiter.future); 524 } 525 } else if (waiters != null) { 526 for (Iterator<Waiter> it = waiters.iterator(); it.hasNext(); ) { 527 Waiter waiter = it.next(); 528 if (waiter.context == context) { 529 it.remove(); 530 waiter.future.cancel(false); 531 } 532 } 533 } 534 } 535 toComplete.forEach(f -> f.complete(null)); 536 } 537 538 public synchronized boolean isEmpty() { 539 return (holders == null || holders.isEmpty()) && (waiters == null || waiters.isEmpty()); 540 } 541 } 542 543 class Context { 544 545 final String id; 546 final boolean shared; 547 final List<String> locks = new CopyOnWriteArrayList<>(); 548 549 Context(boolean shared) { 550 this.id = String.format("%08x", counter.incrementAndGet()); 551 this.shared = shared; 552 } 553 554 public CompletableFuture<?> lock(List<String> keys) { 555 locks.addAll(keys); 556 CompletableFuture<?>[] futures = keys.stream() 557 .map(k -> IpcServer.this.locks.computeIfAbsent(k, Lock::new)) 558 .map(l -> l.lock(this)) 559 .toArray(CompletableFuture[]::new); 560 return CompletableFuture.allOf(futures); 561 } 562 563 public void unlock() { 564 locks.stream() 565 .map(k -> IpcServer.this.locks.computeIfAbsent(k, Lock::new)) 566 .forEach(l -> { 567 l.unlock(this); 568 IpcServer.this.locks.compute(l.key, (k, v) -> (v == l && v.isEmpty()) ? null : v); 569 }); 570 } 571 } 572}