1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven.cling.executor.embedded;
20
21 import java.io.Closeable;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.io.OutputStream;
25 import java.io.PrintStream;
26 import java.lang.reflect.Constructor;
27 import java.lang.reflect.Method;
28 import java.net.MalformedURLException;
29 import java.net.URL;
30 import java.net.URLClassLoader;
31 import java.nio.file.Files;
32 import java.nio.file.Path;
33 import java.util.ArrayList;
34 import java.util.Arrays;
35 import java.util.Collection;
36 import java.util.Collections;
37 import java.util.HashMap;
38 import java.util.HashSet;
39 import java.util.List;
40 import java.util.Map;
41 import java.util.Properties;
42 import java.util.Set;
43 import java.util.concurrent.ConcurrentHashMap;
44 import java.util.concurrent.atomic.AtomicBoolean;
45 import java.util.function.Function;
46 import java.util.stream.Stream;
47
48 import org.apache.maven.api.cli.Executor;
49 import org.apache.maven.api.cli.ExecutorException;
50 import org.apache.maven.api.cli.ExecutorRequest;
51
52 import static java.util.Objects.requireNonNull;
53
54
55
56
57
58
59 public class EmbeddedMavenExecutor implements Executor {
60
61
62
63 protected static final Map<String, String> MVN4_MAIN_CLASSES = Map.of(
64 "mvn",
65 "org.apache.maven.cling.MavenCling",
66 "mvnenc",
67 "org.apache.maven.cling.MavenEncCling",
68 "mvnsh",
69 "org.apache.maven.cling.MavenShellCling");
70
71
72
73
74 protected static final class Context {
75 private final URLClassLoader bootClassLoader;
76 private final String version;
77 private final Object classWorld;
78 private final Set<String> originalClassRealmIds;
79 private final ClassLoader tccl;
80 private final Map<String, Function<ExecutorRequest, Integer>> commands;
81 private final Collection<Object> keepAlive;
82
83 private Context(
84 URLClassLoader bootClassLoader,
85 String version,
86 Object classWorld,
87 Set<String> originalClassRealmIds,
88 ClassLoader tccl,
89 Map<String, Function<ExecutorRequest, Integer>> commands,
90 Collection<Object> keepAlive) {
91 this.bootClassLoader = bootClassLoader;
92 this.version = version;
93 this.classWorld = classWorld;
94 this.originalClassRealmIds = originalClassRealmIds;
95 this.tccl = tccl;
96 this.commands = commands;
97 this.keepAlive = keepAlive;
98 }
99 }
100
101 protected final boolean cacheContexts;
102 protected final boolean useMavenArgsEnv;
103 protected final AtomicBoolean closed;
104 protected final InputStream originalStdin;
105 protected final PrintStream originalStdout;
106 protected final PrintStream originalStderr;
107 protected final Properties originalProperties;
108 protected final ClassLoader originalClassLoader;
109 protected final ConcurrentHashMap<Path, Context> contexts;
110
111 public EmbeddedMavenExecutor() {
112 this(true, true);
113 }
114
115 public EmbeddedMavenExecutor(boolean cacheContexts, boolean useMavenArgsEnv) {
116 this.cacheContexts = cacheContexts;
117 this.useMavenArgsEnv = useMavenArgsEnv;
118 this.closed = new AtomicBoolean(false);
119 this.originalStdin = System.in;
120 this.originalStdout = System.out;
121 this.originalStderr = System.err;
122 this.originalClassLoader = Thread.currentThread().getContextClassLoader();
123 this.contexts = new ConcurrentHashMap<>();
124 this.originalProperties = new Properties();
125 this.originalProperties.putAll(System.getProperties());
126 }
127
128 @Override
129 public int execute(ExecutorRequest executorRequest) throws ExecutorException {
130 requireNonNull(executorRequest);
131 if (closed.get()) {
132 throw new ExecutorException("Executor is closed");
133 }
134 validate(executorRequest);
135 Context context = mayCreate(executorRequest);
136 String command = executorRequest.command();
137 Function<ExecutorRequest, Integer> exec = context.commands.get(command);
138 if (exec == null) {
139 throw new IllegalArgumentException(
140 "Unknown command: '" + command + "' for '" + executorRequest.installationDirectory() + "'");
141 }
142
143 Thread.currentThread().setContextClassLoader(context.tccl);
144 try {
145 return exec.apply(executorRequest);
146 } catch (Exception e) {
147 throw new ExecutorException("Failed to execute", e);
148 } finally {
149 try {
150 disposeRuntimeCreatedRealms(context);
151 } finally {
152 System.setIn(originalStdin);
153 System.setOut(originalStdout);
154 System.setErr(originalStderr);
155 Thread.currentThread().setContextClassLoader(originalClassLoader);
156 System.setProperties(originalProperties);
157 if (!cacheContexts) {
158 doClose(context);
159 }
160 }
161 }
162 }
163
164
165
166
167 protected void disposeRuntimeCreatedRealms(Context context) {
168 try {
169 Method getRealms = context.classWorld.getClass().getMethod("getRealms");
170 Method disposeRealm = context.classWorld.getClass().getMethod("disposeRealm", String.class);
171 List<Object> realms = (List<Object>) getRealms.invoke(context.classWorld);
172 for (Object realm : realms) {
173 String realmId = (String) realm.getClass().getMethod("getId").invoke(realm);
174 if (!context.originalClassRealmIds.contains(realmId)) {
175 disposeRealm.invoke(context.classWorld, realmId);
176 }
177 }
178 } catch (Exception e) {
179 throw new ExecutorException("Failed to dispose runtime created realms", e);
180 }
181 }
182
183 @Override
184 public String mavenVersion(ExecutorRequest executorRequest) throws ExecutorException {
185 requireNonNull(executorRequest);
186 validate(executorRequest);
187 if (closed.get()) {
188 throw new ExecutorException("Executor is closed");
189 }
190 return mayCreate(executorRequest).version;
191 }
192
193 protected Context mayCreate(ExecutorRequest executorRequest) {
194 Path mavenHome = ExecutorRequest.getCanonicalPath(executorRequest.installationDirectory());
195 if (cacheContexts) {
196 return contexts.computeIfAbsent(mavenHome, k -> doCreate(mavenHome, executorRequest));
197 } else {
198 return doCreate(mavenHome, executorRequest);
199 }
200 }
201
202 protected Context doCreate(Path mavenHome, ExecutorRequest executorRequest) {
203 if (!Files.isDirectory(mavenHome)) {
204 throw new IllegalArgumentException("Installation directory must point to existing directory: " + mavenHome);
205 }
206 if (!MVN4_MAIN_CLASSES.containsKey(executorRequest.command())) {
207 throw new IllegalArgumentException(
208 getClass().getSimpleName() + " does not support command " + executorRequest.command());
209 }
210 if (executorRequest.environmentVariables().isPresent()) {
211 throw new IllegalArgumentException(getClass().getSimpleName() + " does not support environment variables");
212 }
213 if (executorRequest.jvmArguments().isPresent()) {
214 throw new IllegalArgumentException(getClass().getSimpleName() + " does not support jvmArguments");
215 }
216 Path boot = mavenHome.resolve("boot");
217 Path m2conf = mavenHome.resolve("bin/m2.conf");
218 if (!Files.isDirectory(boot) || !Files.isRegularFile(m2conf)) {
219 throw new IllegalArgumentException(
220 "Installation directory does not point to Maven installation: " + mavenHome);
221 }
222
223 ArrayList<String> mavenArgs = new ArrayList<>();
224 String mavenArgsEnv = System.getenv("MAVEN_ARGS");
225 if (useMavenArgsEnv && mavenArgsEnv != null && !mavenArgsEnv.isEmpty()) {
226 Arrays.stream(mavenArgsEnv.split(" "))
227 .filter(s -> !s.trim().isEmpty())
228 .forEach(s -> mavenArgs.add(0, s));
229 }
230
231 Properties properties = prepareProperties(executorRequest);
232
233 properties.setProperty(
234 "maven.mainClass", requireNonNull(MVN4_MAIN_CLASSES.get(ExecutorRequest.MVN), "mainClass"));
235 System.setProperties(properties);
236 URLClassLoader bootClassLoader = createMavenBootClassLoader(boot, Collections.emptyList());
237 Thread.currentThread().setContextClassLoader(bootClassLoader);
238 try {
239 Class<?> launcherClass = bootClassLoader.loadClass("org.codehaus.plexus.classworlds.launcher.Launcher");
240 Object launcher = launcherClass.getDeclaredConstructor().newInstance();
241 Method configure = launcherClass.getMethod("configure", InputStream.class);
242 try (InputStream inputStream = Files.newInputStream(m2conf)) {
243 configure.invoke(launcher, inputStream);
244 }
245 Object classWorld = launcherClass.getMethod("getWorld").invoke(launcher);
246 Set<String> originalClassRealmIds = new HashSet<>();
247
248
249 Method getRealms = classWorld.getClass().getMethod("getRealms");
250 List<Object> realms = (List<Object>) getRealms.invoke(classWorld);
251 for (Object realm : realms) {
252 Method realmGetId = realm.getClass().getMethod("getId");
253 originalClassRealmIds.add((String) realmGetId.invoke(realm));
254 }
255
256 Class<?> cliClass =
257 (Class<?>) launcherClass.getMethod("getMainClass").invoke(launcher);
258 String version = getMavenVersion(cliClass);
259 Map<String, Function<ExecutorRequest, Integer>> commands = new HashMap<>();
260 ArrayList<Object> keepAlive = new ArrayList<>();
261
262 if (version.startsWith("3.")) {
263
264 if (!ExecutorRequest.MVN.equals(executorRequest.command())) {
265 throw new IllegalArgumentException(getClass().getSimpleName() + " w/ mvn3 does not support command "
266 + executorRequest.command());
267 }
268 keepAlive.add(cliClass.getClassLoader().loadClass("org.fusesource.jansi.internal.JansiLoader"));
269 Constructor<?> newMavenCli = cliClass.getConstructor(classWorld.getClass());
270 Object mavenCli = newMavenCli.newInstance(classWorld);
271 Class<?>[] parameterTypes = {String[].class, String.class, PrintStream.class, PrintStream.class};
272 Method doMain = cliClass.getMethod("doMain", parameterTypes);
273 commands.put(ExecutorRequest.MVN, r -> {
274 System.setProperties(prepareProperties(r));
275 try {
276 ArrayList<String> args = new ArrayList<>(mavenArgs);
277 args.addAll(r.arguments());
278 PrintStream stdout = r.stdOut().isEmpty()
279 ? null
280 : new PrintStream(r.stdOut().orElseThrow(), true);
281 PrintStream stderr = r.stdErr().isEmpty()
282 ? null
283 : new PrintStream(r.stdErr().orElseThrow(), true);
284 return (int) doMain.invoke(mavenCli, new Object[] {
285 args.toArray(new String[0]), r.cwd().toString(), stdout, stderr
286 });
287 } catch (Exception e) {
288 throw new ExecutorException("Failed to execute", e);
289 }
290 });
291 } else {
292
293 keepAlive.add(cliClass.getClassLoader().loadClass("org.jline.nativ.JLineNativeLoader"));
294 for (Map.Entry<String, String> cmdEntry : MVN4_MAIN_CLASSES.entrySet()) {
295 Class<?> cmdClass = cliClass.getClassLoader().loadClass(cmdEntry.getValue());
296 Method mainMethod = cmdClass.getMethod(
297 "main",
298 String[].class,
299 classWorld.getClass(),
300 InputStream.class,
301 OutputStream.class,
302 OutputStream.class);
303 commands.put(cmdEntry.getKey(), r -> {
304 System.setProperties(prepareProperties(r));
305 try {
306 ArrayList<String> args = new ArrayList<>(mavenArgs);
307 args.addAll(r.arguments());
308 return (int) mainMethod.invoke(
309 null,
310 args.toArray(new String[0]),
311 classWorld,
312 r.stdIn().orElse(null),
313 r.stdOut().orElse(null),
314 r.stdErr().orElse(null));
315 } catch (Exception e) {
316 throw new ExecutorException("Failed to execute", e);
317 }
318 });
319 }
320 }
321
322 return new Context(
323 bootClassLoader,
324 version,
325 classWorld,
326 originalClassRealmIds,
327 cliClass.getClassLoader(),
328 commands,
329 keepAlive);
330 } catch (Exception e) {
331 throw new ExecutorException("Failed to create executor", e);
332 } finally {
333 Thread.currentThread().setContextClassLoader(originalClassLoader);
334 System.setProperties(originalProperties);
335 }
336 }
337
338 protected Properties prepareProperties(ExecutorRequest request) {
339 System.setProperties(null);
340
341 Properties properties = new Properties();
342 properties.putAll(System.getProperties());
343
344 properties.setProperty("user.dir", request.cwd().toString());
345 properties.setProperty("user.home", request.userHomeDirectory().toString());
346
347 Path mavenHome = request.installationDirectory();
348 properties.setProperty("maven.home", mavenHome.toString());
349 properties.setProperty(
350 "maven.multiModuleProjectDirectory", request.cwd().toString());
351
352
353 properties.setProperty(
354 "library.jansi.path", mavenHome.resolve("lib/jansi-native").toString());
355
356
357 properties.setProperty(
358 "library.jline.path", mavenHome.resolve("lib/jline-native").toString());
359
360 if (request.jvmSystemProperties().isPresent()) {
361 properties.putAll(request.jvmSystemProperties().get());
362 }
363
364 return properties;
365 }
366
367 @Override
368 public void close() throws ExecutorException {
369 if (closed.compareAndExchange(false, true)) {
370 try {
371 ArrayList<Exception> exceptions = new ArrayList<>();
372 for (Context context : contexts.values()) {
373 try {
374 doClose(context);
375 } catch (Exception e) {
376 exceptions.add(e);
377 }
378 }
379 if (!exceptions.isEmpty()) {
380 ExecutorException e = new ExecutorException("Could not close cleanly");
381 exceptions.forEach(e::addSuppressed);
382 throw e;
383 }
384 } finally {
385 System.setProperties(originalProperties);
386 }
387 }
388 }
389
390 protected void doClose(Context context) throws ExecutorException {
391 Thread.currentThread().setContextClassLoader(context.bootClassLoader);
392 try {
393 try {
394 ((Closeable) context.classWorld).close();
395 } finally {
396 context.bootClassLoader.close();
397 }
398 } catch (Exception e) {
399 throw new ExecutorException("Failed to close cleanly", e);
400 } finally {
401 Thread.currentThread().setContextClassLoader(originalClassLoader);
402 }
403 }
404
405 protected void validate(ExecutorRequest executorRequest) throws ExecutorException {}
406
407 protected URLClassLoader createMavenBootClassLoader(Path boot, List<URL> extraClasspath) {
408 ArrayList<URL> urls = new ArrayList<>(extraClasspath);
409 try (Stream<Path> stream = Files.list(boot)) {
410 stream.filter(Files::isRegularFile)
411 .filter(p -> p.toString().endsWith(".jar"))
412 .forEach(f -> {
413 try {
414 urls.add(f.toUri().toURL());
415 } catch (MalformedURLException e) {
416 throw new ExecutorException("Failed to build classpath: " + f, e);
417 }
418 });
419 } catch (IOException e) {
420 throw new ExecutorException("Failed to build classpath: " + e, e);
421 }
422 if (urls.isEmpty()) {
423 throw new IllegalArgumentException("Invalid Maven home directory; boot is empty");
424 }
425 return new URLClassLoader(
426 urls.toArray(new URL[0]), ClassLoader.getSystemClassLoader().getParent());
427 }
428
429 protected String getMavenVersion(Class<?> clazz) throws IOException {
430 Properties props = new Properties();
431 try (InputStream is = clazz.getResourceAsStream("/META-INF/maven/org.apache.maven/maven-core/pom.properties")) {
432 if (is != null) {
433 props.load(is);
434 }
435 String version = props.getProperty("version");
436 if (version != null) {
437 return version;
438 }
439 return UNKNOWN_VERSION;
440 }
441 }
442 }