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