View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.lifecycle.internal;
20  
21  import javax.inject.Inject;
22  import javax.inject.Named;
23  import javax.inject.Provider;
24  import javax.inject.Singleton;
25  
26  import java.util.ArrayList;
27  import java.util.Arrays;
28  import java.util.Collection;
29  import java.util.Collections;
30  import java.util.List;
31  import java.util.Map;
32  import java.util.Set;
33  import java.util.TreeSet;
34  import java.util.concurrent.ConcurrentHashMap;
35  import java.util.concurrent.locks.Lock;
36  import java.util.concurrent.locks.ReentrantLock;
37  import java.util.concurrent.locks.ReentrantReadWriteLock;
38  
39  import org.apache.maven.api.SessionData;
40  import org.apache.maven.api.services.MavenException;
41  import org.apache.maven.api.services.MessageBuilderFactory;
42  import org.apache.maven.artifact.Artifact;
43  import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
44  import org.apache.maven.artifact.resolver.filter.CumulativeScopeArtifactFilter;
45  import org.apache.maven.execution.ExecutionEvent;
46  import org.apache.maven.execution.MavenSession;
47  import org.apache.maven.internal.MultilineMessageHelper;
48  import org.apache.maven.lifecycle.LifecycleExecutionException;
49  import org.apache.maven.lifecycle.MissingProjectException;
50  import org.apache.maven.plugin.BuildPluginManager;
51  import org.apache.maven.plugin.MavenPluginManager;
52  import org.apache.maven.plugin.MojoExecution;
53  import org.apache.maven.plugin.MojoExecutionException;
54  import org.apache.maven.plugin.MojoExecutionRunner;
55  import org.apache.maven.plugin.MojoFailureException;
56  import org.apache.maven.plugin.MojosExecutionStrategy;
57  import org.apache.maven.plugin.PluginConfigurationException;
58  import org.apache.maven.plugin.PluginIncompatibleException;
59  import org.apache.maven.plugin.PluginManagerException;
60  import org.apache.maven.plugin.descriptor.MojoDescriptor;
61  import org.apache.maven.project.MavenProject;
62  import org.slf4j.Logger;
63  import org.slf4j.LoggerFactory;
64  
65  /**
66   * <p>
67   * Executes an individual mojo
68   * </p>
69   * <strong>NOTE:</strong> This class is not part of any public api and can be changed or deleted without prior notice.
70   *
71   * @since 3.0
72   */
73  @Named
74  @Singleton
75  public class MojoExecutor {
76  
77      private static final Logger LOGGER = LoggerFactory.getLogger(MojoExecutor.class);
78      private static final SessionData.Key<ProjectIndex> PROJECT_INDEX = SessionData.key(ProjectIndex.class);
79  
80      @SuppressWarnings({"unchecked", "rawtypes"})
81      private static final SessionData.Key<Map<MavenProject, OwnerReentrantLock>> PROJECT_LOCKS =
82              (SessionData.Key) SessionData.key(Map.class, ProjectLock.class);
83  
84      private final BuildPluginManager pluginManager;
85      private final MavenPluginManager mavenPluginManager;
86      private final LifecycleDependencyResolver lifeCycleDependencyResolver;
87      private final ExecutionEventCatapult eventCatapult;
88  
89      private final OwnerReentrantReadWriteLock aggregatorLock = new OwnerReentrantReadWriteLock();
90  
91      private final Provider<MojosExecutionStrategy> mojosExecutionStrategy;
92  
93      private final MessageBuilderFactory messageBuilderFactory;
94  
95      private final Map<Thread, MojoDescriptor> mojos = new ConcurrentHashMap<>();
96  
97      @Inject
98      public MojoExecutor(
99              BuildPluginManager pluginManager,
100             MavenPluginManager mavenPluginManager,
101             LifecycleDependencyResolver lifeCycleDependencyResolver,
102             ExecutionEventCatapult eventCatapult,
103             Provider<MojosExecutionStrategy> mojosExecutionStrategy,
104             MessageBuilderFactory messageBuilderFactory) {
105         this.pluginManager = pluginManager;
106         this.mavenPluginManager = mavenPluginManager;
107         this.lifeCycleDependencyResolver = lifeCycleDependencyResolver;
108         this.eventCatapult = eventCatapult;
109         this.mojosExecutionStrategy = mojosExecutionStrategy;
110         this.messageBuilderFactory = messageBuilderFactory;
111     }
112 
113     public DependencyContext newDependencyContext(MavenSession session, List<MojoExecution> mojoExecutions) {
114         Set<String> scopesToCollect = new TreeSet<>();
115         Set<String> scopesToResolve = new TreeSet<>();
116 
117         collectDependencyRequirements(scopesToResolve, scopesToCollect, mojoExecutions);
118 
119         return new DependencyContext(session.getCurrentProject(), scopesToCollect, scopesToResolve);
120     }
121 
122     private void collectDependencyRequirements(
123             Set<String> scopesToResolve, Set<String> scopesToCollect, Collection<MojoExecution> mojoExecutions) {
124         for (MojoExecution mojoExecution : mojoExecutions) {
125             MojoDescriptor mojoDescriptor = mojoExecution.getMojoDescriptor();
126 
127             scopesToResolve.addAll(toScopes(mojoDescriptor.getDependencyResolutionRequired()));
128 
129             scopesToCollect.addAll(toScopes(mojoDescriptor.getDependencyCollectionRequired()));
130         }
131     }
132 
133     private Collection<String> toScopes(String classpath) {
134         Collection<String> scopes = Collections.emptyList();
135 
136         if (classpath != null && !classpath.isEmpty()) {
137             if (Artifact.SCOPE_COMPILE.equals(classpath)) {
138                 scopes = Arrays.asList(Artifact.SCOPE_COMPILE, Artifact.SCOPE_SYSTEM, Artifact.SCOPE_PROVIDED);
139             } else if (Artifact.SCOPE_RUNTIME.equals(classpath)) {
140                 scopes = Arrays.asList(Artifact.SCOPE_COMPILE, Artifact.SCOPE_RUNTIME);
141             } else if (Artifact.SCOPE_COMPILE_PLUS_RUNTIME.equals(classpath)) {
142                 scopes = Arrays.asList(
143                         Artifact.SCOPE_COMPILE, Artifact.SCOPE_SYSTEM, Artifact.SCOPE_PROVIDED, Artifact.SCOPE_RUNTIME);
144             } else if (Artifact.SCOPE_RUNTIME_PLUS_SYSTEM.equals(classpath)) {
145                 scopes = Arrays.asList(Artifact.SCOPE_COMPILE, Artifact.SCOPE_SYSTEM, Artifact.SCOPE_RUNTIME);
146             } else if (Artifact.SCOPE_TEST.equals(classpath)) {
147                 scopes = Arrays.asList(
148                         Artifact.SCOPE_COMPILE,
149                         Artifact.SCOPE_SYSTEM,
150                         Artifact.SCOPE_PROVIDED,
151                         Artifact.SCOPE_RUNTIME,
152                         Artifact.SCOPE_TEST);
153             }
154         }
155         return Collections.unmodifiableCollection(scopes);
156     }
157 
158     public void execute(final MavenSession session, final List<MojoExecution> mojoExecutions)
159             throws LifecycleExecutionException {
160 
161         final DependencyContext dependencyContext = newDependencyContext(session, mojoExecutions);
162 
163         final PhaseRecorder phaseRecorder = new PhaseRecorder(session.getCurrentProject());
164 
165         mojosExecutionStrategy.get().execute(mojoExecutions, session, new MojoExecutionRunner() {
166             @Override
167             public void run(MojoExecution mojoExecution) throws LifecycleExecutionException {
168                 MojoExecutor.this.execute(session, mojoExecution, dependencyContext, phaseRecorder);
169             }
170         });
171     }
172 
173     private void execute(
174             MavenSession session,
175             MojoExecution mojoExecution,
176             DependencyContext dependencyContext,
177             PhaseRecorder phaseRecorder)
178             throws LifecycleExecutionException {
179         execute(session, mojoExecution, dependencyContext);
180         phaseRecorder.observeExecution(mojoExecution);
181     }
182 
183     private void execute(MavenSession session, MojoExecution mojoExecution, DependencyContext dependencyContext)
184             throws LifecycleExecutionException {
185         MojoDescriptor mojoDescriptor = mojoExecution.getMojoDescriptor();
186 
187         try {
188             mavenPluginManager.checkPrerequisites(mojoDescriptor.getPluginDescriptor());
189         } catch (PluginIncompatibleException e) {
190             throw new LifecycleExecutionException(messageBuilderFactory, mojoExecution, session.getCurrentProject(), e);
191         }
192 
193         if (mojoDescriptor.isProjectRequired() && !session.getRequest().isProjectPresent()) {
194             Throwable cause = new MissingProjectException(
195                     "Goal requires a project to execute" + " but there is no POM in this directory ("
196                             + session.getExecutionRootDirectory() + ")."
197                             + " Please verify you invoked Maven from the correct directory.");
198             throw new LifecycleExecutionException(messageBuilderFactory, mojoExecution, null, cause);
199         }
200 
201         if (mojoDescriptor.isOnlineRequired() && session.isOffline()) {
202             if (MojoExecution.Source.CLI.equals(mojoExecution.getSource())) {
203                 Throwable cause = new IllegalStateException(
204                         "Goal requires online mode for execution" + " but Maven is currently offline.");
205                 throw new LifecycleExecutionException(
206                         messageBuilderFactory, mojoExecution, session.getCurrentProject(), cause);
207             } else {
208                 eventCatapult.fire(ExecutionEvent.Type.MojoSkipped, session, mojoExecution);
209 
210                 return;
211             }
212         }
213 
214         doExecute(session, mojoExecution, dependencyContext);
215     }
216 
217     protected static class NoLock implements NoExceptionCloseable {
218         public NoLock() {}
219 
220         @Override
221         public void close() {}
222     }
223 
224     /**
225      * Aggregating mojo executions (possibly) modify all MavenProjects, including those that are currently in use
226      * by concurrently running mojo executions. To prevent race conditions, an aggregating execution will block
227      * all other executions until finished.
228      * We also lock on a given project to forbid a forked lifecycle to be executed concurrently with the project.
229      * TODO: ideally, the builder should take care of the ordering in a smarter way
230      * TODO: and concurrency issues fixed with MNG-7157
231      */
232     protected class ProjectLock implements NoExceptionCloseable {
233         final Lock acquiredAggregatorLock;
234         final OwnerReentrantLock acquiredProjectLock;
235 
236         ProjectLock(MavenSession session, MojoDescriptor mojoDescriptor) {
237             mojos.put(Thread.currentThread(), mojoDescriptor);
238             boolean aggregator = mojoDescriptor.isAggregator();
239             acquiredAggregatorLock = aggregator ? aggregatorLock.writeLock() : aggregatorLock.readLock();
240             acquiredProjectLock = getProjectLock(session);
241             if (!acquiredAggregatorLock.tryLock()) {
242                 Thread owner = aggregatorLock.getOwner();
243                 MojoDescriptor ownerMojo = owner != null ? mojos.get(owner) : null;
244                 String str = ownerMojo != null ? " The " + ownerMojo.getId() : "An";
245                 String msg = str + " aggregator mojo is already being executed "
246                         + "in this parallel build, those kind of mojos require exclusive access to "
247                         + "reactor to prevent race conditions. This mojo execution will be blocked "
248                         + "until the aggregator mojo is done.";
249                 warn(msg);
250                 acquiredAggregatorLock.lock();
251             }
252             if (!acquiredProjectLock.tryLock()) {
253                 Thread owner = acquiredProjectLock.getOwner();
254                 MojoDescriptor ownerMojo = owner != null ? mojos.get(owner) : null;
255                 String str = ownerMojo != null ? " The " + ownerMojo.getId() : "A";
256                 String msg = str + " mojo is already being executed "
257                         + "on the project " + session.getCurrentProject().getGroupId()
258                         + ":" + session.getCurrentProject().getArtifactId() + ". "
259                         + "This mojo execution will be blocked "
260                         + "until the mojo is done.";
261                 warn(msg);
262                 acquiredProjectLock.lock();
263             }
264         }
265 
266         @Override
267         public void close() {
268             // release the lock in the reverse order of the acquisition
269             acquiredProjectLock.unlock();
270             acquiredAggregatorLock.unlock();
271             mojos.remove(Thread.currentThread());
272         }
273 
274         private OwnerReentrantLock getProjectLock(MavenSession session) {
275             SessionData data = session.getSession().getData();
276             Map<MavenProject, OwnerReentrantLock> locks = data.computeIfAbsent(PROJECT_LOCKS, ConcurrentHashMap::new);
277             return locks.computeIfAbsent(session.getCurrentProject(), p -> new OwnerReentrantLock());
278         }
279     }
280 
281     static class OwnerReentrantLock extends ReentrantLock {
282         @Override
283         public Thread getOwner() {
284             return super.getOwner();
285         }
286     }
287 
288     static class OwnerReentrantReadWriteLock extends ReentrantReadWriteLock {
289         @Override
290         public Thread getOwner() {
291             return super.getOwner();
292         }
293     }
294 
295     private static void warn(String msg) {
296         for (String s : MultilineMessageHelper.format(msg)) {
297             LOGGER.warn(s);
298         }
299     }
300 
301     private void doExecute(MavenSession session, MojoExecution mojoExecution, DependencyContext dependencyContext)
302             throws LifecycleExecutionException {
303         MojoDescriptor mojoDescriptor = mojoExecution.getMojoDescriptor();
304 
305         List<MavenProject> forkedProjects = executeForkedExecutions(mojoExecution, session);
306 
307         ensureDependenciesAreResolved(mojoDescriptor, session, dependencyContext);
308 
309         try (NoExceptionCloseable lock = getProjectLock(session, mojoDescriptor)) {
310             doExecute2(session, mojoExecution);
311         } finally {
312             for (MavenProject forkedProject : forkedProjects) {
313                 forkedProject.setExecutionProject(null);
314             }
315         }
316     }
317 
318     protected interface NoExceptionCloseable extends AutoCloseable {
319         @Override
320         void close();
321     }
322 
323     protected NoExceptionCloseable getProjectLock(MavenSession session, MojoDescriptor mojoDescriptor) {
324         if (useProjectLock(session)) {
325             return new ProjectLock(session, mojoDescriptor);
326         } else {
327             return new NoLock();
328         }
329     }
330 
331     protected boolean useProjectLock(MavenSession session) {
332         return session.getRequest().getDegreeOfConcurrency() > 1;
333     }
334 
335     private void doExecute2(MavenSession session, MojoExecution mojoExecution) throws LifecycleExecutionException {
336         eventCatapult.fire(ExecutionEvent.Type.MojoStarted, session, mojoExecution);
337         try {
338             try {
339                 pluginManager.executeMojo(session, mojoExecution);
340             } catch (MojoFailureException
341                     | PluginManagerException
342                     | PluginConfigurationException
343                     | MojoExecutionException
344                     | MavenException e) {
345                 throw new LifecycleExecutionException(
346                         messageBuilderFactory, mojoExecution, session.getCurrentProject(), e);
347             }
348 
349             eventCatapult.fire(ExecutionEvent.Type.MojoSucceeded, session, mojoExecution);
350         } catch (LifecycleExecutionException e) {
351             eventCatapult.fire(ExecutionEvent.Type.MojoFailed, session, mojoExecution, e);
352 
353             throw e;
354         }
355     }
356 
357     public void ensureDependenciesAreResolved(
358             MojoDescriptor mojoDescriptor, MavenSession session, DependencyContext dependencyContext)
359             throws LifecycleExecutionException {
360 
361         MavenProject project = dependencyContext.getProject();
362         boolean aggregating = mojoDescriptor.isAggregator();
363 
364         if (dependencyContext.isResolutionRequiredForCurrentProject()) {
365             Collection<String> scopesToCollect = dependencyContext.getScopesToCollectForCurrentProject();
366             Collection<String> scopesToResolve = dependencyContext.getScopesToResolveForCurrentProject();
367 
368             lifeCycleDependencyResolver.resolveProjectDependencies(
369                     project, scopesToCollect, scopesToResolve, session, aggregating, Collections.emptySet());
370 
371             dependencyContext.synchronizeWithProjectState();
372         }
373 
374         if (aggregating) {
375             Collection<String> scopesToCollect = toScopes(mojoDescriptor.getDependencyCollectionRequired());
376             Collection<String> scopesToResolve = toScopes(mojoDescriptor.getDependencyResolutionRequired());
377 
378             if (dependencyContext.isResolutionRequiredForAggregatedProjects(scopesToCollect, scopesToResolve)) {
379                 for (MavenProject aggregatedProject : session.getProjects()) {
380                     if (aggregatedProject != project) {
381                         lifeCycleDependencyResolver.resolveProjectDependencies(
382                                 aggregatedProject,
383                                 scopesToCollect,
384                                 scopesToResolve,
385                                 session,
386                                 aggregating,
387                                 Collections.emptySet());
388                     }
389                 }
390             }
391         }
392 
393         ArtifactFilter artifactFilter = getArtifactFilter(mojoDescriptor);
394         List<MavenProject> projectsToResolve = LifecycleDependencyResolver.getProjects(
395                 session.getCurrentProject(), session, mojoDescriptor.isAggregator());
396         for (MavenProject projectToResolve : projectsToResolve) {
397             projectToResolve.setArtifactFilter(artifactFilter);
398         }
399     }
400 
401     private ArtifactFilter getArtifactFilter(MojoDescriptor mojoDescriptor) {
402         String scopeToResolve = mojoDescriptor.getDependencyResolutionRequired();
403         String scopeToCollect = mojoDescriptor.getDependencyCollectionRequired();
404 
405         List<String> scopes = new ArrayList<>(2);
406         if (scopeToCollect != null && !scopeToCollect.isEmpty()) {
407             scopes.add(scopeToCollect);
408         }
409         if (scopeToResolve != null && !scopeToResolve.isEmpty()) {
410             scopes.add(scopeToResolve);
411         }
412 
413         if (scopes.isEmpty()) {
414             return null;
415         } else {
416             return new CumulativeScopeArtifactFilter(scopes);
417         }
418     }
419 
420     public List<MavenProject> executeForkedExecutions(MojoExecution mojoExecution, MavenSession session)
421             throws LifecycleExecutionException {
422         List<MavenProject> forkedProjects = Collections.emptyList();
423 
424         Map<String, List<MojoExecution>> forkedExecutions = mojoExecution.getForkedExecutions();
425 
426         if (!forkedExecutions.isEmpty()) {
427             eventCatapult.fire(ExecutionEvent.Type.ForkStarted, session, mojoExecution);
428 
429             MavenProject project = session.getCurrentProject();
430 
431             forkedProjects = new ArrayList<>(forkedExecutions.size());
432 
433             try {
434                 for (Map.Entry<String, List<MojoExecution>> fork : forkedExecutions.entrySet()) {
435                     String projectId = fork.getKey();
436 
437                     ProjectIndex projectIndex = session.getSession()
438                             .getData()
439                             .computeIfAbsent(PROJECT_INDEX, () -> new ProjectIndex(session.getProjects()));
440 
441                     int index = projectIndex.getIndices().get(projectId);
442 
443                     MavenProject forkedProject = projectIndex.getProjects().get(projectId);
444 
445                     forkedProjects.add(forkedProject);
446 
447                     MavenProject executedProject = forkedProject.clone();
448 
449                     forkedProject.setExecutionProject(executedProject);
450 
451                     List<MojoExecution> mojoExecutions = fork.getValue();
452 
453                     if (mojoExecutions.isEmpty()) {
454                         continue;
455                     }
456 
457                     try {
458                         session.setCurrentProject(executedProject);
459                         session.getProjects().set(index, executedProject);
460                         projectIndex.getProjects().put(projectId, executedProject);
461 
462                         eventCatapult.fire(ExecutionEvent.Type.ForkedProjectStarted, session, mojoExecution);
463 
464                         execute(session, mojoExecutions);
465 
466                         eventCatapult.fire(ExecutionEvent.Type.ForkedProjectSucceeded, session, mojoExecution);
467                     } catch (LifecycleExecutionException e) {
468                         eventCatapult.fire(ExecutionEvent.Type.ForkedProjectFailed, session, mojoExecution, e);
469 
470                         throw e;
471                     } finally {
472                         projectIndex.getProjects().put(projectId, forkedProject);
473                         session.getProjects().set(index, forkedProject);
474                         session.setCurrentProject(project);
475                     }
476                 }
477 
478                 eventCatapult.fire(ExecutionEvent.Type.ForkSucceeded, session, mojoExecution);
479             } catch (LifecycleExecutionException e) {
480                 eventCatapult.fire(ExecutionEvent.Type.ForkFailed, session, mojoExecution, e);
481 
482                 throw e;
483             }
484         }
485 
486         return forkedProjects;
487     }
488 }