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