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