/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.sling.maven.slingstart.run; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedList; import java.util.List; import java.util.Properties; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.DefaultArtifact; import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager; import org.apache.maven.artifact.resolver.ArtifactNotFoundException; import org.apache.maven.artifact.resolver.ArtifactResolutionException; import org.apache.maven.artifact.resolver.ArtifactResolver; import org.apache.maven.artifact.versioning.VersionRange; import org.apache.maven.execution.MavenSession; import org.apache.maven.model.Dependency; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.project.MavenProject; import org.apache.sling.maven.slingstart.BuildConstants; /** * Start one or multiple launchpad instance(s). */ @Mojo( name = "start", defaultPhase = LifecyclePhase.PRE_INTEGRATION_TEST, threadSafe = true ) public class StartMojo extends AbstractStartStopMojo { /** * Overwrites debug parameter of all server configurations (if set). * Attaches a debugger to the forked JVM. If set to {@code "true"}, the process will allow a debugger to connect on port 8000. * If set to some other string, that string will be appended to the server's {@code vmOpts}, allowing you to configure arbitrary debugging options. */ @Parameter(property = "launchpad.debug") protected String debug; /** * Ready timeout in seconds. If the launchpad has not been started in this * time, it's assumed that the startup failed. */ @Parameter(property = "launchpad.ready.timeout", defaultValue = "600") private int launchpadReadyTimeOutSec; /** * The launchpad jar. This option has precedence over "launchpadDependency". */ @Parameter(property = "launchpad.jar") private File launchpadJar; /** * The launchpad jar as a dependency. This is only used if "launchpadJar" is not * specified. */ @Parameter private Dependency launchpadDependency; /** * Clean the working directory before start. */ @Parameter(property = "launchpad.clean.workdir", defaultValue = "false") private boolean cleanWorkingDirectory; /** * Keep the launchpad running. * @deprecated Use {@link AbstractStartStopMojo#blockUntilKeyIsPressed} instead. */ @Deprecated @Parameter(property = "launchpad.keep.running", defaultValue = "false") private boolean keepLaunchpadRunning; /** * Set the execution of launchpad instances to be run in parallel (threads) */ @Parameter(property = "launchpad.parallelExecution", defaultValue = "true") private boolean parallelExecution; /** * The Maven project. */ @Parameter(property = "project", readonly = true, required = true) private MavenProject project; /** * The Maven session. */ @Parameter(property = "session", readonly = true, required = true) private MavenSession mavenSession; @Component private ArtifactHandlerManager artifactHandlerManager; /** * Used to look up Artifacts in the remote repository. * */ @Component private ArtifactResolver resolver; /** * Get a resolved Artifact from the coordinates provided * * @return the artifact, which has been resolved. * @throws MojoExecutionException */ private Artifact getArtifact(final Dependency d) throws MojoExecutionException { final Artifact prjArtifact = new DefaultArtifact(d.getGroupId(), d.getArtifactId(), VersionRange.createFromVersion(d.getVersion()), d.getScope(), d.getType(), d.getClassifier(), this.artifactHandlerManager.getArtifactHandler(d.getType())); try { this.resolver.resolve(prjArtifact, this.project.getRemoteArtifactRepositories(), this.mavenSession.getLocalRepository()); } catch (final ArtifactResolutionException e) { throw new MojoExecutionException("Unable to get artifact for " + d, e); } catch (ArtifactNotFoundException e) { throw new MojoExecutionException("Unable to get artifact for " + d, e); } return prjArtifact; } @Override protected void doExecute() throws MojoExecutionException, MojoFailureException { // delete properties if ( systemPropertiesFile != null && systemPropertiesFile.exists() ) { FileUtils.deleteQuietly(this.systemPropertiesFile); } // get configurations final Collection<ServerConfiguration> configurations = getLaunchpadConfigurations(); // create the common environment final LaunchpadEnvironment env = new LaunchpadEnvironment(this.findLaunchpadJar(), this.cleanWorkingDirectory, !this.keepLaunchpadRunning, this.launchpadReadyTimeOutSec, this.debug); // create callables final Collection<LauncherCallable> tasks = new LinkedList<LauncherCallable>(); for (final ServerConfiguration launchpadConfiguration : configurations) { validateConfiguration(launchpadConfiguration); tasks.add(createTask(launchpadConfiguration, env)); } // create the launchpad runner properties this.createLaunchpadRunnerProperties(configurations); if (parallelExecution) { // ExecutorService for starting launchpad instances in parallel final ExecutorService executor = Executors.newCachedThreadPool(); try { final List<Future<ProcessDescription>> resultsCollector = executor.invokeAll(tasks); for (final Future<ProcessDescription> future : resultsCollector) { try { if (null == future.get()) { throw new MojoExecutionException("Cannot start all the instances"); } } catch (final ExecutionException e) { throw new MojoExecutionException(e.getLocalizedMessage(), e); } } } catch ( final InterruptedException e) { throw new MojoExecutionException(e.getLocalizedMessage(), e); } } else { for (final LauncherCallable task : tasks) { try { if (null == task.call()) { throw new MojoExecutionException("Cannot start all the instances"); } } catch (final Exception e) { throw new MojoExecutionException(e.getLocalizedMessage(), e); } } } if (this.keepLaunchpadRunning) { getLog().info("Press CTRL-C to stop launchpad instance(s)..."); while ( true && this.isRunning(tasks)) { try { Thread.sleep(5000); } catch (final InterruptedException ie) { break; } } } blockIfNecessary(); } /** * Are all launchpads still running? */ private boolean isRunning(final Collection<LauncherCallable> tasks) { for(final LauncherCallable task : tasks) { if ( !task.isRunning() ) { return false; } } return true; } private void createLaunchpadRunnerProperties(final Collection<ServerConfiguration> configurations) throws MojoExecutionException { // create properties OutputStream writer = null; final Properties props = new Properties(); try { writer = new FileOutputStream(this.systemPropertiesFile); // disable sling startup check props.put("launchpad.skip.startupcheck", "true"); // write out all instances int index = 0; for (final ServerConfiguration launchpadConfiguration : configurations) { index++; props.put("launchpad.instance.id." + String.valueOf(index), launchpadConfiguration.getId()); String runMode = launchpadConfiguration.getRunmode(); if ( runMode == null ) { runMode = ""; } props.put("launchpad.instance.runmode." + String.valueOf(index), runMode); props.put("launchpad.instance.server." + String.valueOf(index), launchpadConfiguration.getServer()); props.put("launchpad.instance.port." + String.valueOf(index), launchpadConfiguration.getPort()); props.put("launchpad.instance.contextPath." + String.valueOf(index), launchpadConfiguration.getContextPath()); final String url = createServerUrl(launchpadConfiguration); props.put("launchpad.instance.url." + String.valueOf(index), url); } props.put("launchpad.instances", String.valueOf(index)); props.store(writer, null); } catch (final IOException e) { throw new MojoExecutionException(e.getLocalizedMessage(), e); } finally { IOUtils.closeQuietly(writer); } } private static String createServerUrl(final ServerConfiguration qc) { final StringBuilder sb = new StringBuilder(); sb.append("http://"); sb.append(qc.getServer()); if ( !qc.getPort().equals("80") ) { sb.append(':'); sb.append(qc.getPort()); } final String contextPath = qc.getContextPath(); if ( contextPath != null && contextPath.trim().length() > 0 && !contextPath.equals("/") ) { if ( !contextPath.startsWith("/") ) { sb.append('/'); } if ( contextPath.endsWith("/") ) { sb.append(contextPath, 0, contextPath.length()-1); } else { sb.append(contextPath); } } return sb.toString(); } /** * @param launchpadConfiguration */ private LauncherCallable createTask(final ServerConfiguration launchpadConfiguration, final LaunchpadEnvironment env) throws MojoExecutionException, MojoFailureException { final String id = launchpadConfiguration.getId(); getLog().debug(new StringBuilder("Starting ").append(id). append(" with runmode ").append(launchpadConfiguration.getRunmode()). append(" on port ").append(launchpadConfiguration.getPort()). append(" in folder ").append(launchpadConfiguration.getFolder().getAbsolutePath()).toString()); // create task return new LauncherCallable(this.getLog(), launchpadConfiguration, env); } /** * Validate a configuration * @param launchpadConfiguration The launchpad configuration * @throws MojoExecutionException */ private void validateConfiguration(final ServerConfiguration launchpadConfiguration) throws MojoExecutionException { if ( launchpadConfiguration.getPort() == null ) { launchpadConfiguration.setPort(String.valueOf(PortHelper.getNextAvailablePort())); } if ( launchpadConfiguration.getControlPort() == null ) { launchpadConfiguration.setControlPort(String.valueOf(PortHelper.getNextAvailablePort())); } // set the id of the launchpad if ( launchpadConfiguration.getId() == null || launchpadConfiguration.getId().trim().length() == 0 ) { String runMode = launchpadConfiguration.getRunmode(); if ( runMode == null ) { runMode = "_"; } final String id = new StringBuilder(runMode.replace(',', '_')).append('-').append(launchpadConfiguration.getPort()).toString(); launchpadConfiguration.setId(id); } // populate folder if not set if (launchpadConfiguration.getFolder() == null) { final File folder = new File(new StringBuilder(this.project.getBuild().getDirectory()).append('/').append(launchpadConfiguration.getId()).toString()); launchpadConfiguration.setFolder(folder); } // context path should not be null if ( launchpadConfiguration.getContextPath() == null ) { launchpadConfiguration.setContextPath(""); } if ( launchpadConfiguration.getInstances() < 0 ) { launchpadConfiguration.setInstances(1); } } /** * Finds the launchpad.jar artifact of the project being built. * * @return the launchpad.jar artifact * @throws MojoFailureException if a launchpad.jar artifact was not found */ private File findLaunchpadJar() throws MojoFailureException, MojoExecutionException { // If a launchpad JAR is specified, use it if (launchpadJar != null) { getLog().info("Using launchpad jar from '" + launchpadJar + "' given as configuration parameter!"); return launchpadJar; } // If a launchpad dependency is configured, resolve it if (launchpadDependency != null) { getLog().info("Using launchpad dependency '" + launchpadDependency + "' given as configuration parameter!"); return getArtifact(launchpadDependency).getFile(); } // If the current project is a slingstart project, use its JAR artifact if (this.project.getPackaging().equals(BuildConstants.PACKAGING_SLINGSTART)) { File jarFile = project.getArtifact().getFile(); if (jarFile != null && jarFile.exists()) { getLog().info("Using launchpad jar being generated as this project's primary artifact: '" + jarFile + "'!"); return jarFile; } else { jarFile = new File(project.getBuild().getDirectory(), project.getBuild().getFinalName() + ".jar"); if (jarFile.exists()) { getLog().info("Using launchpad jar being generated as this project's primary artifact: '" + jarFile + "'!"); return jarFile; } } } // In case there was a provisioning model found but this is not a slingstart project, the JAR might be attached already through goal "package" for (Artifact attachedArtifact : project.getAttachedArtifacts()) { // find the attached artifact with classifier "standalone" if (BuildConstants.TYPE_JAR.equals(attachedArtifact.getType()) && BuildConstants.CLASSIFIER_APP.equals(attachedArtifact.getClassifier())) { getLog().info("Using launchpad jar being attached as additional project artifact: '" + attachedArtifact.getFile() + "'!"); return attachedArtifact.getFile(); } } // Last chance: use the first declared dependency with type "slingstart" final Set<Artifact> dependencies = this.project.getDependencyArtifacts(); for (final Artifact dep : dependencies) { if (BuildConstants.PACKAGING_SLINGSTART.equals(dep.getType())) { final Dependency d = new Dependency(); d.setGroupId(dep.getGroupId()); d.setArtifactId(dep.getArtifactId()); d.setVersion(dep.getVersion()); d.setScope(Artifact.SCOPE_RUNTIME); d.setType(BuildConstants.TYPE_JAR); getLog().info("Using launchpad jar from first dependency of type 'slingstart': '"+ d +"'!"); return getArtifact(d).getFile(); } } // Launchpad has not been found, throw an exception throw new MojoFailureException("Could not find the launchpad jar. " + "Either specify the 'launchpadJar' configuration or use this inside a slingstart project."); } /** * Get all configurations * @return Collection of configurations. */ private Collection<ServerConfiguration> getLaunchpadConfigurations() { final List<ServerConfiguration> configs = new ArrayList<ServerConfiguration>(); if ( this.servers != null && !this.servers.isEmpty() ) { for(final ServerConfiguration config : this.servers) { // if instances is set to 0, no instance is added if ( config.getInstances() != 0 ) { configs.add(config); for(int i=2; i<=config.getInstances();i++) { final ServerConfiguration replicaConfig = config.copy(); replicaConfig.setPort(null); final File folder = replicaConfig.getFolder(); if ( folder != null ) { replicaConfig.setFolder(new File(folder.getParentFile(), folder.getName() + '-' + String.valueOf(i))); } configs.add(replicaConfig); } config.setInstances(1); } } } else { // use single default instance configs.add(new ServerConfiguration()); } return configs; } }