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>.