1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.eclipse.aether.named.ipc;
20
21 import java.io.Closeable;
22 import java.io.DataInputStream;
23 import java.io.DataOutputStream;
24 import java.io.EOFException;
25 import java.io.File;
26 import java.io.FileWriter;
27 import java.io.IOException;
28 import java.io.InterruptedIOException;
29 import java.io.OutputStream;
30 import java.io.PrintWriter;
31 import java.io.RandomAccessFile;
32 import java.net.SocketAddress;
33 import java.net.URL;
34 import java.nio.channels.ByteChannel;
35 import java.nio.channels.Channels;
36 import java.nio.channels.FileLock;
37 import java.nio.channels.ServerSocketChannel;
38 import java.nio.channels.SocketChannel;
39 import java.nio.charset.StandardCharsets;
40 import java.nio.file.Files;
41 import java.nio.file.Path;
42 import java.nio.file.Paths;
43 import java.security.SecureRandom;
44 import java.util.ArrayList;
45 import java.util.Arrays;
46 import java.util.Collection;
47 import java.util.List;
48 import java.util.Locale;
49 import java.util.Map;
50 import java.util.Objects;
51 import java.util.concurrent.CompletableFuture;
52 import java.util.concurrent.ConcurrentHashMap;
53 import java.util.concurrent.ExecutionException;
54 import java.util.concurrent.ExecutorService;
55 import java.util.concurrent.Executors;
56 import java.util.concurrent.Future;
57 import java.util.concurrent.TimeUnit;
58 import java.util.concurrent.TimeoutException;
59 import java.util.concurrent.atomic.AtomicInteger;
60
61 import static org.eclipse.aether.named.ipc.IpcMessages.REQUEST_ACQUIRE;
62 import static org.eclipse.aether.named.ipc.IpcMessages.REQUEST_CLOSE;
63 import static org.eclipse.aether.named.ipc.IpcMessages.REQUEST_CONTEXT;
64 import static org.eclipse.aether.named.ipc.IpcMessages.REQUEST_STOP;
65 import static org.eclipse.aether.named.ipc.IpcMessages.RESPONSE_ACQUIRE;
66 import static org.eclipse.aether.named.ipc.IpcMessages.RESPONSE_CLOSE;
67 import static org.eclipse.aether.named.ipc.IpcMessages.RESPONSE_CONTEXT;
68 import static org.eclipse.aether.named.ipc.IpcMessages.RESPONSE_STOP;
69
70
71
72
73
74
75
76 public class IpcClient {
77
78 static final boolean IS_WINDOWS =
79 System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("win");
80
81 private static final SecureRandom SECURE_RANDOM = new SecureRandom();
82
83 protected volatile boolean initialized;
84 protected final Path lockPath;
85 protected final Path logPath;
86 protected final Path syncPath;
87 protected final boolean noFork;
88
89 protected volatile SocketChannel socket;
90 protected volatile DataOutputStream output;
91 protected volatile DataInputStream input;
92 protected volatile Thread receiver;
93 protected volatile Process process;
94
95 protected final AtomicInteger requestId = new AtomicInteger();
96 protected final Map<Integer, CompletableFuture<List<String>>> responses = new ConcurrentHashMap<>();
97
98
99
100
101
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
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
161 }
162 }
163
164 ServerSocketChannel ss = family.openServerSocket();
165 String tmpaddr = SocketFamily.toString(ss.getLocalAddress());
166
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
209
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
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
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 }