blob: 2278d6f87cb9ebe97e7f8359a43a77540ba45a3b [file] [log] [blame]
/*
* 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.feature.launcher;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.ProcessBuilder.Redirect;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
import org.apache.maven.artifact.versioning.VersionRange;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.Dependency;
import org.apache.maven.plugin.AbstractMojo;
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.maven.shared.utils.Os;
import org.codehaus.plexus.archiver.UnArchiver;
import org.codehaus.plexus.archiver.manager.ArchiverManager;
import org.codehaus.plexus.archiver.manager.NoSuchArchiverException;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.impl.ArtifactResolver;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.resolution.ArtifactRequest;
import org.eclipse.aether.resolution.ArtifactResolutionException;
import org.eclipse.aether.resolution.ArtifactResult;
/**
* Start one or multiple <a href="https://sling.apache.org/documentation/development/feature-model.html">Sling Feature(s)</a>.
*/
@Mojo( name = "start", defaultPhase = LifecyclePhase.PRE_INTEGRATION_TEST )
public class StartMojo extends AbstractMojo {
private static final String JAVA_HOME = "JAVA_HOME";
private static final String JAVA_OPTS = "JAVA_OPTS";
/**
* The directory in which the features are launched (below its child directory {@code launchers/<launch-id>}).
*/
@Parameter( defaultValue = "${project.build.directory}", property = "outputDir", required = true )
private File outputDirectory;
/**
* The version of the <a href="https://github.com/apache/sling-org-apache-sling-feature-launcher">Sling Feature Launcher</a> to use.
*/
@Parameter( required = true, defaultValue = "1.1.4")
private String featureLauncherVersion;
// TODO: extract this field into common parent class
/**
* List of {@link Launch} objects to start. Each is having the following format:
* <pre>{@code
* <id>...</id> <!-- the id of the launch, must be unique within the list, is mandatory -->
* <dependency>...</dependency> <!-- the Maven coordinates of the feature model -->
* <launcherArguments> <!-- additional arguments to pass to the launcher -->
* <frameworkProperties>
* <org.osgi.service.http.port>8090</org.osgi.service.http.port>
* </framweworkProperties>
* ..
* </launcherArguments>
* <environmentVariables><!--additional environment variables to pass to the launcher -->
* <JAVA_HOME>...</JAVA_HOME>
* </environmentVariables>}
* </pre>
*/
@Parameter(required = true)
private List<Launch> launches;
@Component
private ArtifactResolver resolver;
@Parameter(defaultValue = "${project.remotePluginRepositories}", readonly = true)
private List<RemoteRepository> remoteRepos;
@Parameter(property = "project", readonly = true, required = true)
protected MavenProject project;
@Parameter(property = "session", readonly = true, required = true)
protected MavenSession mavenSession;
@Component
private ProcessTracker processes;
/**
* To look up UnArchiver implementations
*/
@Component
private ArchiverManager archiverManager;
public void execute() throws MojoExecutionException, MojoFailureException {
try {
// the feature launcher before version 1.1.28 used a single jar, while versions
// after that provide an assembly per SLING-10956
VersionRange beforeAssemblyRange = VersionRange.createFromVersionSpec("(,1.1.26]");
boolean useAssembly = !beforeAssemblyRange.containsVersion(new DefaultArtifactVersion(featureLauncherVersion));
RepositorySystemSession repositorySession = mavenSession.getRepositorySession();
File workDir = new File(outputDirectory, "launchers");
workDir.mkdirs();
File launcher;
if (useAssembly) {
// fetch the assembly artifact
Artifact launcherAssemblyArtifact = new DefaultArtifact("org.apache.sling:org.apache.sling.feature.launcher:tar.gz:" + featureLauncherVersion);
File assemblyArchive = resolver
.resolveArtifact(repositorySession, new ArtifactRequest(launcherAssemblyArtifact, remoteRepos, null))
.getArtifact()
.getFile();
// unpack the file
UnArchiver unArchiver = archiverManager.getUnArchiver( assemblyArchive );
unArchiver.setSourceFile(assemblyArchive);
unArchiver.setDestFile(workDir);
unArchiver.extract();
// system property
Path relPath = Paths.get(launcherAssemblyArtifact.getArtifactId() + "-" + launcherAssemblyArtifact.getVersion(), "bin");
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
relPath = relPath.resolve("launcher.bat");
} else {
relPath = relPath.resolve("launcher");
}
launcher = workDir.toPath().resolve(relPath).toFile();
} else {
Artifact launcherArtifact = new DefaultArtifact("org.apache.sling:org.apache.sling.feature.launcher:" + featureLauncherVersion);
launcher = resolver
.resolveArtifact(repositorySession, new ArtifactRequest(launcherArtifact, remoteRepos, null))
.getArtifact()
.getFile();
}
for ( Launch launch : launches ) {
if (launch.isSkip()) {
getLog().info("Skipping starting launch with id " + launch.getId());
continue; // skip it
}
launch.validate();
Artifact artifact = toArtifact(launch.getFeature());
ArtifactResult result = resolver.resolveArtifact(repositorySession, new ArtifactRequest(artifact, remoteRepos, null));
File featureFile = result.getArtifact().getFile();
String javahome = System.getenv(JAVA_HOME);
if (javahome == null || javahome.isEmpty()) {
// SLING-9843 fallback to java.home system property if JAVA_HOME env variable is not set
getLog().warn("The JAVA_HOME env variable was not set, falling back to the java.home system property");
javahome = System.getProperty("java.home");
}
List<String> args = new ArrayList<>();
if (useAssembly) {
// use the post v1.1.28 launcher script
Map<String, String> newEnv = new HashMap<>(launch.getEnvironmentVariables());
newEnv.put(JAVA_HOME, javahome);
// SLING-9994 - if any extra vm options were supplied, apply them here
StringBuilder javaOptsBuilder = null;
String[] vmOptions = launch.getLauncherArguments().getVmOptions();
for (String vmOption : vmOptions) {
if (vmOption != null && !vmOption.isEmpty()) {
if (javaOptsBuilder == null) {
javaOptsBuilder = new StringBuilder();
} else {
javaOptsBuilder.append(" ");
}
javaOptsBuilder.append(vmOption);
}
}
if (javaOptsBuilder != null) {
// pass vmOptions through JAVA_OPTS environment variable?
if (newEnv.containsKey(JAVA_OPTS)) {
// if the original value existed append it to our buffer
javaOptsBuilder.append(" ").append(newEnv.get(JAVA_OPTS));
}
newEnv.put(JAVA_OPTS, javaOptsBuilder.toString());
}
args.add(launcher.getAbsolutePath());
launch.setEnvironmentVariables(newEnv);
} else {
// use the pre v1.1.28 single jar technique
args.add(javahome + File.separatorChar + "bin" + File.separatorChar + "java");
// SLING-9994 - if any extra vm options were supplied, apply them here
String[] vmOptions = launch.getLauncherArguments().getVmOptions();
for (String vmOption : vmOptions) {
if (vmOption != null && !vmOption.isEmpty()) {
args.add(vmOption);
}
}
args.add("-jar");
args.add(launcher.getAbsolutePath());
}
args.add("-f");
args.add(featureFile.getAbsolutePath());
args.add("-p");
args.add(launch.getId());
for ( Map.Entry<String, String> frameworkProperty : launch.getLauncherArguments().getFrameworkProperties().entrySet() ) {
args.add("-D");
args.add(frameworkProperty.getKey()+"="+frameworkProperty.getValue());
}
for ( Map.Entry<String, String> variable : launch.getLauncherArguments().getVariables().entrySet() ) {
args.add("-V");
args.add(variable.getKey()+"="+variable.getValue());
}
// TODO - add support for all arguments supported by the feature launcher
ProcessBuilder pb = new ProcessBuilder(args);
pb.redirectOutput(Redirect.INHERIT);
pb.redirectInput(Redirect.INHERIT);
pb.directory(workDir);
launch.getEnvironmentVariables().entrySet()
.forEach( e -> {
getLog().info("Setting environment variable '" + e.getKey() + "' to '" + e.getValue() + "'");
pb.environment().put(e.getKey(), e.getValue());
} );
getLog().info("Starting launch with id '" + launch.getId() + "', args=" + args);
CountDownLatch latch = new CountDownLatch(1);
Process process = pb.start();
Thread monitor = new Thread("launch-monitor-" + launch.getId()) {
@Override
public void run() {
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
String line;
try {
while ( (line = reader.readLine()) != null ) {
System.out.println(line); // NOSONAR - we pass through the subprocess stderr
if ( line.contains("Framework started")) {
latch.countDown();
break;
}
}
} catch (IOException e) {
getLog().warn(e.getMessage(), e);
}
}
};
monitor.start();
getLog().info("Waiting for " + launch.getId() + " to start");
boolean started = latch.await(launch.getStartTimeoutSeconds(), TimeUnit.SECONDS);
if ( !started ) {
ProcessTracker.stop(process);
throw new MojoExecutionException("Launch " + launch.getId() + " failed to start in " + launch.getStartTimeoutSeconds() + " seconds.");
}
processes.startTracking(launch.getId(), process);
}
} catch (NoSuchArchiverException | InvalidVersionSpecificationException | ArtifactResolutionException | IOException e) {
throw new MojoExecutionException(e.getMessage(), e);
} catch ( InterruptedException e ) {
Thread.currentThread().interrupt();
throw new MojoExecutionException("Execution interrupted", e);
}
}
private Artifact toArtifact(Dependency dependency) {
return new DefaultArtifact(dependency.getGroupId(), dependency.getArtifactId(), dependency.getClassifier(), dependency.getType(), dependency.getVersion());
}
}