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.Closeable; 022import java.io.DataInputStream; 023import java.io.DataOutputStream; 024import java.io.EOFException; 025import java.io.File; 026import java.io.FileWriter; 027import java.io.IOException; 028import java.io.InterruptedIOException; 029import java.io.OutputStream; 030import java.io.PrintWriter; 031import java.io.RandomAccessFile; 032import java.net.SocketAddress; 033import java.net.URL; 034import java.nio.channels.ByteChannel; 035import java.nio.channels.Channels; 036import java.nio.channels.FileLock; 037import java.nio.channels.ServerSocketChannel; 038import java.nio.channels.SocketChannel; 039import java.nio.charset.StandardCharsets; 040import java.nio.file.Files; 041import java.nio.file.Path; 042import java.nio.file.Paths; 043import java.security.SecureRandom; 044import java.util.ArrayList; 045import java.util.Arrays; 046import java.util.Collection; 047import java.util.List; 048import java.util.Locale; 049import java.util.Map; 050import java.util.Objects; 051import java.util.concurrent.CompletableFuture; 052import java.util.concurrent.ConcurrentHashMap; 053import java.util.concurrent.ExecutionException; 054import java.util.concurrent.ExecutorService; 055import java.util.concurrent.Executors; 056import java.util.concurrent.Future; 057import java.util.concurrent.TimeUnit; 058import java.util.concurrent.TimeoutException; 059import java.util.concurrent.atomic.AtomicInteger; 060 061import static org.eclipse.aether.named.ipc.IpcMessages.REQUEST_ACQUIRE; 062import static org.eclipse.aether.named.ipc.IpcMessages.REQUEST_CLOSE; 063import static org.eclipse.aether.named.ipc.IpcMessages.REQUEST_CONTEXT; 064import static org.eclipse.aether.named.ipc.IpcMessages.REQUEST_STOP; 065import static org.eclipse.aether.named.ipc.IpcMessages.RESPONSE_ACQUIRE; 066import static org.eclipse.aether.named.ipc.IpcMessages.RESPONSE_CLOSE; 067import static org.eclipse.aether.named.ipc.IpcMessages.RESPONSE_CONTEXT; 068import static org.eclipse.aether.named.ipc.IpcMessages.RESPONSE_STOP; 069 070/** 071 * Client side implementation. 072 * The client instance is bound to a given maven repository. 073 * 074 * @since 2.0.1 075 */ 076public class IpcClient { 077 078 static final boolean IS_WINDOWS = 079 System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("win"); 080 081 private static final SecureRandom SECURE_RANDOM = new SecureRandom(); 082 083 protected volatile boolean initialized; 084 protected final Path lockPath; 085 protected final Path logPath; 086 protected final Path syncPath; 087 protected final boolean noFork; 088 089 protected volatile SocketChannel socket; 090 protected volatile DataOutputStream output; 091 protected volatile DataInputStream input; 092 protected volatile Thread receiver; 093 protected volatile Process process; 094 095 protected final AtomicInteger requestId = new AtomicInteger(); 096 protected final Map<Integer, CompletableFuture<List<String>>> responses = new ConcurrentHashMap<>(); 097 098 /** 099 * The bootstrap token shared with the server this client spawned (if it spawned one): the only credential 100 * accepted by the server for a stop request. Remains {@code null} when this client attached to an already 101 * running server, which consequently cannot be stopped from here. 102 */ 103 protected volatile String bootstrapToken; 104 105 IpcClient(Path lockPath, Path logPath, Path syncPath) { 106 this.lockPath = lockPath; 107 this.logPath = logPath; 108 this.syncPath = syncPath; 109 this.noFork = Boolean.parseBoolean( 110 System.getProperty(IpcServer.SYSTEM_PROP_NO_FORK, Boolean.toString(IpcServer.DEFAULT_NO_FORK))); 111 } 112 113 void ensureInitialized() throws IOException { 114 if (!initialized) { 115 // caller must block on this method 116 synchronized (this) { 117 if (!initialized) { 118 socket = createClient(); 119 ByteChannel wrapper = new ByteChannelWrapper(socket); 120 input = new DataInputStream(Channels.newInputStream(wrapper)); 121 output = new DataOutputStream(Channels.newOutputStream(wrapper)); 122 receiver = new Thread(this::receive); 123 receiver.setDaemon(true); 124 receiver.start(); 125 initialized = true; 126 } 127 } 128 } 129 } 130 131 private synchronized DataInputStream getInput() { 132 return input; 133 } 134 135 private synchronized DataOutputStream getOutput() { 136 return output; 137 } 138 139 SocketChannel createClient() throws IOException { 140 SocketFamily family = 141 SocketFamily.valueOf(System.getProperty(IpcServer.SYSTEM_PROP_FAMILY, IpcServer.DEFAULT_FAMILY)); 142 143 Path lockPath = this.lockPath.toAbsolutePath().normalize(); 144 Path lockFile = 145 lockPath.resolve(".maven-resolver-ipc-lock-" + family.name().toLowerCase(Locale.ENGLISH)); 146 if (!Files.isRegularFile(lockFile)) { 147 if (!Files.isDirectory(lockFile.getParent())) { 148 Files.createDirectories(lockFile.getParent()); 149 } 150 } 151 152 try (RandomAccessFile raf = new RandomAccessFile(lockFile.toFile(), "rw")) { 153 try (FileLock lock = raf.getChannel().lock()) { 154 String line = raf.readLine(); 155 if (line != null) { 156 try { 157 SocketAddress address = SocketFamily.fromString(line); 158 return SocketChannel.open(address); 159 } catch (IOException e) { 160 // ignore 161 } 162 } 163 164 ServerSocketChannel ss = family.openServerSocket(); 165 String tmpaddr = SocketFamily.toString(ss.getLocalAddress()); 166 // the token authorizes stopping the daemon: it must not be guessable by other local users 167 String rand = Long.toHexString(SECURE_RANDOM.nextLong()) + Long.toHexString(SECURE_RANDOM.nextLong()); 168 String nativeName = 169 System.getProperty(IpcServer.SYSTEM_PROP_NATIVE_NAME, IpcServer.DEFAULT_NATIVE_NAME); 170 String syncCmd = IS_WINDOWS ? nativeName + ".exe" : nativeName; 171 172 boolean debug = Boolean.parseBoolean( 173 System.getProperty(IpcServer.SYSTEM_PROP_DEBUG, Boolean.toString(IpcServer.DEFAULT_DEBUG))); 174 boolean noNative = Boolean.parseBoolean(System.getProperty( 175 IpcServer.SYSTEM_PROP_NO_NATIVE, Boolean.toString(IpcServer.DEFAULT_NO_NATIVE))); 176 if (!noNative) { 177 noNative = !Files.isExecutable(syncPath.resolve(syncCmd)); 178 } 179 Closeable close; 180 Path logFile = logPath.resolve("resolver-ipcsync-" + rand + ".log"); 181 List<String> args = new ArrayList<>(); 182 if (noNative) { 183 if (noFork) { 184 IpcServer server = IpcServer.runServer(family, tmpaddr, rand); 185 close = server::close; 186 } else { 187 String javaHome = System.getenv("JAVA_HOME"); 188 if (javaHome == null) { 189 javaHome = System.getProperty("java.home"); 190 } 191 String javaCmd = IS_WINDOWS ? "bin\\java.exe" : "bin/java"; 192 String java = Paths.get(javaHome) 193 .resolve(javaCmd) 194 .toAbsolutePath() 195 .toString(); 196 args.add(java); 197 String classpath = getJarPath(getClass()) + File.pathSeparator + getJarPath(IpcServer.class); 198 args.add("-cp"); 199 args.add(classpath); 200 String timeout = System.getProperty(IpcServer.SYSTEM_PROP_IDLE_TIMEOUT); 201 if (timeout != null) { 202 args.add("-D" + IpcServer.SYSTEM_PROP_IDLE_TIMEOUT + "=" + timeout); 203 } 204 args.add("-D" + IpcServer.SYSTEM_PROP_DEBUG + "=" + debug); 205 args.add(IpcServer.class.getName()); 206 args.add(family.name()); 207 args.add(tmpaddr); 208 // the bootstrap token is passed via stdin ("-" placeholder in argv): process arguments 209 // are commonly visible to other local users (e.g. /proc/<pid>/cmdline) 210 args.add("-"); 211 ProcessBuilder processBuilder = new ProcessBuilder(); 212 ProcessBuilder.Redirect discard = ProcessBuilder.Redirect.to(logFile.toFile()); 213 Files.createDirectories(logPath); 214 this.process = processBuilder 215 .directory(lockFile.getParent().toFile()) 216 .command(args) 217 .redirectOutput(discard) 218 .redirectError(discard) 219 .start(); 220 writeBootstrapToken(this.process, rand); 221 close = this.process::destroyForcibly; 222 } 223 } else { 224 args.add(syncPath.resolve(syncCmd).toString()); 225 String timeout = System.getProperty(IpcServer.SYSTEM_PROP_IDLE_TIMEOUT); 226 if (timeout != null) { 227 args.add("-D" + IpcServer.SYSTEM_PROP_IDLE_TIMEOUT + "=" + timeout); 228 } 229 args.add("-D" + IpcServer.SYSTEM_PROP_DEBUG + "=" + debug); 230 args.add(family.name()); 231 args.add(tmpaddr); 232 // see above: the bootstrap token goes via stdin, not argv 233 args.add("-"); 234 ProcessBuilder processBuilder = new ProcessBuilder(); 235 ProcessBuilder.Redirect discard = ProcessBuilder.Redirect.to(logFile.toFile()); 236 Files.createDirectories(logPath); 237 this.process = processBuilder 238 .directory(lockFile.getParent().toFile()) 239 .command(args) 240 .redirectOutput(discard) 241 .redirectError(discard) 242 .start(); 243 writeBootstrapToken(this.process, rand); 244 close = this.process::destroyForcibly; 245 } 246 247 ExecutorService es = Executors.newSingleThreadExecutor(); 248 Future<String[]> future = es.submit(() -> { 249 SocketChannel s = ss.accept(); 250 DataInputStream dis = new DataInputStream(Channels.newInputStream(s)); 251 String rand2 = dis.readUTF(); 252 String addr2 = dis.readUTF(); 253 return new String[] {rand2, addr2}; 254 }); 255 String[] res; 256 try { 257 res = future.get(5, TimeUnit.SECONDS); 258 } catch (Exception e) { 259 try (PrintWriter writer = new PrintWriter(new FileWriter(logFile.toFile(), true))) { 260 writer.println("Arguments:"); 261 args.forEach(writer::println); 262 writer.println(); 263 writer.println("Exception:"); 264 e.printStackTrace(writer); 265 } 266 close.close(); 267 throw e; 268 } finally { 269 es.shutdownNow(); 270 ss.close(); 271 } 272 if (!Objects.equals(rand, res[0])) { 273 close.close(); 274 throw new IllegalStateException("IpcServer did not respond with the correct random"); 275 } 276 this.bootstrapToken = rand; 277 278 SocketAddress addr = SocketFamily.fromString(res[1]); 279 SocketChannel socket = SocketChannel.open(addr); 280 281 raf.seek(0); 282 raf.writeBytes(res[1] + "\n"); 283 return socket; 284 } catch (Exception e) { 285 throw new RuntimeException("Unable to create and connect to lock server", e); 286 } 287 } 288 } 289 290 private static void writeBootstrapToken(Process process, String token) throws IOException { 291 try (OutputStream os = process.getOutputStream()) { 292 os.write((token + "\n").getBytes(StandardCharsets.UTF_8)); 293 } 294 } 295 296 private String getJarPath(Class<?> clazz) { 297 String classpath; 298 String className = clazz.getName().replace('.', '/') + ".class"; 299 URL resource = clazz.getResource("/" + className); 300 if (resource == null) { 301 throw new IllegalStateException("Unable to find resource for class " + clazz.getName()); 302 } 303 String url = resource.toString(); 304 if (url.startsWith("jar:")) { 305 url = url.substring("jar:".length(), url.indexOf("!/")); 306 if (url.startsWith("file:")) { 307 classpath = url.substring("file:".length()); 308 } else { 309 throw new IllegalStateException(); 310 } 311 } else if (url.startsWith("file:")) { 312 classpath = url.substring("file:".length(), url.indexOf(className)); 313 } else { 314 throw new IllegalStateException(); 315 } 316 if (IS_WINDOWS) { 317 if (classpath.startsWith("/")) { 318 classpath = classpath.substring(1); 319 } 320 classpath = classpath.replace('/', '\\'); 321 } 322 323 return classpath; 324 } 325 326 void receive() { 327 try { 328 while (true) { 329 DataInputStream in = getInput(); 330 if (in == null) { 331 throw new IOException("Connection closed"); 332 } 333 int id = in.readInt(); 334 int sz = in.readInt(); 335 List<String> s = new ArrayList<>(Math.max(0, Math.min(sz, 1024))); 336 for (int i = 0; i < sz; i++) { 337 s.add(in.readUTF()); 338 } 339 CompletableFuture<List<String>> f = responses.remove(id); 340 if (f == null) { 341 continue; 342 } 343 if (s.isEmpty()) { 344 f.completeExceptionally(new IOException("Protocol error: empty response")); 345 continue; 346 } 347 f.complete(s); 348 } 349 } catch (EOFException e) { 350 close(new IOException("Server disconnected", e)); 351 } catch (Exception e) { 352 close(e); 353 } 354 } 355 356 List<String> send(List<String> request, long time, TimeUnit unit) throws TimeoutException, IOException { 357 ensureInitialized(); 358 DataOutputStream out = getOutput(); 359 if (out == null) { 360 throw new IOException("Connection closed"); 361 } 362 int id = requestId.incrementAndGet(); 363 CompletableFuture<List<String>> response = new CompletableFuture<>(); 364 responses.put(id, response); 365 synchronized (out) { 366 out.writeInt(id); 367 out.writeInt(request.size()); 368 for (String s : request) { 369 out.writeUTF(s); 370 } 371 out.flush(); 372 } 373 try { 374 return response.get(time, unit); 375 } catch (InterruptedException e) { 376 responses.remove(id); 377 throw (IOException) new InterruptedIOException("Interrupted").initCause(e); 378 } catch (ExecutionException e) { 379 throw new IOException("Execution error", e); 380 } catch (TimeoutException e) { 381 responses.remove(id); 382 throw e; 383 } 384 } 385 386 void close() { 387 try { 388 if (noFork) { 389 stopServer(); 390 } 391 } finally { 392 close(new IOException("Closing")); 393 } 394 } 395 396 synchronized void close(Throwable e) { 397 initialized = false; 398 if (socket != null) { 399 try { 400 socket.close(); 401 } catch (IOException t) { 402 e.addSuppressed(t); 403 } 404 socket = null; 405 input = null; 406 output = null; 407 } 408 if (receiver != null && Thread.currentThread() != receiver) { 409 receiver.interrupt(); 410 try { 411 receiver.join(1000); 412 } catch (InterruptedException t) { 413 e.addSuppressed(t); 414 } 415 } 416 responses.values().forEach(f -> f.completeExceptionally(e)); 417 responses.clear(); 418 } 419 420 String newContext(boolean shared, long time, TimeUnit unit) throws TimeoutException { 421 RuntimeException error = new RuntimeException("Unable to create new sync context"); 422 for (int i = 0; i < 2; i++) { 423 try { 424 List<String> response = send(Arrays.asList(REQUEST_CONTEXT, Boolean.toString(shared)), time, unit); 425 if (response.size() != 2 || !RESPONSE_CONTEXT.equals(response.get(0))) { 426 throw new IOException("Unexpected response: " + response); 427 } 428 return response.get(1); 429 } catch (TimeoutException e) { 430 throw e; 431 } catch (Exception e) { 432 close(e); 433 error.addSuppressed(e); 434 } 435 } 436 throw error; 437 } 438 439 void lock(String contextId, Collection<String> keys, long time, TimeUnit unit) throws TimeoutException { 440 try { 441 List<String> req = new ArrayList<>(keys.size() + 2); 442 req.add(REQUEST_ACQUIRE); 443 req.add(contextId); 444 req.addAll(keys); 445 List<String> response = send(req, time, unit); 446 if (response.size() != 1 || !RESPONSE_ACQUIRE.equals(response.get(0))) { 447 throw new IOException("Unexpected response: " + response); 448 } 449 } catch (TimeoutException e) { 450 throw e; 451 } catch (Exception e) { 452 close(e); 453 throw new RuntimeException("Unable to perform lock (contextId = " + contextId + ")", e); 454 } 455 } 456 457 void unlock(String contextId) { 458 try { 459 List<String> response = send(Arrays.asList(REQUEST_CLOSE, contextId), 10, TimeUnit.SECONDS); 460 if (response.size() != 1 || !RESPONSE_CLOSE.equals(response.get(0))) { 461 throw new IOException("Unexpected response: " + response); 462 } 463 } catch (Exception e) { 464 close(e); 465 throw new RuntimeException("Unable to unlock (contextId = " + contextId + ")", e); 466 } 467 } 468 469 /** 470 * To be used in tests to stop server immediately. Should not be used outside of tests. 471 */ 472 void stopServer() { 473 String token = bootstrapToken; 474 try { 475 List<String> response = send(List.of(REQUEST_STOP, token == null ? "" : token), 30, TimeUnit.SECONDS); 476 if (response.size() != 1 || !RESPONSE_STOP.equals(response.get(0))) { 477 throw new IOException("Unexpected response: " + response); 478 } 479 480 Process forkedProcess = process; 481 if (forkedProcess != null) { 482 if (!forkedProcess.waitFor(30, TimeUnit.SECONDS)) { 483 forkedProcess.destroyForcibly(); 484 if (!forkedProcess.waitFor(5, TimeUnit.SECONDS)) { 485 throw new IOException("Unable to stop forked server process"); 486 } 487 } 488 process = null; 489 } 490 } catch (Exception e) { 491 close(e); 492 throw new RuntimeException("Unable to stop server", e); 493 } 494 } 495 496 @Override 497 public String toString() { 498 return "IpcClient{" 499 + "lockPath=" + lockPath + "," 500 + "syncServerPath=" + syncPath + "," 501 + "address='" + getAddress() + "'}"; 502 } 503 504 private String getAddress() { 505 SocketChannel s = socket; 506 if (s == null) { 507 return "[closed]"; 508 } 509 try { 510 return SocketFamily.toString(s.getLocalAddress()); 511 } catch (IOException e) { 512 return "[not bound]"; 513 } 514 } 515}