You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ro...@apache.org on 2017/11/07 10:26:15 UTC
[sling-slingstart-maven-plugin] 04/18: SLING-4474 : Provide a way
to start/stop an instance through maven mojos
This is an automated email from the ASF dual-hosted git repository.
rombert pushed a commit to annotated tag slingstart-maven-plugin-1.1.0
in repository https://gitbox.apache.org/repos/asf/sling-slingstart-maven-plugin.git
commit b0c766a2725ce3a57f48ccfbb4a401b59d0b1949
Author: Carsten Ziegeler <cz...@apache.org>
AuthorDate: Sat Mar 14 12:44:44 2015 +0000
SLING-4474 : Provide a way to start/stop an instance through maven mojos
git-svn-id: https://svn.apache.org/repos/asf/sling/trunk/tooling/maven/slingstart-maven-plugin@1666678 13f79535-47bb-0310-9956-ffa450edef68
---
.../sling/maven/slingstart/launcher/Launcher.java | 95 +++++
.../maven/slingstart/launcher/LauncherMBean.java | 34 ++
.../sling/maven/slingstart/launcher/Main.java | 105 +++++
.../maven/slingstart/run/ControlListener.java | 160 ++++++++
.../maven/slingstart/run/LauncherCallable.java | 328 +++++++++++++++
.../maven/slingstart/run/LaunchpadEnvironment.java | 140 +++++++
.../sling/maven/slingstart/run/PortHelper.java | 49 +++
.../maven/slingstart/run/ProcessDescription.java | 81 ++++
.../slingstart/run/ProcessDescriptionProvider.java | 106 +++++
.../maven/slingstart/run/ServerConfiguration.java | 157 ++++++++
.../sling/maven/slingstart/run/StartMojo.java | 444 +++++++++++++++++++++
.../sling/maven/slingstart/run/StopMojo.java | 89 +++++
12 files changed, 1788 insertions(+)
diff --git a/src/main/java/org/apache/sling/maven/slingstart/launcher/Launcher.java b/src/main/java/org/apache/sling/maven/slingstart/launcher/Launcher.java
new file mode 100644
index 0000000..8b72dc0
--- /dev/null
+++ b/src/main/java/org/apache/sling/maven/slingstart/launcher/Launcher.java
@@ -0,0 +1,95 @@
+/*
+ * 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.launcher;
+
+import java.io.BufferedReader;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.List;
+
+public class Launcher implements LauncherMBean {
+
+ private final int listenerPort;
+
+ public Launcher(final int listenerPort) {
+ this.listenerPort = listenerPort;
+ }
+
+ @Override
+ public void startupFinished() {
+ final List<String> hosts = new ArrayList<String>();
+ hosts.add("localhost");
+ hosts.add("127.0.0.1");
+
+ boolean done = false;
+ int index = 0;
+ while ( !done && index < hosts.size() ) {
+ final String hostName = hosts.get(index);
+ final int twoMinutes = 2 * 60 * 1000;
+
+ Socket clientSocket = null;
+ DataOutputStream out = null;
+ BufferedReader in = null;
+ try {
+ clientSocket = new Socket();
+ clientSocket.connect(new InetSocketAddress(hostName, listenerPort), twoMinutes);
+ // without that, read() call on the InputStream associated with this Socket is infinite
+ clientSocket.setSoTimeout(twoMinutes);
+
+ out = new DataOutputStream(clientSocket.getOutputStream());
+ in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
+ out.writeBytes("started\n");
+ in.readLine();
+ done = true;
+ } catch (final Throwable ignore) {
+ // catch Throwable because InetSocketAddress and Socket#connect throws unchecked exceptions
+ // we ignore this for now
+ } finally {
+ if ( in != null ) {
+ try {
+ in.close();
+ } catch ( final IOException ioe) {
+ // ignore
+ }
+ }
+ if ( out != null ) {
+ try {
+ out.close();
+ } catch ( final IOException ioe) {
+ // ignore
+ }
+ }
+ if ( clientSocket != null ) {
+ try {
+ clientSocket.close();
+ } catch (final IOException e) {
+ // ignore
+ }
+ }
+ }
+ index++;
+ }
+ }
+
+ @Override
+ public void startupProgress(Float ratio) {
+ // nothing to do
+ }
+}
diff --git a/src/main/java/org/apache/sling/maven/slingstart/launcher/LauncherMBean.java b/src/main/java/org/apache/sling/maven/slingstart/launcher/LauncherMBean.java
new file mode 100644
index 0000000..1247bfa
--- /dev/null
+++ b/src/main/java/org/apache/sling/maven/slingstart/launcher/LauncherMBean.java
@@ -0,0 +1,34 @@
+/*
+ * 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.launcher;
+
+/**
+ * The launcher MBean interface.
+ */
+public interface LauncherMBean {
+
+ /**
+ * Notify the launcher about the finish of the startup.
+ */
+ void startupFinished();
+
+ /**
+ * Notify the launcher about the progress of the startup.
+ * @param ratio Startup progress ratio
+ */
+ void startupProgress(Float ratio);
+}
diff --git a/src/main/java/org/apache/sling/maven/slingstart/launcher/Main.java b/src/main/java/org/apache/sling/maven/slingstart/launcher/Main.java
new file mode 100644
index 0000000..3a72704
--- /dev/null
+++ b/src/main/java/org/apache/sling/maven/slingstart/launcher/Main.java
@@ -0,0 +1,105 @@
+/*
+ * 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.launcher;
+
+import java.io.File;
+import java.lang.management.ManagementFactory;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.net.URLClassLoader;
+
+import javax.management.MBeanServer;
+import javax.management.ObjectName;
+
+/**
+ * Main class for launching Apache Sling.
+ *
+ */
+public class Main {
+
+ /** Arguments to pass to the real main class */
+ private final String[] startupArgs;
+
+ /** Verbose flag */
+ private final boolean verbose;
+
+ /** App jar */
+ private final File appJar;
+
+ /** Listener port. */
+ private final int listenerPort;
+
+ /** Main class default value */
+ private final static String MAIN_CLASS_DEF = "org.apache.sling.launchpad.app.Main";
+
+ /** Delimeter string */
+ private final static String DELIM =
+ "-------------------------------------------------------------------";
+
+ /**
+ * Create a new launcher
+ * First argument is the launchpad jar
+ * Second argument is the listener port
+ * Third argument is verbose
+ */
+ public Main(final String[] args) {
+ if ( args == null || args.length < 3 ) {
+ throw new IllegalArgumentException("Missing configuration: " + args);
+ }
+ this.appJar = new File(args[0]);
+ this.listenerPort = Integer.valueOf(args[1]);
+ this.verbose = Boolean.valueOf(args[2]);
+ this.startupArgs = new String[args.length-3];
+ System.arraycopy(args, 3, this.startupArgs, 0, this.startupArgs.length);
+ }
+
+ /**
+ * Startup
+ */
+ public void run() throws Exception {
+ if (verbose) {
+ System.out.println(DELIM);
+ System.out.println("Slingstart application: " + this.appJar);
+ System.out.println("Main class: " + MAIN_CLASS_DEF);
+ System.out.println("Listener Port: " + String.valueOf(this.listenerPort));
+ System.out.println(DELIM);
+ }
+
+ final ClassLoader cl = new URLClassLoader(new URL[] {this.appJar.toURI().toURL()});
+ Thread.currentThread().setContextClassLoader(cl);
+
+ // create and register mbean
+ final MBeanServer jmxServer = ManagementFactory.getPlatformMBeanServer();
+ jmxServer.registerMBean(new Launcher(this.listenerPort),
+ new ObjectName("org.apache.sling.launchpad:type=Launcher"));
+
+ final Class<?> mainClass = cl.loadClass(MAIN_CLASS_DEF);
+ final Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
+ mainMethod.invoke(null, (Object)this.startupArgs);
+ }
+
+ public static void main(final String[] args) {
+ try {
+ final Main m = new Main(args);
+ m.run();
+ } catch ( final Exception e) {
+ e.printStackTrace();
+ System.exit(1);
+ }
+ }
+}
+
diff --git a/src/main/java/org/apache/sling/maven/slingstart/run/ControlListener.java b/src/main/java/org/apache/sling/maven/slingstart/run/ControlListener.java
new file mode 100644
index 0000000..9aee56a
--- /dev/null
+++ b/src/main/java/org/apache/sling/maven/slingstart/run/ControlListener.java
@@ -0,0 +1,160 @@
+/*
+ * 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.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+
+/**
+ * Control listener.
+ * This class listens for the startup of a launchpad instance.
+ */
+public class ControlListener implements Runnable {
+
+ // command sent by the client to notify startup
+ private static final String COMMAND_STARTED = "started";
+
+ private static final String RESPONSE_OK = "ok";
+
+ // The default interface to listen on
+ private static final String DEFAULT_LISTEN_INTERFACE = "127.0.0.1";
+
+ // The port to listen on
+ private final int port;
+
+ private volatile boolean started = false;
+
+ private volatile boolean stopped = false;
+
+ private volatile ServerSocket server;
+
+ public ControlListener(final int p) {
+ this.port = p;
+ final Thread listener = new Thread(this);
+ listener.setDaemon(true);
+ listener.setName("Launchapd startup listener");
+ listener.start();
+ }
+
+ public int getPort() {
+ return this.port;
+ }
+
+ public boolean isStarted() {
+ return this.started;
+ }
+
+ public void stop() {
+ stopped = true;
+ if ( server != null ) {
+ try {
+ server.close();
+ } catch (IOException e) {
+ // ignore
+ }
+ }
+ }
+
+ /**
+ * Implements the server thread receiving commands from clients and acting
+ * upon them.
+ */
+ @Override
+ public void run() {
+ final InetSocketAddress socketAddress = getSocketAddress(this.port);
+ try {
+ server = new ServerSocket();
+ server.bind(socketAddress);
+ } catch (final IOException ioe) {
+ return;
+ }
+
+ try {
+ while (!stopped) {
+
+ final Socket s = server.accept();
+
+ try {
+ final String commandLine = readLine(s);
+ if (commandLine == null) {
+ final String msg = "ERR: missing command";
+ writeLine(s, msg);
+ continue;
+ }
+
+ final String command = commandLine;
+
+ if (COMMAND_STARTED.equals(command)) {
+ writeLine(s, RESPONSE_OK);
+ this.started = true;
+ this.stopped = true;
+ break;
+
+ } else {
+ final String msg = "ERR:" + command;
+ writeLine(s, msg);
+
+ }
+ } finally {
+ try {
+ s.close();
+ } catch (IOException ignore) {
+ }
+ }
+ }
+ } catch (final IOException ioe) {
+ // ignore
+ } finally {
+ try {
+ server.close();
+ } catch (final IOException ignore) {
+ // ignore
+ }
+ }
+ }
+
+ private String readLine(final Socket socket) throws IOException {
+ final BufferedReader br = new BufferedReader(new InputStreamReader(
+ socket.getInputStream(), "UTF-8"));
+ return br.readLine();
+ }
+
+ private void writeLine(final Socket socket, final String line) throws IOException {
+ final BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(
+ socket.getOutputStream(), "UTF-8"));
+ bw.write(line);
+ bw.write("\r\n");
+ bw.flush();
+ }
+
+ private static InetSocketAddress getSocketAddress(final int port) {
+ final String address = DEFAULT_LISTEN_INTERFACE;
+
+ final InetSocketAddress addr = new InetSocketAddress(address, port);
+ if (!addr.isUnresolved()) {
+ return addr;
+ }
+
+ return null;
+ }
+}
diff --git a/src/main/java/org/apache/sling/maven/slingstart/run/LauncherCallable.java b/src/main/java/org/apache/sling/maven/slingstart/run/LauncherCallable.java
new file mode 100644
index 0000000..6ac2b13
--- /dev/null
+++ b/src/main/java/org/apache/sling/maven/slingstart/run/LauncherCallable.java
@@ -0,0 +1,328 @@
+/*
+ * 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.BufferedReader;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.LineNumberReader;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.maven.plugin.logging.Log;
+import org.apache.sling.maven.slingstart.launcher.Main;
+
+/**
+ * A callable for launchpad an instance
+ */
+public class LauncherCallable implements Callable<ProcessDescription> {
+
+ private final LaunchpadEnvironment environment;
+ private final ServerConfiguration configuration;
+ private final Log logger;
+
+ public LauncherCallable(final Log logger,
+ final ServerConfiguration configuration,
+ final LaunchpadEnvironment environment) {
+ this.logger = logger;
+ this.configuration = configuration;
+ this.environment = environment;
+ }
+
+ /**
+ * @see java.util.concurrent.Callable#call()
+ */
+ @Override
+ public ProcessDescription call() throws Exception {
+
+ // fail if launchpad with this id is already started
+ if (!ProcessDescriptionProvider.getInstance().isRunConfigurationAvailable(configuration.getId())) {
+ throw new Exception("Launchpad with id " + configuration.getId() + " is not available");
+ }
+
+ // get the launchpad jar
+ final File launchpad = this.environment.prepare(this.configuration.getFolder());
+
+ // Lock the launchpad id
+ final String launchpadKey = ProcessDescriptionProvider.getInstance().getId(configuration.getId());
+
+ // start launchpad
+ ProcessDescription cfg = this.start(launchpad);
+
+ // Add thread hook to shutdown launchpad
+ if (environment.isShutdownOnExit()) {
+ cfg.installShutdownHook();
+ }
+
+ // Add configuration to the config provider
+ ProcessDescriptionProvider.getInstance().addRunConfiguration(cfg, launchpadKey);
+
+ boolean started = false;
+ try {
+ final long endTime = System.currentTimeMillis() + this.environment.getReadyTimeOutSec() * 1000;
+ boolean finished = false;
+ while ( !started && !finished && System.currentTimeMillis() < endTime ) {
+ Thread.sleep(5000);
+ started = cfg.getControlListener().isStarted();
+ try {
+ // if we get an exit value, the process has stopped
+ cfg.getProcess().exitValue();
+ finished = true;
+ } catch ( final IllegalThreadStateException itse) {
+ // everything as expected
+ }
+ }
+
+ if ( finished ) {
+ throw new Exception("Launchpad did exit unexpectedly.");
+ }
+ if ( !started ) {
+ throw new Exception("Launchpad did not start successfully in " + this.environment.getReadyTimeOutSec() + " seconds.");
+ }
+ this.logger.info("Started Launchpad " + configuration.getId() +
+ " [" + configuration.getRunmode() + ", " + configuration.getPort() + "]");
+ } finally {
+ // stop control port
+ cfg.getControlListener().stop();
+
+ // call launchpad stop routine if not properly started
+ if (!started) {
+ stop(this.logger, cfg);
+ ProcessDescriptionProvider.getInstance().removeRunConfiguration(cfg.getId());
+ cfg = null;
+ }
+ }
+
+ return cfg;
+ }
+
+ public boolean isRunning() {
+ return getControlPortFile(this.configuration.getFolder()).exists();
+ }
+
+ private void add(final List<String> args, final String value) {
+ if ( value != null ) {
+ final String[] single = value.trim().split(" ");
+ for(final String v : single) {
+ if ( v.trim().length() > 0 ) {
+ args.add(v.trim());
+ }
+ }
+ }
+ }
+
+ private ProcessDescription start(final File jar) throws Exception {
+ final ProcessDescription cfg = new ProcessDescription(this.configuration.getId(), this.configuration.getFolder());
+
+ final ProcessBuilder builder = new ProcessBuilder();
+ final List<String> args = new ArrayList<String>();
+
+ args.add("java");
+ add(args, this.configuration.getVmOpts());
+
+ args.add("-cp");
+ args.add("bin");
+ args.add(Main.class.getName());
+ // first three arguments: jar, listener port, verbose
+ args.add(jar.getPath());
+ args.add(String.valueOf(cfg.getControlListener().getPort()));
+ args.add("true");
+
+ // from here on launchpad properties
+ add(args, this.configuration.getOpts());
+
+ final String contextPath = this.configuration.getContextPath();
+ if ( contextPath != null && contextPath.length() > 0 && !contextPath.equals("/") ) {
+ args.add("-r");
+ args.add(contextPath);
+ }
+
+ if ( this.configuration.getPort() != null ) {
+ args.add("-p");
+ args.add(this.configuration.getPort());
+ }
+
+ if ( this.configuration.getRunmode() != null ) {
+ args.add("-Dsling.run.modes=" + this.configuration.getRunmode());
+ }
+
+ builder.command(args.toArray(new String[args.size()]));
+ builder.directory(this.configuration.getFolder());
+ builder.redirectErrorStream(true);
+// builder.redirectOutput(Redirect.INHERIT);
+// builder.redirectError(Redirect.INHERIT);
+
+ logger.info("Starting Launchpad " + this.configuration.getId() + "...");
+ logger.debug("Launchpad cmd: " + builder.command());
+ logger.debug("Launchpad dir: " + builder.directory());
+
+ try {
+ cfg.setProcess(builder.start());
+ } catch (final IOException e) {
+ if (cfg.getProcess() != null) {
+ cfg.getProcess().destroy();
+ cfg.setProcess(null);
+ }
+ throw new Exception("Could not start the Launchpad", e);
+ }
+
+ return cfg;
+ }
+
+ public static void stop(final Log LOG, final ProcessDescription cfg) throws Exception {
+ boolean isNew = false;
+
+ if (cfg.getProcess() != null || isNew ) {
+ LOG.info("Stopping Launchpad " + cfg.getId());
+ boolean destroy = true;
+ final int twoMinutes = 2 * 60 * 1000;
+ final File controlPortFile = getControlPortFile(cfg.getDirectory());
+ LOG.debug("Control port file " + controlPortFile + " exists: " + controlPortFile.exists());
+ if ( controlPortFile.exists() ) {
+ // reading control port
+ int controlPort = -1;
+ String secretKey = null;
+ LineNumberReader lnr = null;
+ String serverName = null;
+ try {
+ lnr = new LineNumberReader(new FileReader(controlPortFile));
+ final String portLine = lnr.readLine();
+ final int pos = portLine.indexOf(':');
+ controlPort = Integer.parseInt(portLine.substring(pos + 1));
+ if ( pos > 0 ) {
+ serverName = portLine.substring(0, pos);
+ }
+ secretKey = lnr.readLine();
+ } catch ( final NumberFormatException ignore) {
+ // we ignore this
+ LOG.debug("Error reading control port file " + controlPortFile, ignore);
+ } catch ( final IOException ignore) {
+ // we ignore this
+ LOG.debug("Error reading control port file " + controlPortFile, ignore);
+ } finally {
+ IOUtils.closeQuietly(lnr);
+ }
+
+ if ( controlPort != -1 ) {
+ final List<String> hosts = new ArrayList<String>();
+ if ( serverName != null ) {
+ hosts.add(serverName);
+ }
+ hosts.add("localhost");
+ hosts.add("127.0.0.1");
+ LOG.debug("Found control port " + controlPort);
+ int index = 0;
+ while ( destroy && index < hosts.size() ) {
+ final String hostName = hosts.get(index);
+
+ Socket clientSocket = null;
+ DataOutputStream out = null;
+ BufferedReader in = null;
+ try {
+ LOG.debug("Trying to connect to " + hostName + ":" + controlPort);
+ clientSocket = new Socket();
+ // set a socket timeout
+ clientSocket.connect(new InetSocketAddress(hostName, controlPort), twoMinutes);
+ // without that, read() call on the InputStream associated with this Socket is infinite
+ clientSocket.setSoTimeout(twoMinutes);
+
+ LOG.debug(hostName + ":" + controlPort + " connection estabilished, sending the 'stop' command...");
+
+ out = new DataOutputStream(clientSocket.getOutputStream());
+ in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
+ if (secretKey != null) {
+ out.writeBytes(secretKey);
+ out.write(' ');
+ }
+ out.writeBytes("stop\n");
+ in.readLine();
+ destroy = false;
+ LOG.debug("'stop' command sent to " + hostName + ":" + controlPort);
+ } catch (final Throwable ignore) {
+ // catch Throwable because InetSocketAddress and Socket#connect throws unchecked exceptions
+ // we ignore this for now
+ LOG.debug("Error sending 'stop' command to " + hostName + ":" + controlPort + " due to: " + ignore.getMessage());
+ } finally {
+ IOUtils.closeQuietly(in);
+ IOUtils.closeQuietly(out);
+ IOUtils.closeQuietly(clientSocket);
+ }
+ index++;
+ }
+ }
+ }
+ if ( cfg.getProcess() != null ) {
+ final Process process = cfg.getProcess();
+
+ if (!destroy) {
+ // as shutdown might block forever, we use a timeout
+ final long now = System.currentTimeMillis();
+ final long end = now + twoMinutes;
+
+ LOG.debug("Waiting for process to stop...");
+
+ while (isAlive(process) && (System.currentTimeMillis() < end)) {
+ try {
+ Thread.sleep(2500);
+ } catch (InterruptedException e) {
+ // ignore
+ }
+ }
+ if (isAlive( process)) {
+ LOG.debug("Process timeout out after 2 minutes");
+ destroy = true;
+ } else {
+ LOG.debug("Process stopped");
+ }
+ }
+
+ if (destroy) {
+ LOG.debug("Destroying process...");
+ process.destroy();
+ LOG.debug("Process destroyed");
+ }
+
+ cfg.setProcess(null);
+ }
+ } else {
+ LOG.warn("Launchpad already stopped");
+ }
+ }
+
+ private static boolean isAlive(Process process) {
+ try {
+ process.exitValue();
+ return false;
+ } catch (IllegalThreadStateException e) {
+ return true;
+ }
+ }
+
+ private static File getControlPortFile(final File directory) {
+ final File launchpadDir = new File(directory, LaunchpadEnvironment.WORK_DIR_NAME);
+ final File confDir = new File(launchpadDir, "conf");
+ final File controlPortFile = new File(confDir, "controlport");
+ return controlPortFile;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/maven/slingstart/run/LaunchpadEnvironment.java b/src/main/java/org/apache/sling/maven/slingstart/run/LaunchpadEnvironment.java
new file mode 100644
index 0000000..cca6ac4
--- /dev/null
+++ b/src/main/java/org/apache/sling/maven/slingstart/run/LaunchpadEnvironment.java
@@ -0,0 +1,140 @@
+/*
+ * 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.InputStream;
+
+import org.codehaus.plexus.util.FileUtils;
+
+/**
+ * Common settings for all launchpad instances.
+ */
+public class LaunchpadEnvironment {
+
+ /** The work directory created by starting launchpad. */
+ public static final String WORK_DIR_NAME = "sling";
+
+ private final File launchpadJar;
+ private final boolean cleanWorkingDirectory;
+ private final boolean shutdownOnExit;
+ private final int readyTimeOutSec;
+
+ public LaunchpadEnvironment(final File launchpadJar,
+ final boolean cleanWorkingDirectory,
+ final boolean shutdownOnExit,
+ final int readyTimeOutSec) {
+ this.launchpadJar = launchpadJar;
+ this.cleanWorkingDirectory = cleanWorkingDirectory;
+ this.shutdownOnExit = shutdownOnExit;
+ this.readyTimeOutSec = readyTimeOutSec;
+ }
+
+ public boolean isShutdownOnExit() {
+ return this.shutdownOnExit;
+ }
+
+ public int getReadyTimeOutSec() {
+ return this.readyTimeOutSec;
+ }
+
+ /**
+ * Check if the launchpad folder exists.
+ */
+ private void ensureFolderExists(final File folder) {
+ if (!folder.exists()) {
+ folder.mkdirs();
+ }
+ if (this.cleanWorkingDirectory) {
+ final File work = new File(folder, WORK_DIR_NAME);
+ org.apache.commons.io.FileUtils.deleteQuietly(work);
+ }
+ }
+
+ private File installLaunchpad(final File folder) throws IOException {
+ if (this.launchpadJar.getParentFile().getAbsolutePath().equals(folder.getAbsolutePath())) {
+ return this.launchpadJar;
+ }
+ try {
+ FileUtils.copyFileToDirectory(this.launchpadJar, folder);
+ return new File(folder, this.launchpadJar.getName());
+ } catch (final IOException ioe) {
+ throw new IOException("Unable to copy " + this.launchpadJar + " to " + folder, ioe);
+ }
+ }
+
+ private void installLauncher(final File folder) throws IOException {
+ final File binDir = new File(folder, "bin");
+ copyResource("org/apache/sling/maven/slingstart/launcher/Launcher.class", binDir);
+ copyResource("org/apache/sling/maven/slingstart/launcher/LauncherMBean.class", binDir);
+ copyResource("org/apache/sling/maven/slingstart/launcher/Main.class", binDir);
+ }
+
+ /**
+ * Prepare a new instance.
+ * @param folder The target folder for the instance
+ * @return The launchpad jar
+ * @throws IOException if an error occurs.
+ */
+ public File prepare(final File folder) throws IOException {
+ this.ensureFolderExists(folder);
+
+ // copy launchpadJar
+ final File launchpad = this.installLaunchpad(folder);
+
+ // install launcher
+ this.installLauncher(folder);
+
+ return launchpad;
+ }
+
+ private void copyResource(final String resource,
+ final File dir)
+ throws IOException {
+ final int lastSlash = resource.lastIndexOf('/');
+ final File baseDir;
+ if ( lastSlash > 0 ) {
+ final String filePath = resource.substring(0, lastSlash).replace('/', File.separatorChar);
+ baseDir = new File(dir, filePath);
+ } else {
+ baseDir = dir;
+ }
+ baseDir.mkdirs();
+ final File file = new File(baseDir, resource.substring(lastSlash + 1));
+ final InputStream is = LaunchpadEnvironment.class.getClassLoader().getResourceAsStream(resource);
+ if ( is == null ) {
+ throw new IOException("Resource not found: " + resource);
+ }
+ final FileOutputStream fos = new FileOutputStream(file);
+ final byte[] buffer = new byte[2048];
+ int l;
+ try {
+ while ( (l = is.read(buffer)) > 0 ) {
+ fos.write(buffer, 0, l);
+ }
+ } finally {
+ if ( fos != null ) {
+ fos.close();
+ }
+ if ( is != null ) {
+ is.close();
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/apache/sling/maven/slingstart/run/PortHelper.java b/src/main/java/org/apache/sling/maven/slingstart/run/PortHelper.java
new file mode 100644
index 0000000..d6aa33d
--- /dev/null
+++ b/src/main/java/org/apache/sling/maven/slingstart/run/PortHelper.java
@@ -0,0 +1,49 @@
+/*
+ * 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.IOException;
+import java.net.ServerSocket;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.maven.plugin.MojoExecutionException;
+
+/**
+ * Simple helper class to find a new port.
+ */
+public class PortHelper {
+
+ private static final Set<Integer> USED_PORTS = new HashSet<Integer>();
+
+ public static synchronized int getNextAvailablePort()
+ throws MojoExecutionException {
+ int unusedPort = 0;
+ do {
+ try {
+ final ServerSocket socket = new ServerSocket( 0 );
+ unusedPort = socket.getLocalPort();
+ socket.close();
+ } catch ( final IOException e ) {
+ throw new MojoExecutionException( "Error getting an available port from system", e );
+ }
+ } while ( USED_PORTS.contains(unusedPort));
+ USED_PORTS.add(unusedPort);
+
+ return unusedPort;
+ }
+}
diff --git a/src/main/java/org/apache/sling/maven/slingstart/run/ProcessDescription.java b/src/main/java/org/apache/sling/maven/slingstart/run/ProcessDescription.java
new file mode 100644
index 0000000..4aff1c4
--- /dev/null
+++ b/src/main/java/org/apache/sling/maven/slingstart/run/ProcessDescription.java
@@ -0,0 +1,81 @@
+/*
+ * 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 org.apache.maven.plugin.MojoExecutionException;
+
+/**
+ * A running launchpad process.
+ */
+public class ProcessDescription {
+
+ private final String id;
+ private final File directory;
+ private final ControlListener listener;
+ private volatile Process process;
+
+ public ProcessDescription(final String id, final File directory) throws MojoExecutionException {
+ this.id = id;
+ this.directory = directory;
+ this.listener = new ControlListener(PortHelper.getNextAvailablePort());
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public File getDirectory() {
+ return directory;
+ }
+
+ public ControlListener getControlListener() {
+ return this.listener;
+ }
+
+ public Process getProcess() {
+ return process;
+ }
+
+ public void setProcess(final Process process) {
+ this.process = process;
+ }
+
+ /**
+ * Install a shutdown hook
+ */
+ public void installShutdownHook() {
+ final ProcessDescription cfg = this;
+ Runtime.getRuntime().addShutdownHook(new Thread() {
+ @Override
+ public void run() {
+ if ( cfg.getProcess() != null ) {
+ System.out.println("Terminating launchpad " + cfg.getId());
+ cfg.getProcess().destroy();
+ cfg.setProcess(null);
+ }
+ }
+ });
+ }
+
+ @Override
+ public String toString() {
+ return "RunningProcessDescription [id=" + id + ", directory="
+ + directory + ", process=" + process + "]";
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/maven/slingstart/run/ProcessDescriptionProvider.java b/src/main/java/org/apache/sling/maven/slingstart/run/ProcessDescriptionProvider.java
new file mode 100644
index 0000000..7b28219
--- /dev/null
+++ b/src/main/java/org/apache/sling/maven/slingstart/run/ProcessDescriptionProvider.java
@@ -0,0 +1,106 @@
+/*
+ * 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.util.HashMap;
+import java.util.Map;
+
+/**
+ * A singleton which is responsible to provide {@link ProcessDescription}s
+ */
+public class ProcessDescriptionProvider {
+
+ private static final String DEFAULT_KEY = "DEFAULT_LAUNCHPAD";
+
+ private static ProcessDescriptionProvider ourInstance = new ProcessDescriptionProvider();
+ private final Map<String, ProcessDescription> configs = new HashMap<String, ProcessDescription>();
+ private final Map<String, String> lockedIds = new HashMap<String, String>();
+
+ private ProcessDescriptionProvider() {
+ // private constructor
+ }
+
+ public static ProcessDescriptionProvider getInstance() {
+ return ourInstance;
+ }
+
+ /**
+ * Prepare an ID for a launchpad that will be started, before saving the config.
+ * @param launchpadId the id of the launchpad to lock
+ * @return id key used to add to configs
+ */
+ public synchronized String getId(final String launchpadId) throws Exception {
+ final String id = (launchpadId == null ? DEFAULT_KEY : launchpadId);
+ if (configs.containsKey(id) || lockedIds.containsKey(id)) {
+ throw new Exception("Launchpad Id " + id + " is already in use");
+ }
+
+ String ts = String.valueOf(System.currentTimeMillis());
+ lockedIds.put(id, ts);
+ return ts;
+ }
+
+ /**
+ *
+ * @param launchpadId
+ * @param unlockKey
+ * @return
+ */
+ public synchronized boolean cancelId(final String launchpadId, final String unlockKey) {
+ final String id = (launchpadId == null ? DEFAULT_KEY : launchpadId);
+ if (lockedIds.containsKey(id) && lockedIds.get(id).equals(unlockKey)) {
+ lockedIds.remove(id);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ *
+ * @param launchpadId
+ * @return
+ */
+ public synchronized ProcessDescription getRunConfiguration(final String launchpadId) {
+ final String id = (launchpadId == null ? DEFAULT_KEY : launchpadId);
+ return configs.get(id);
+ }
+
+ /**
+ *
+ * @param launchpadId
+ * @return
+ */
+ public synchronized boolean isRunConfigurationAvailable(final String launchpadId) {
+ return getRunConfiguration(launchpadId) == null && !lockedIds.containsKey(launchpadId);
+ }
+
+ public synchronized void addRunConfiguration(ProcessDescription cfg, final String unlockKey) throws Exception {
+ String id = cfg.getId() == null ? DEFAULT_KEY : cfg.getId();
+ if (!lockedIds.containsKey(id) || !lockedIds.get(id).equals(unlockKey)) {
+ throw new Exception("Cannot add configuration. Id " + id + " doesn't exist");
+ }
+ lockedIds.remove(cfg.getId());
+ configs.put(cfg.getId(), cfg);
+ }
+
+ public synchronized void removeRunConfiguration(final String launchpadId) {
+ final String id = (launchpadId == null ? DEFAULT_KEY : launchpadId);
+ configs.remove(id);
+ }
+
+}
diff --git a/src/main/java/org/apache/sling/maven/slingstart/run/ServerConfiguration.java b/src/main/java/org/apache/sling/maven/slingstart/run/ServerConfiguration.java
new file mode 100644
index 0000000..7b8ccd8
--- /dev/null
+++ b/src/main/java/org/apache/sling/maven/slingstart/run/ServerConfiguration.java
@@ -0,0 +1,157 @@
+/*
+ * 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.Serializable;
+
+/**
+ * A server configuration
+ */
+public class ServerConfiguration implements Serializable {
+
+ private static final long serialVersionUID = 1922175510880318125L;
+
+ private static final String DEFAULT_VM_OPTS = "-Xmx1024m -XX:MaxPermSize=256m -Djava.awt.headless=true";
+
+ /** The unique id. */
+ private String id;
+
+ /** The run mode string. */
+ private String runmode;
+
+ /** The port to use. */
+ private String port;
+
+ /** The context path. */
+ private String contextPath;
+
+ /** The vm options. */
+ private String vmOpts = DEFAULT_VM_OPTS;
+
+ /** Additional application options. */
+ private String opts;
+
+ /** Number of instances. */
+ private int instances = 1;
+
+ /** The folder to use. */
+ private File folder;
+
+ /**
+ * Get the instance id
+ * @return The instance id
+ */
+ public String getId() {
+ return id;
+ }
+
+ /**
+ * Set the instance id
+ * @param id New instance id
+ */
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getRunmode() {
+ return runmode;
+ }
+
+ public void setRunmode(final String runmode) {
+ this.runmode = runmode;
+ }
+
+ public String getPort() {
+ return port;
+ }
+
+ public void setPort(final String port) {
+ this.port = port;
+ }
+
+ public String getContextPath() {
+ return contextPath;
+ }
+
+ public void setContextPath(final String contextPath) {
+ this.contextPath = contextPath;
+ }
+
+ public String getVmOpts() {
+ return vmOpts;
+ }
+
+ public void setVmOpts(final String vmOpts) {
+ this.vmOpts = vmOpts;
+ }
+
+ public String getOpts() {
+ return opts;
+ }
+
+ public void setOpts(final String opts) {
+ this.opts = opts;
+ }
+
+ public int getInstances() {
+ return this.instances;
+ }
+
+ public void setInstances(final int value) {
+ this.instances = value;
+ }
+
+ public File getFolder() {
+ return folder;
+ }
+
+ public void setFolder(final File folder) {
+ this.folder = folder.getAbsoluteFile();
+ }
+
+ /**
+ * Get the server
+ * @return The server
+ */
+ public String getServer() {
+ // hard coded for now
+ return "localhost";
+ }
+
+ public ServerConfiguration copy() {
+ final ServerConfiguration copy = new ServerConfiguration();
+ // we do not copy the id
+ copy.setRunmode(this.getRunmode());
+ copy.setPort(this.getPort());
+ copy.setContextPath(this.getContextPath());
+ copy.setVmOpts(this.getVmOpts());
+ copy.setOpts(this.getOpts());
+ copy.setInstances(1);
+ copy.setFolder(this.getFolder());
+
+ return copy;
+ }
+
+ @Override
+ public String toString() {
+ return "LaunchpadConfiguration [id=" + id + ", runmode=" + runmode
+ + ", port=" + port + ", contextPath=" + contextPath
+ + ", vmOpts=" + vmOpts + ", opts=" + opts + ", instances="
+ + instances + ", folder=" + folder + "]";
+ }
+}
diff --git a/src/main/java/org/apache/sling/maven/slingstart/run/StartMojo.java b/src/main/java/org/apache/sling/maven/slingstart/run/StartMojo.java
new file mode 100644
index 0000000..e496d58
--- /dev/null
+++ b/src/main/java/org/apache/sling/maven/slingstart/run/StartMojo.java
@@ -0,0 +1,444 @@
+/*
+ * 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.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.sling.maven.slingstart.BuildConstants;
+
+/**
+ * Mojo for starting launchpad.
+ */
+@Mojo(
+ name = "start",
+ defaultPhase = LifecyclePhase.PRE_INTEGRATION_TEST,
+ threadSafe = true
+ )
+public class StartMojo extends AbstractMojo {
+
+ /**
+ * Set this to "true" to skip starting the launchpad
+ *
+ */
+ @Parameter(property = "launchpad.skip", defaultValue = "false")
+ protected boolean skipLaunchpad;
+
+ /**
+ * Parameter containing the list of server configurations
+ */
+ @Parameter
+ private List<ServerConfiguration> servers;
+
+ /**
+ * 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.
+ */
+ @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 system properties file will contain all started instances with their ports etc.
+ */
+ @Parameter(defaultValue = "${project.build.directory}/launchpad-runner.properties")
+ protected File systemPropertiesFile;
+
+ /**
+ * 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;
+ }
+
+ /**
+ * @see org.apache.maven.plugin.Mojo#execute()
+ */
+ @Override
+ public void execute() throws MojoExecutionException, MojoFailureException {
+ if (this.skipLaunchpad) {
+ this.getLog().info("Executing of the start launchpad mojo is disabled by configuration.");
+ return;
+ }
+
+ // 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);
+
+ // 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;
+ }
+ }
+ }
+ }
+
+ /**
+ * 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()));
+ }
+
+ // 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) {
+ return launchpadJar;
+ }
+
+ // If a launchpad dependency is configured, resolve it
+ if (launchpadDependency != null) {
+ return getArtifact(launchpadDependency).getFile();
+ }
+
+ // If the current project is a slingstart project, use its JAR artifact
+ if (this.project.getPackaging().equals(BuildConstants.PACKAGING_SLINGSTART)) {
+ final File jarFile = new File(this.project.getBuild().getDirectory(), this.project.getBuild().getFinalName() + ".jar");
+ if (jarFile.exists()) {
+ return jarFile;
+ }
+ }
+
+ // 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);
+ 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;
+ }
+}
diff --git a/src/main/java/org/apache/sling/maven/slingstart/run/StopMojo.java b/src/main/java/org/apache/sling/maven/slingstart/run/StopMojo.java
new file mode 100644
index 0000000..0c58f2a
--- /dev/null
+++ b/src/main/java/org/apache/sling/maven/slingstart/run/StopMojo.java
@@ -0,0 +1,89 @@
+/*
+ * 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.FileReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Properties;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugins.annotations.LifecyclePhase;
+import org.apache.maven.plugins.annotations.Mojo;
+
+/**
+ * Stops the running launchpad instances.
+ *
+ */
+@Mojo(
+ name = "stop",
+ defaultPhase = LifecyclePhase.POST_INTEGRATION_TEST,
+ threadSafe = true
+)
+public class StopMojo extends StartMojo {
+
+ @Override
+ public void execute() throws MojoExecutionException {
+ if (this.skipLaunchpad) {
+ this.getLog().info("Executing of the stop-multiple launchpad mojo is disabled by configuration.");
+ return;
+ }
+ // read configurations
+ final Properties launchpadConfigProps = new Properties();
+ Reader reader = null;
+ try {
+ reader = new FileReader(this.systemPropertiesFile);
+ launchpadConfigProps.load(reader);
+ } catch ( final IOException ioe) {
+ throw new MojoExecutionException("Unable to read launchpad runner configuration properties.", ioe);
+ } finally {
+ IOUtils.closeQuietly(reader);
+ }
+
+ final int instances = Integer.valueOf(launchpadConfigProps.getProperty("launchpad.instances"));
+ final List<ProcessDescription> configurations = new ArrayList<ProcessDescription>();
+ for(int i=1;i<=instances;i++) {
+ final String id = launchpadConfigProps.getProperty("launchpad.instance.id." + String.valueOf(i));
+
+ final ProcessDescription config = ProcessDescriptionProvider.getInstance().getRunConfiguration(id);
+ if ( config == null ) {
+ getLog().warn("No launchpad configuration found for instance " + id);
+ } else {
+ configurations.add(config);
+ }
+ }
+
+ if (configurations.size() > 0) {
+ getLog().info(new StringBuilder("Stopping ").append(configurations.size()).append(" Launchpad instances").toString());
+
+ for (final ProcessDescription cfg : configurations) {
+
+ try {
+ LauncherCallable.stop(this.getLog(), cfg);
+ ProcessDescriptionProvider.getInstance().removeRunConfiguration(cfg.getId());
+ } catch (Exception e) {
+ throw new MojoExecutionException("Could not stop launchpad " + cfg.getId(), e);
+ }
+ }
+ } else {
+ getLog().warn("No stored configuration file was found at " + this.systemPropertiesFile + " - no Launchapd will be stopped");
+ }
+ }
+}
--
To stop receiving notification emails like this one, please contact
"commits@sling.apache.org" <co...@sling.apache.org>.