You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by an...@apache.org on 2020/02/13 16:29:36 UTC

[sling-org-apache-sling-kickstart] 01/01: Renamed Quickstart to Kickstart

This is an automated email from the ASF dual-hosted git repository.

andysch pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-kickstart.git

commit f38674383ea57c1b585beb22f011779774d0ca45
Author: Andreas Schaefer <sc...@iMac.local>
AuthorDate: Thu Feb 6 13:55:37 2020 -0800

    Renamed Quickstart to Kickstart
---
 Readme.md                                          |   27 +
 pom.xml                                            |  308 ++++++
 .../apache/sling/kickstart/app/SlingStarter.java   |  322 ++++++
 .../sling/kickstart/control/ControlAction.java     |   53 +
 .../sling/kickstart/control/ControlListener.java   |  628 +++++++++++
 .../sling/kickstart/control/ControlTarget.java     |   32 +
 .../org.apache.sling.feature.launcher.spi.Launcher |    1 +
 ...eature.launcher.spi.extensions.ExtensionHandler |    2 +
 src/main/resources/feature-sling12.json            | 1139 ++++++++++++++++++++
 .../feature/starter/it/LaunchpadReadyRule.java     |  121 +++
 .../apache/sling/feature/starter/it/SmokeIT.java   |  195 ++++
 .../sling/feature/starter/it/package-info.java     |   30 +
 12 files changed, 2858 insertions(+)

diff --git a/Readme.md b/Readme.md
new file mode 100644
index 0000000..2fc61a3
--- /dev/null
+++ b/Readme.md
@@ -0,0 +1,27 @@
+# Sling Feature Starter
+
+This project is the Feature Model based version of the **sling-org-apache-sling-starter**
+module and creates an executable JAR file for now.
+It is also a test case for the Slingstart Feature Maven Plugin as it uses it
+to launch a Launchpad Ready Rule and Smoke tests.
+
+## Build
+
+This plugin depends on the **Sling Start Feature Maven Plugin** (also in the Sling
+Whiteboard) which is then used to run the IT tests:
+
+1. Go to **sling-slingstart-feature-maven-plugin** module in Sling Whiteboard
+2. Build with: `mvn clean install`
+3. Go back to **sling-org-apache-sling-feature-starter**
+4. Build and Launch it with: `mvn clean install`
+5. Sling will come up and run the IT tests and then shut down. Sling can be
+   kept running after the end of the IT tests by providing the property
+   **block.sling.at.the.end** with the value **true**
+
+## Usage
+
+After the resulting jar file **org.apache.sling.feature.starter-<version>.jar**
+can be executed with:
+```
+java -jar org.apache.sling.feature.starter-<version>.jar ...
+```
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..fc5d676
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,308 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<!--
+    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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.sling</groupId>
+        <artifactId>sling</artifactId>
+        <version>35</version>
+        <relativePath />
+    </parent>
+
+    <artifactId>org.apache.sling.kickstart</artifactId>
+    <version>0.0.1-SNAPSHOT</version>
+    <packaging>slingkickstart</packaging>
+
+    <name>Apache Sling Kickstart Launcher</name>
+    <description>
+        An Executable JAR file to launch Sling
+    </description>
+
+    <!--    <scm>-->
+    <!--        <connection>scm:git:https://gitbox.apache.org/repos/asf/sling-org-apache-sling-feature-starter.git</connection>-->
+    <!--        <developerConnection>scm:git:https://gitbox.apache.org/repos/asf/sling-org-apache-sling-feature-starter.git</developerConnection>-->
+    <!--        <url>https://gitbox.apache.org/repos/asf?p=sling-org-apache-sling-feature-starter.git</url>-->
+    <!--        <tag>HEAD</tag>-->
+    <!--    </scm>-->
+
+    <properties>
+        <sling.java.version>8</sling.java.version>
+        <picocli.version>3.6.0</picocli.version>
+        <appassembler-maven-plugin.version>2.0.0</appassembler-maven-plugin.version>
+        <org.apache.sling.feature.extension.content.version>1.0.4</org.apache.sling.feature.extension.content.version>
+        <org.apache.sling.feature.launcher.version>1.1.2</org.apache.sling.feature.launcher.version>
+        <org.apache.sling.feature.io.version>1.2.2</org.apache.sling.feature.io.version>
+        <org.apache.felix.converter.version>1.0.8</org.apache.felix.converter.version>
+
+        <sling.java.version>8</sling.java.version>
+        <IT.expected.bundles.count>126</IT.expected.bundles.count>
+
+        <block.sling.at.the.end>false</block.sling.at.the.end>
+    </properties>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>ianal-maven-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>verify-legal-files</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <artifactId>maven-clean-plugin</artifactId>
+                <configuration>
+                    <filesets>
+                        <fileset>
+                            <directory>${basedir}</directory>
+                            <includes>
+                                <include>sling/**</include>
+                                <include>coverage.ec</include>
+                            </includes>
+                        </fileset>
+                    </filesets>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-dependency-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>unpack-dependencies</id>
+                        <phase>prepare-package</phase>
+                        <goals>
+                            <goal>unpack-dependencies</goal>
+                        </goals>
+                        <configuration>
+                            <excludes>META-INF/**</excludes>
+                            <outputDirectory>${project.build.directory}/classes</outputDirectory>
+                            <overWriteReleases>false</overWriteReleases>
+                            <overWriteSnapshots>true</overWriteSnapshots>
+                            <includeArtifactIds>
+                                commons-io,
+                                org.apache.sling.feature.extension.content,
+                                org.apache.sling.feature.launcher,
+                                osgi.core,commons-lang,
+                                org.apache.sling.feature,
+                                org.apache.sling.feature.io,
+                                org.apache.felix.converter,
+                                picocli,
+                                slf4j-api,
+                                slf4j-simple
+                            </includeArtifactIds>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <configuration>
+                    <archive>
+                        <manifest>
+                            <mainClass>org.apache.sling.kickstart.app.SlingStarter</mainClass>
+                        </manifest>
+                    </archive>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.rat</groupId>
+                <artifactId>apache-rat-plugin</artifactId>
+                <configuration>
+                    <excludes>
+                        <exclude>readme.md</exclude>
+                        <exclude>src/main/resources/META-INF/services/**</exclude>
+                        <exclude>**/*.properties</exclude>
+                        <exclude>launcher/**</exclude>
+                    </excludes>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>build-helper-maven-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>reserve-network-port</id>
+                        <goals>
+                            <!-- pre-integration-test is too late -->
+                            <goal>reserve-network-port</goal>
+                        </goals>
+                        <phase>process-resources</phase>
+                        <configuration>
+                            <portNames>
+                                <portName>http.port</portName>
+                                <portName>sling.control.port</portName>
+                            </portNames>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.sling</groupId>
+                <artifactId>sling-kickstart-maven-plugin</artifactId>
+                <version>0.0.1-SNAPSHOT</version>
+                <extensions>true</extensions>
+                <executions>
+                    <execution>
+                        <id>start-container-before-IT</id>
+                        <goals>
+                            <goal>start</goal>
+                        </goals>
+                    </execution>
+<!--                    <execution>-->
+<!--                        <id>stop-container-after-IT</id>-->
+<!--                        <goals>-->
+<!--                            <goal>stop</goal>-->
+<!--                        </goals>-->
+<!--                        <configuration>-->
+<!--                            &lt;!&ndash; Let the Test Server run to manually verify the setup. TODO: remove later &ndash;&gt;-->
+<!--                            <shouldBlockUntilKeyIsPressed>${block.sling.at.the.end}</shouldBlockUntilKeyIsPressed>-->
+<!--                        </configuration>-->
+<!--                    </execution>-->
+                </executions>
+                <configuration>
+                    <launchpadJar>${project.build.directory}/${project.artifactId}-${project.version}.jar</launchpadJar>
+                    <parallelExecution>false</parallelExecution>
+                    <servers>
+                        <server>
+                            <port>${http.port}</port>
+                            <controlPort>${sling.control.port}</controlPort>
+                            <debug>true</debug>
+                        </server>
+                    </servers>
+                </configuration>
+            </plugin>
+            <plugin>
+                <artifactId>maven-failsafe-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>integration-test</goal>
+                            <goal>verify</goal>
+                        </goals>
+                    </execution>
+                </executions>
+                <configuration>
+                    <systemPropertyVariables>
+                        <launchpad.http.port>${http.port}</launchpad.http.port>
+                        <IT.expected.bundles.count>${IT.expected.bundles.count}</IT.expected.bundles.count>
+                    </systemPropertyVariables>
+                </configuration>
+            </plugin>
+        </plugins>
+
+        <pluginManagement>
+            <plugins>
+                <plugin>
+                    <!-- Extend RAT configuration from parent pom -->
+                    <groupId>org.apache.rat</groupId>
+                    <artifactId>apache-rat-plugin</artifactId>
+                    <configuration>
+                        <excludes combine.children="append">
+                            <!-- Exclude sling instance -->
+                            <exclude>sling/**</exclude>
+                        </excludes>
+                    </configuration>
+                </plugin>
+            </plugins>
+        </pluginManagement>
+    </build>
+
+    <dependencies>
+        <!--
+         | CLI
+        -->
+        <dependency>
+            <groupId>info.picocli</groupId>
+            <artifactId>picocli</artifactId>
+            <version>${picocli.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>commons-lang</groupId>
+            <artifactId>commons-lang</artifactId>
+            <version>2.6</version>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>osgi.core</artifactId>
+            <version>7.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-simple</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.framework</artifactId>
+            <version>6.0.2</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.feature.extension.content</artifactId>
+            <version>${org.apache.sling.feature.extension.content.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.feature.launcher</artifactId>
+            <version>${org.apache.sling.feature.launcher.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.feature.io</artifactId>
+            <version>${org.apache.sling.feature.io.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.converter</artifactId>
+            <version>${org.apache.felix.converter.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>2.6</version>
+        </dependency>
+
+        <!-- Testing dependencies -->
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpclient</artifactId>
+            <version>4.5.10</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.utils</artifactId>
+            <version>1.11.2</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/src/main/java/org/apache/sling/kickstart/app/SlingStarter.java b/src/main/java/org/apache/sling/kickstart/app/SlingStarter.java
new file mode 100644
index 0000000..beda54f
--- /dev/null
+++ b/src/main/java/org/apache/sling/kickstart/app/SlingStarter.java
@@ -0,0 +1,322 @@
+/*
+ * 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.kickstart.app;
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.sling.feature.launcher.impl.Main;
+import org.apache.sling.kickstart.control.ControlAction;
+import org.apache.sling.kickstart.control.ControlListener;
+import org.apache.sling.kickstart.control.ControlTarget;
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Option;
+import picocli.CommandLine.Parameters;
+
+
+@Command(
+    name = "java -jar <Sling Feature Starter JAR File>",
+    description = "Apache Sling Feature Starter",
+    footer = "Copyright(c) 2019 The Apache Software Foundation."
+)
+public class SlingStarter implements Runnable, ControlTarget {
+
+    @Option(names = { "-s", "--mainFeature" }, description = "main feature file (file path or URL) replacing the provided Sling Feature File", required = false)
+    private String mainFeatureFile;
+
+    @Option(names = { "-af", "--additionalFeature" }, description = "additional feature files", required = false)
+    private List<String> additionalFeatureFile;
+
+    @Option(names = { "-j", "--control" }, description = "host and port to use for control connection in the format '[host:]port' (default 127.0.0.1:0)", required = false)
+    private String controlAddress;
+
+    @Option(names = { "-l", "--logLevel" }, description = "the initial loglevel (0..4, FATAL, ERROR, WARN, INFO, DEBUG)", required = false)
+    private String logLevel;
+
+    @Option(names = { "-f", "--logFile" }, description = "the log file, \"-\" for stdout (default logs/error.log)", required = false)
+    private String logFile;
+
+    @Option(names = { "-c", "--slingHome" }, description = "the sling context directory (default sling)", required = false)
+    private String slingHome;
+
+    //AS TODO: does this still apply here
+    @Option(names = { "-i", "--launcherHome" }, description = "the launcher home directory (default launcher)", required = false)
+    private String launcherHome;
+
+    @Option(names = { "-a", "--address" }, description = "the interface to bind to (use 0.0.0.0 for any)", required = false)
+    private String address;
+
+    @Option(names = { "-p", "--port" }, description = "the port to listen to (default 8080)", required = false)
+    private String port;
+
+    @Option(names = { "-r", "--context" }, description = "the root servlet context path for the http service (default is /)", required = false)
+    private String contextPath;
+
+    @Option(names = { "-n", "--noShutdownHook" }, description = "don't install the shutdown hook")
+    private boolean noShutdownHook;
+
+    @Option(names = { "-v", "--verbose" }, description = "the feature launcher is verbose on launch", required = false)
+    private boolean verbose;
+
+    @Option(names = {"-D", "--define"}, description = "sets property n to value v. Make sure to use this option *after* the jar filename. " +
+        "The JVM also has a -D option which has a different meaning", required = false)
+    private Map<String, String> properties = new HashMap<>();
+
+    @Option(names = { "-h", "--help" }, usageHelp = true, description = "Display the usage message.")
+    private boolean helpRequested;
+
+    @Parameters(paramLabel = "COMMAND", description = "Optional Command for Server Instance Interaction, can be one of: 'start', 'stop', 'status' or 'threads'", arity = "0..1")
+    private String command;
+
+    // The name of the environment variable to consult to find out
+    // about sling.home
+    private static final String ENV_SLING_HOME = "SLING_HOME";
+
+    /**
+     * The name of the configuration property indicating the socket to use for
+     * the control connection. The value of this property is either just a port
+     * number (in which case the host is assumed to be <code>localhost</code>)
+     * or a host name (or IP address) and port number separated by a colon.
+     */
+    protected static final String PROP_CONTROL_SOCKET = "sling.control.socket";
+
+    /** The Sling configuration property name setting the initial log level */
+    private static final String PROP_LOG_LEVEL = "org.apache.sling.commons.log.level";
+
+    /** The Sling configuration property name setting the initial log file */
+    private static final String PROP_LOG_FILE = "org.apache.sling.commons.log.file";
+
+    /**
+     * The configuration property setting the port on which the HTTP service
+     * listens
+     */
+    private static final String PROP_PORT = "org.osgi.service.http.port";
+
+    /**
+     * The configuration property setting the context path where the HTTP service
+     * mounts itself.
+     */
+    private static final String PROP_CONTEXT_PATH = "org.apache.felix.http.context_path";
+
+    /**
+     * Host name or IP Address of the interface to listen on.
+     */
+    private static final String PROP_HOST = "org.apache.felix.http.host";
+
+    /**
+     * Name of the configuration property (or system property) indicating
+     * whether the shutdown hook should be installed or not. If this property is
+     * not set or set to {@code true} (case insensitive), the shutdown hook
+     * properly shutting down the framework is installed on startup. Otherwise,
+     * if this property is set to any value other than {@code true} (case
+     * insensitive) the shutdown hook is not installed.
+     * <p>
+     * The respective command line option is {@code -n}.
+     */
+    private static final String PROP_SHUTDOWN_HOOK = "sling.shutdown.hook";
+
+    private boolean started = false;
+
+    @Override
+    public void run() {
+        try {
+            URL mainFeatureURL = checkFeatureFile(mainFeatureFile);
+            if(mainFeatureURL == null) {
+                mainFeatureURL = getClass().getResource("/feature-sling12.json");
+            }
+            List<String> argumentList = new ArrayList<>();
+            argumentList.add("-f");
+            argumentList.add(mainFeatureURL.toString());
+            if(additionalFeatureFile != null) {
+                for (String additional : additionalFeatureFile) {
+                    URL additionalURL = checkFeatureFile(additional);
+                    if (additionalURL != null) {
+                        argumentList.add("-f");
+                        argumentList.add(additionalURL.toString());
+                    }
+                }
+            }
+            if(StringUtils.isNotEmpty(logLevel)) {
+                addArgument(argumentList, PROP_LOG_LEVEL, logLevel);
+            }
+            if(StringUtils.isNotEmpty(logFile)) {
+                addArgument(argumentList, PROP_LOG_FILE, logFile);
+            }
+            if(StringUtils.isNotEmpty(port)) {
+                addArgument(argumentList, PROP_PORT, port);
+            }
+            if(StringUtils.isNotEmpty(address)) {
+                addArgument(argumentList, PROP_HOST, address);
+            }
+            if(StringUtils.isNotEmpty(contextPath)) {
+                addArgument(argumentList, PROP_CONTEXT_PATH, contextPath);
+            }
+            if(verbose) {
+                argumentList.add("-v");
+            }
+            System.out.println("Before Launching Feature Launcher, arguments: " + argumentList);
+            // Now we have to handle any Start Option
+            ControlAction controlAction = getControlAction(command);
+            int answer = doControlAction(controlAction, controlAddress);
+            if (answer >= 0) {
+                doTerminateVM(answer);
+                return;
+            }
+
+            // finally start Sling
+            if (!doStart(argumentList)) {
+                error("Failed to start Sling; terminating", null);
+                doTerminateVM(1);
+                return;
+            }
+        } catch(Throwable t) {
+            System.out.println("Caught an Exception: " + t.getLocalizedMessage());
+            t.printStackTrace();
+        }
+    }
+
+    private void addArgument(List<String> list, String key, String value) {
+        list.add("-D");
+        list.add(key + "=" + value);
+    }
+
+    private URL checkFeatureFile(String featureFile) {
+        URL answer = null;
+        if(featureFile != null && !featureFile.isEmpty()) {
+            try {
+                URL check = new URL(featureFile);
+                check.toURI();
+                answer = check;
+            } catch (MalformedURLException | URISyntaxException e) {
+                // Try it as a file
+                File check = new File(featureFile);
+                if (!check.exists() || !check.canRead()) {
+                    throw new RuntimeException("Given Feature File is not a valid URL or File: '" + featureFile + "'", e);
+                }
+                try {
+                    answer = check.toURI().toURL();
+                } catch (MalformedURLException ex) {
+                    throw new RuntimeException("Given Feature File cannot be converted to an URL: '" + featureFile + "'", e);
+                }
+            }
+        }
+        return answer;
+    }
+
+    public static void main(String[] args) {
+        CommandLine.run(new SlingStarter(), args);
+    }
+
+    private int doControlAction(ControlAction controlAction, String controlAddress) {
+        final ControlListener sl = new ControlListener(
+            this,
+            controlAddress
+        );
+        switch (controlAction) {
+            case FOREGROUND:
+                if (!sl.listen()) {
+                    return -1;
+                }
+                break;
+            case START:
+                if (!sl.listen()) {
+                    // assume service already running
+                    return 0;
+                }
+                break;
+            case STOP:
+                return sl.shutdownServer();
+            case STATUS:
+                return sl.statusServer();
+            case THREADS:
+                return sl.dumpThreads();
+        }
+        return -1;
+    }
+
+    private boolean doStart(List<String> argumentList) {
+        // prevent duplicate start
+        if ( this.started) {
+            info("Apache Sling has already been started", new Exception("Where did this come from"));
+            return true;
+        }
+
+        info("Starting Apache Sling in " + slingHome, null);
+        this.started = true;
+        System.out.println("Start Command: '" + command + "'");
+        try {
+            Main.main(argumentList.toArray(new String[]{}));
+        } catch(Error | RuntimeException e) {
+            error("Launching Sling Feature failed", e);
+            return false;
+        }
+        return true;
+    }
+
+    private ControlAction getControlAction(String command) {
+        ControlAction answer = ControlAction.FOREGROUND;
+        try {
+            answer = ControlAction.valueOf(command.toUpperCase());
+        } catch (IllegalArgumentException e) {
+            throw new RuntimeException("Given Control Action is not valid: '" + command.toUpperCase() + "'");
+        } catch (NullPointerException e) {
+            // Ignore as we set the default to FOREGROUND anyhow
+        }
+        return answer;
+    }
+
+    @Override
+    public String getHome() {
+        return slingHome;
+    }
+
+    @Override
+    public void doStop() {
+        info("Stop Application", null);
+        System.exit(0);
+    }
+
+    @Override
+    public void doTerminateVM(int status) {
+        info("Terminate VM, status: " + status, null);
+        System.exit(status);
+    }
+
+    @Override
+    public void info(String message, Throwable t) {
+        System.out.println(message);
+        if(t != null) {
+            t.printStackTrace();
+        }
+    }
+
+    @Override
+    public void error(String message, Throwable t) {
+        System.err.println(message);
+        if(t != null) {
+            t.printStackTrace(System.err);
+        }
+    }
+}
diff --git a/src/main/java/org/apache/sling/kickstart/control/ControlAction.java b/src/main/java/org/apache/sling/kickstart/control/ControlAction.java
new file mode 100644
index 0000000..feea4ca
--- /dev/null
+++ b/src/main/java/org/apache/sling/kickstart/control/ControlAction.java
@@ -0,0 +1,53 @@
+/*
+ * 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.kickstart.control;
+
+/**
+ * The <code>ControlAction</code> defines values to used as the action for the
+ * Feature Launcher control with the {@link Main#doControlAction()} method.
+ */
+public enum ControlAction {
+
+    /**
+     * Indicates the Feature Launcher application should be started and a listener should
+     * be installed to accept control commands.
+     */
+    START,
+
+    /**
+     * Indicates to connect to a running Feature Launcher application having installed a
+     * listener and send that application the command to shutdown.
+     */
+    STOP,
+
+    /**
+     * Indicates to connect to a running Feature Launcher application having installed a
+     * listener and ask that application about its state.
+     */
+    STATUS,
+
+    FOREGROUND,
+
+    /**
+     * Indicates to connect to a running Feature Launcher application having installed a
+     * listener and ask for a thread dump.
+     */
+    THREADS;
+
+}
diff --git a/src/main/java/org/apache/sling/kickstart/control/ControlListener.java b/src/main/java/org/apache/sling/kickstart/control/ControlListener.java
new file mode 100644
index 0000000..d621842
--- /dev/null
+++ b/src/main/java/org/apache/sling/kickstart/control/ControlListener.java
@@ -0,0 +1,628 @@
+/*
+ * 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.kickstart.control;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.LineNumberReader;
+import java.io.OutputStreamWriter;
+import java.lang.management.LockInfo;
+import java.lang.management.ManagementFactory;
+import java.lang.management.MonitorInfo;
+import java.lang.management.ThreadInfo;
+import java.lang.management.ThreadMXBean;
+import java.math.BigInteger;
+import java.net.ConnectException;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * The <code>ControlListener</code> class is a helper class for the {@link ControlTarget}
+ * class to support in Sling standalone application process communication. This
+ * class implements the client and server sides of a TCP/IP based communication
+ * channel to control a running Sling application.
+ * <p>
+ * The server side listens for commands on a configurable host and port &endash;
+ * <code>localhost:63000</code> by default &endash; supporting the following
+ * commands:
+ * <table>
+ * <tr>
+ * <th>Command</th>
+ * <th>Description</th>
+ * </tr>
+ * <tr>
+ * <td><code>status</code></td>
+ * <td>Request status information. Currently only <i>OK</i> is sent back. If no
+ * connection can be created to the server the client assumes Sling is not
+ * running.</td>
+ * </tr>
+ * <tr>
+ * <td><code>stop</code></td>
+ * <td>Requests Sling to shutdown.</td>
+ * </tr>
+ * </table>
+ */
+public class ControlListener implements Runnable {
+
+    // command sent by the client to cause Sling to shutdown
+    static final String COMMAND_STOP = "stop";
+
+    // command sent by the client to check for the status of the server
+    static final String COMMAND_STATUS = "status";
+
+    // command sent by the client to request a thread dump
+    static final String COMMAND_THREADS = "threads";
+
+    // the response sent by the server if the command executed successfully
+    private static final String RESPONSE_OK = "OK";
+
+    // the status response sent by the server when shutting down
+    private static final String RESPONSE_STOPPING = "STOPPING";
+
+    // The default interface to listen on
+    private static final String DEFAULT_LISTEN_INTERFACE = "127.0.0.1";
+
+    // The default port to listen on and to connect to - we select it randomly
+    private static final int DEFAULT_LISTEN_PORT = 0;
+
+    // The reference to the Main class to shutdown on request
+    private final ControlTarget controlTarget;
+
+    private final String listenSpec;
+
+    private String secretKey;
+    private InetSocketAddress socketAddress;
+
+    private volatile Thread shutdownThread = null;
+
+    /**
+     * Creates an instance of this control support class.
+     * <p>
+     * The host (name or address) and port number of the socket is defined by
+     * the <code>listenSpec</code> parameter. This parameter is defined as
+     * <code>[ host ":" ] port</code>. If the parameter is empty or
+     * <code>null</code> it defaults to <i>localhost:0</i>. If the host name
+     * is missing it defaults to <i>localhost</i>.
+     *
+     * @param controlTarget The Main class reference. This is only required if this
+     *            instance is used for the server side to listen for remote stop
+     *            commands. Otherwise this argument may be <code>null</code>.
+     * @param listenSpec The specification for the host and port for the socket
+     *            connection. See above for the format of this parameter.
+     */
+    public ControlListener(final ControlTarget controlTarget, final String listenSpec) {
+        this.controlTarget = controlTarget;
+        this.listenSpec = listenSpec; // socketAddress = this.getSocketAddress(listenSpec, selectNewPort);
+    }
+
+    /**
+     * Implements the server side of the control connection starting a thread
+     * listening on the host and port configured on setup of this instance.
+     */
+    public boolean listen() {
+        final File configFile = getConfigFile();
+        if (configFile.canRead() && statusServer() == 0) {
+            // server already running, fail
+            controlTarget.error("Sling already active in " + this.controlTarget.getHome(), null);
+            return false;
+        }
+        configFile.delete();
+
+        final Thread listener = new Thread(this);
+        listener.setDaemon(true);
+        listener.setName("Apache Sling Control Listener (inactive)");
+        listener.start();
+        return true;
+    }
+
+    /**
+     * Implements the client side of the control connection sending the command
+     * to shutdown Sling.
+     */
+    public int shutdownServer() {
+        return sendCommand(COMMAND_STOP);
+    }
+
+    /**
+     * Implements the client side of the control connection sending the command
+     * to check whether Sling is active.
+     */
+    public int statusServer() {
+        return sendCommand(COMMAND_STATUS);
+    }
+
+    /**
+     * Implements the client side of the control connection sending the command
+     * to retrieve a thread dump.
+     */
+    public int dumpThreads() {
+        return sendCommand(COMMAND_THREADS);
+    }
+
+    // ---------- Runnable interface
+
+    /**
+     * Implements the server thread receiving commands from clients and acting
+     * upon them.
+     */
+    @Override
+    public void run() {
+        this.configure(false);
+
+        final ServerSocket server;
+        try {
+            server = new ServerSocket();
+            server.bind(this.socketAddress);
+            writePortToConfigFile(getConfigFile(),
+                new InetSocketAddress(server.getInetAddress(), server.getLocalPort()), this.secretKey);
+            Thread.currentThread().setName(
+                "Apache Sling Control Listener@" + server.getInetAddress() + ":" + server.getLocalPort());
+            controlTarget.info("Apache Sling Control Listener started", null);
+        } catch (final IOException ioe) {
+            controlTarget.error("Failed to start Apache Sling Control Listener", ioe);
+            return;
+        }
+
+        long delay = 0;
+
+        try {
+            while (true) {
+
+                final Socket s;
+                try {
+                    s = server.accept();
+                } catch (IOException ioe) {
+                    // accept terminated, most probably due to Socket.close()
+                    // just end the loop and exit
+                    break;
+                }
+
+                // delay processing after unsuccessful attempts
+                if (delay > 0) {
+                    controlTarget.info(s.getRemoteSocketAddress() + ": Delay: " + (delay / 1000), null);
+                    try {
+                        Thread.sleep(delay);
+                    } catch (InterruptedException e) {
+                    }
+                }
+
+                try {
+                    final String commandLine = readLine(s);
+                    if (commandLine == null) {
+                        final String msg = "ERR: missing command";
+                        writeLine(s, msg);
+                        continue;
+                    }
+
+                    final int blank = commandLine.indexOf(' ');
+                    if (blank < 0) {
+                        final String msg = "ERR: missing key";
+                        writeLine(s, msg);
+                        continue;
+                    }
+
+                    if (!secretKey.equals(commandLine.substring(0, blank))) {
+                        final String msg = "ERR: wrong key";
+                        writeLine(s, msg);
+                        delay = (delay > 0) ? delay * 2 : 1000L;
+                        continue;
+                    }
+
+                    final String command = commandLine.substring(blank + 1);
+                    controlTarget.info(s.getRemoteSocketAddress() + ">" + command, null);
+
+                    if (COMMAND_STOP.equals(command)) {
+                        if (this.shutdownThread != null) {
+                            writeLine(s, RESPONSE_STOPPING);
+                        } else {
+                            this.shutdownThread = new Thread("Apache Sling Control Listener: Shutdown") {
+                                @Override
+                                public void run() {
+                                    controlTarget.doStop();
+                                    try {
+                                        server.close();
+                                    } catch (final IOException ignore) {
+                                    }
+                                }
+                            };
+                            this.shutdownThread.start();
+                            writeLine(s, RESPONSE_OK);
+                        }
+
+                    } else if (COMMAND_STATUS.equals(command)) {
+                        writeLine(s, (this.shutdownThread == null) ? RESPONSE_OK : RESPONSE_STOPPING);
+
+                    } else if (COMMAND_THREADS.equals(command)) {
+                        dumpThreads(s);
+
+                    } else {
+                        final String msg = "ERR:" + command;
+                        writeLine(s, msg);
+
+                    }
+                } finally {
+                    try {
+                        s.close();
+                    } catch (IOException ignore) {
+                    }
+                }
+            }
+        } catch (final IOException ioe) {
+            controlTarget.error("Failure reading from client", ioe);
+        } finally {
+            try {
+                server.close();
+            } catch (final IOException ignore) {
+            }
+        }
+
+        getConfigFile().delete();
+
+        // everything has stopped and when this thread terminates,
+        // the VM should stop. If there are still some non-daemon threads
+        // active, this will not happen, so we force this here ...
+        controlTarget.info("Apache Sling terminated, exiting Java VM", null);
+        this.controlTarget.doTerminateVM(0);
+    }
+
+    // ---------- socket support
+
+    private void dumpThreads(final Socket socket) throws IOException {
+
+        final ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
+        final ThreadInfo[] threadInfos = threadBean.dumpAllThreads(true, true);
+
+        for (ThreadInfo thread : threadInfos) {
+            printThread(socket, thread);
+
+            // add locked synchronizers
+            final LockInfo[] locks = thread.getLockedSynchronizers();
+            writeLine(socket, "-");
+            writeLine(socket, "-   Locked ownable synchronizers:");
+            if (locks.length > 0) {
+                for (LockInfo li : locks) {
+                    writeLine(socket, String.format("-        - locked %s",
+                        formatLockInfo(
+                            li.getClassName(),
+                            li.getIdentityHashCode()
+                        )
+                    ));
+                }
+            } else {
+                writeLine(socket, "-        - None");
+            }
+
+            // empty separator line
+            writeLine(socket, "-");
+        }
+
+        final long[] deadLocked;
+        if (threadBean.isSynchronizerUsageSupported()) {
+            deadLocked = threadBean.findDeadlockedThreads();
+        } else {
+            deadLocked = threadBean.findMonitorDeadlockedThreads();
+        }
+        if (deadLocked != null) {
+            final ThreadInfo[] dl = threadBean.getThreadInfo(deadLocked, true, true);
+            final Set<ThreadInfo> dlSet = new HashSet<ThreadInfo>(Arrays.asList(dl));
+            int deadlockCount = 0;
+            for (ThreadInfo current : dl) {
+                if (dlSet.remove(current)) {
+
+                    // find and record a single deadlock
+                    ArrayList<ThreadInfo> loop = new ArrayList<ThreadInfo>();
+                    do {
+                        loop.add(current);
+                        for (ThreadInfo cand : dl) {
+                            if (cand.getThreadId() == current.getLockOwnerId()) {
+                                current = (dlSet.remove(cand)) ? cand : null;
+                                break;
+                            }
+                        }
+                    } while (current != null);
+
+                    deadlockCount++;
+
+                    // print the deadlock
+                    writeLine(socket, "-Found one Java-level deadlock:");
+                    writeLine(socket, "-=============================");
+                    for (ThreadInfo thread : loop) {
+                        writeLine(socket, String.format("-\"%s\" #%d",
+                            thread.getThreadName(),
+                            thread.getThreadId()
+                        ));
+                        writeLine(socket, String.format("-  waiting on %s",
+                            formatLockInfo(
+                                thread.getLockInfo().getClassName(),
+                                thread.getLockInfo().getIdentityHashCode()
+                            )
+                        ));
+                        writeLine(socket, String.format("-  which is held by \"%s\" #%d",
+                            thread.getLockOwnerName(),
+                            thread.getLockOwnerId()
+                        ));
+                    }
+                    writeLine(socket, "-");
+
+                    writeLine(socket, "-Java stack information for the threads listed above:");
+                    writeLine(socket, "-===================================================");
+
+                    for (ThreadInfo thread : loop) {
+                        printThread(socket, thread);
+                    }
+                    writeLine(socket, "-");
+                }
+            }
+
+//            "Thread-8":
+//                waiting to lock monitor 7f89fb80da08 (object 7f37a0968, a java.lang.Object),
+//                which is held by "Thread-7"
+//              "Thread-7":
+//                waiting to lock monitor 7f89fb80b0b0 (object 7f37a0958, a java.lang.Object),
+//                which is held by "Thread-8"
+
+            writeLine(socket, String.format("-Found %d deadlocks.",
+                deadlockCount
+            ));
+        }
+
+        writeLine(socket, RESPONSE_OK);
+    }
+
+    private String formatLockInfo(final String className, final int objectId) {
+        return String.format("<%08x> (a %s)", objectId, className);
+    }
+
+    private void printThread(final Socket socket, final ThreadInfo thread) throws IOException {
+        writeLine(socket, String.format("-\"%s\" #%d",
+            thread.getThreadName(),
+            thread.getThreadId()
+        ));
+
+        writeLine(socket, String.format("-    java.lang.Thread.State: %s",
+            thread.getThreadState()
+        ));
+
+        final MonitorInfo[] monitors = thread.getLockedMonitors();
+        final StackTraceElement[] trace = thread.getStackTrace();
+        for (int i=0; i < trace.length; i++) {
+            StackTraceElement ste = trace[i];
+            if (ste.isNativeMethod()) {
+                writeLine(socket, String.format("-        at %s.%s(Native Method)",
+                    ste.getClassName(),
+                    ste.getMethodName()
+                ));
+            } else {
+                writeLine(socket, String.format("-        at %s.%s(%s:%d)",
+                    ste.getClassName(),
+                    ste.getMethodName(),
+                    ste.getFileName(),
+                    ste.getLineNumber()
+                ));
+            }
+
+            if (i == 0 && thread.getLockInfo() != null) {
+                writeLine(socket, String.format("-        - waiting on %s%s",
+                    formatLockInfo(
+                        thread.getLockInfo().getClassName(),
+                        thread.getLockInfo().getIdentityHashCode()
+                    ),
+                    (thread.getLockOwnerId() >= 0)
+                        ? String.format(" owned by \"%s\" #%d",
+                            thread.getLockOwnerName(),
+                            thread.getLockOwnerId()
+                        ):""
+                ));
+            }
+
+            for (MonitorInfo mi : monitors) {
+                if (i == mi.getLockedStackDepth()) {
+                    writeLine(socket, String.format("-        - locked %s",
+                        formatLockInfo(
+                            mi.getClassName(),
+                            mi.getIdentityHashCode()
+                        )
+                    ));
+                }
+            }
+        }
+    }
+
+    /**
+     * Sends the given command to the server indicated by the configured
+     * socket address and logs the reply.
+     *
+     * @param command The command to send
+     *
+     * @return A code indicating success of sending the command.
+     */
+    private int sendCommand(final String command) {
+        if (configure(true)) {
+            if (this.secretKey == null) {
+                controlTarget.info("Missing secret key to protect sending '" + command + "' to " + this.socketAddress, null);
+                return 4; // LSB code for unknown status
+            }
+
+            Socket socket = null;
+            try {
+                socket = new Socket();
+                socket.connect(this.socketAddress);
+                writeLine0(socket, this.secretKey + " " + command);
+                final String result = readLine(socket);
+                controlTarget.info("Sent '" + command + "' to " + this.socketAddress + ": " + result, null);
+                return 0; // LSB code for everything's fine
+            } catch (final ConnectException ce) {
+                controlTarget.info("No Apache Sling running at " + this.socketAddress, null);
+                return 3; // LSB code for programm not running
+            } catch (final IOException ioe) {
+                controlTarget.error("Failed sending '" + command + "' to " + this.socketAddress, ioe);
+                return 1; // LSB code for programm dead
+            } finally {
+                if (socket != null) {
+                    try {
+                        socket.close();
+                    } catch (IOException ignore) {
+                    }
+                }
+            }
+        }
+        controlTarget.info("No socket address to send '" + command + "' to", null);
+        return 4; // LSB code for unknown status
+    }
+
+    private String readLine(final Socket socket) throws IOException {
+        final BufferedReader br = new BufferedReader(new InputStreamReader(
+            socket.getInputStream(), "UTF-8"));
+
+        StringBuilder b = new StringBuilder();
+        boolean more = true;
+        while (more) {
+            String s = br.readLine();
+            if (s != null && s.startsWith("-")) {
+                s = s.substring(1);
+            } else {
+                more = false;
+            }
+            if (b.length() > 0) {
+                b.append("\r\n");
+            }
+            b.append(s);
+        }
+
+        return b.toString();
+    }
+
+    private void writeLine(final Socket socket, final String line) throws IOException {
+        controlTarget.info(socket.getRemoteSocketAddress() + "<" + line, null);
+        this.writeLine0(socket, line);
+    }
+
+    private void writeLine0(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();
+    }
+
+    /**
+     * Read the port from the config file
+     * @return The port or null
+     */
+    private boolean configure(final boolean fromConfigFile) {
+        boolean result = false;
+        if (fromConfigFile) {
+            final File configFile = this.getConfigFile();
+            if (configFile.canRead()) {
+                try ( final LineNumberReader lnr = new LineNumberReader(new FileReader(configFile))) {
+                    this.socketAddress = getSocketAddress(lnr.readLine());
+                    this.secretKey = lnr.readLine();
+                    result = true;
+                } catch (final IOException ignore) {
+                    // ignore
+                }
+            }
+        } else {
+            this.socketAddress = getSocketAddress(this.listenSpec);
+            this.secretKey = generateKey();
+            result = true;
+        }
+
+        return result;
+    }
+
+    private static String generateKey() {
+         return new BigInteger(165, new SecureRandom()).toString(32);
+    }
+
+    /**
+     * Return the control port file
+     */
+    private File getConfigFile() {
+        final File configDir = new File(this.controlTarget.getHome(), "conf");
+        return new File(configDir, "controlport");
+    }
+
+    private InetSocketAddress getSocketAddress(String listenSpec) {
+        try {
+
+            final String address;
+            final int port;
+            if (listenSpec == null) {
+                address = DEFAULT_LISTEN_INTERFACE;
+                port = DEFAULT_LISTEN_PORT;
+            } else {
+                final int colon = listenSpec.indexOf(':');
+                if (colon < 0) {
+                    address = DEFAULT_LISTEN_INTERFACE;
+                    port = Integer.parseInt(listenSpec);
+                } else {
+                    address = listenSpec.substring(0, colon);
+                    port = Integer.parseInt(listenSpec.substring(colon + 1));
+                }
+            }
+
+            final InetSocketAddress addr = new InetSocketAddress(address, port);
+            if (!addr.isUnresolved()) {
+                return addr;
+            }
+
+            controlTarget.error("Unknown host in '" + listenSpec, null);
+        } catch (final NumberFormatException nfe) {
+            controlTarget.error("Cannot parse port number from '" + listenSpec + "'",
+                null);
+        }
+
+        return null;
+    }
+
+    private void writePortToConfigFile(final File configFile, final InetSocketAddress socketAddress,
+            final String secretKey) {
+        configFile.getParentFile().mkdirs();
+        FileWriter fw = null;
+        try {
+            fw = new FileWriter(configFile);
+            fw.write(socketAddress.getAddress().getHostAddress());
+            fw.write(':');
+            fw.write(String.valueOf(socketAddress.getPort()));
+            fw.write('\n');
+            fw.write(secretKey);
+            fw.write('\n');
+        } catch (final IOException ignore) {
+            // ignore
+        } finally {
+            if (fw != null) {
+                try {
+                    fw.close();
+                } catch (final IOException ignore) {
+                }
+            }
+        }
+    }
+}
diff --git a/src/main/java/org/apache/sling/kickstart/control/ControlTarget.java b/src/main/java/org/apache/sling/kickstart/control/ControlTarget.java
new file mode 100644
index 0000000..3f714ba
--- /dev/null
+++ b/src/main/java/org/apache/sling/kickstart/control/ControlTarget.java
@@ -0,0 +1,32 @@
+/*
+ * 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.kickstart.control;
+
+public interface ControlTarget {
+
+    public String getHome();
+
+    public void doStop();
+
+    public void doTerminateVM(int status);
+
+    void info(String message, Throwable t);
+
+    void error(String message, Throwable t);
+}
diff --git a/src/main/resources/META-INF/services/org.apache.sling.feature.launcher.spi.Launcher b/src/main/resources/META-INF/services/org.apache.sling.feature.launcher.spi.Launcher
new file mode 100644
index 0000000..21483ff
--- /dev/null
+++ b/src/main/resources/META-INF/services/org.apache.sling.feature.launcher.spi.Launcher
@@ -0,0 +1 @@
+org.apache.sling.feature.launcher.impl.launchers.FrameworkLauncher
\ No newline at end of file
diff --git a/src/main/resources/META-INF/services/org.apache.sling.feature.launcher.spi.extensions.ExtensionHandler b/src/main/resources/META-INF/services/org.apache.sling.feature.launcher.spi.extensions.ExtensionHandler
new file mode 100644
index 0000000..e936f9a
--- /dev/null
+++ b/src/main/resources/META-INF/services/org.apache.sling.feature.launcher.spi.extensions.ExtensionHandler
@@ -0,0 +1,2 @@
+org.apache.sling.feature.launcher.impl.extensions.handlers.RepoInitHandler
+org.apache.sling.feature.launcher.impl.extensions.handlers.ContentPackageHandler
diff --git a/src/main/resources/feature-sling12.json b/src/main/resources/feature-sling12.json
new file mode 100644
index 0000000..85b68c4
--- /dev/null
+++ b/src/main/resources/feature-sling12.json
@@ -0,0 +1,1139 @@
+{
+  "id":"org.apache.sling:org.apache.sling.feature.model.starter:slingosgifeature:sling12:12-SNAPSHOT",
+  "title":"Apache Sling Feature Module Starter Application",
+  "description":"The Sling Starter application built from Feature Models",
+  "vendor":"The Apache Software Foundation",
+  "license":"Apache License, Version 2.0",
+  "variables":{
+    "composum.nodes.version":"1.11.3",
+    "oak.version":"1.16.0",
+    "jackson.version":"2.9.9",
+    "slf4j.version":"1.7.25",
+    "jackrabbit.version":"2.18.2"
+  },
+  "bundles":[
+    {
+      "id":"org.apache.aries:org.apache.aries.util:1.1.3",
+      "start-order":"1"
+    },
+    {
+      "id":"org.apache.commons:commons-lang3:3.8.1",
+      "start-order":"1"
+    },
+    {
+      "id":"org.apache.felix:org.apache.felix.configadmin:1.9.14",
+      "start-order":"1"
+    },
+    {
+      "id":"org.apache.felix:org.apache.felix.eventadmin:1.5.0",
+      "start-order":"1"
+    },
+    {
+      "id":"org.apache.geronimo.specs:geronimo-annotation_1.3_spec:1.1",
+      "start-order":"1"
+    },
+    {
+      "id":"org.apache.geronimo.specs:geronimo-atinject_1.0_spec:1.1",
+      "start-order":"1"
+    },
+    {
+      "id":"org.apache.geronimo.specs:geronimo-ws-metadata_2.0_spec:1.1.3",
+      "start-order":"1"
+    },
+    {
+      "id":"org.apache.servicemix.bundles:org.apache.servicemix.bundles.jaxb-impl:2.2.11_1",
+      "start-order":"1"
+    },
+    {
+      "id":"org.apache.servicemix.bundles:org.apache.servicemix.bundles.saaj-impl:1.3.23_2",
+      "start-order":"1"
+    },
+    {
+      "id":"org.apache.servicemix.specs:org.apache.servicemix.specs.jaxb-api-2.2:2.9.0",
+      "start-order":"1"
+    },
+    {
+      "id":"org.apache.servicemix.specs:org.apache.servicemix.specs.jaxws-api-2.2:2.9.0",
+      "start-order":"1"
+    },
+    {
+      "id":"org.apache.servicemix.specs:org.apache.servicemix.specs.saaj-api-1.3:2.8.0",
+      "start-order":"1"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.commons.log:5.1.10",
+      "start-order":"1"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.commons.logservice:1.0.6",
+      "start-order":"1"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.installer.core:3.9.0",
+      "start-order":"1"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.installer.factory.configuration:1.2.2",
+      "start-order":"1"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.installer.provider.file:1.1.0",
+      "start-order":"1"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.javax.activation:0.1.0",
+      "start-order":"1"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.settings:1.3.10",
+      "start-order":"1"
+    },
+    {
+      "id":"org.jvnet.staxex:stax-ex:1.7.6",
+      "start-order":"1"
+    },
+    {
+      "id":"org.osgi:org.osgi.util.function:1.1.0",
+      "start-order":"1"
+    },
+    {
+      "id":"org.osgi:org.osgi.util.promise:1.1.0",
+      "start-order":"1"
+    },
+    {
+      "id":"org.slf4j:jcl-over-slf4j:1.7.25",
+      "start-order":"1"
+    },
+    {
+      "id":"org.slf4j:log4j-over-slf4j:1.7.25",
+      "start-order":"1"
+    },
+    {
+      "id":"org.slf4j:slf4j-api:1.7.25",
+      "start-order":"1"
+    },
+    {
+      "id":"com.composum.sling.core:composum-sling-core-commons:1.11.3",
+      "start-order":"20"
+    },
+    {
+      "id":"com.composum.sling.core:composum-sling-core-console:1.11.3",
+      "start-order":"20"
+    },
+    {
+      "id":"com.composum.sling.core:composum-sling-core-jslibs:1.11.3",
+      "start-order":"20"
+    },
+    {
+      "id":"com.composum.sling.core:composum-sling-package-manager:1.11.3",
+      "start-order":"20"
+    },
+    {
+      "id":"com.composum.sling.core:composum-sling-user-management:1.11.3",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.jackrabbit.vault:org.apache.jackrabbit.vault:3.2.4",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.felix:org.apache.felix.healthcheck.api:2.0.2",
+      "start-order":"5"
+    },
+    {
+      "id":"org.apache.felix:org.apache.felix.healthcheck.core:2.0.6",
+      "start-order":"5"
+    },
+    {
+      "id":"org.apache.felix:org.apache.felix.healthcheck.generalchecks:2.0.4",
+      "start-order":"5"
+    },
+    {
+      "id":"org.apache.felix:org.apache.felix.healthcheck.webconsoleplugin:2.0.0",
+      "start-order":"5"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.hc.api:1.0.4",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.hc.support:1.0.6",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.launchpad.base:6.0.2-2.6.36",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.felix:org.apache.felix.jaas:1.0.2",
+      "start-order":"10"
+    },
+    {
+      "id":"org.apache.jackrabbit:oak-api:1.16.0",
+      "start-order":"15"
+    },
+    {
+      "id":"org.apache.jackrabbit:oak-blob-plugins:1.16.0",
+      "start-order":"15"
+    },
+    {
+      "id":"org.apache.jackrabbit:oak-blob:1.16.0",
+      "start-order":"15"
+    },
+    {
+      "id":"org.apache.jackrabbit:oak-commons:1.16.0",
+      "start-order":"15"
+    },
+    {
+      "id":"org.apache.jackrabbit:oak-core-spi:1.16.0",
+      "start-order":"15"
+    },
+    {
+      "id":"org.apache.jackrabbit:oak-core:1.16.0",
+      "start-order":"15"
+    },
+    {
+      "id":"org.apache.jackrabbit:oak-jcr:1.16.0",
+      "start-order":"15"
+    },
+    {
+      "id":"org.apache.jackrabbit:oak-lucene:1.16.0",
+      "start-order":"15"
+    },
+    {
+      "id":"org.apache.jackrabbit:oak-query-spi:1.16.0",
+      "start-order":"15"
+    },
+    {
+      "id":"org.apache.jackrabbit:oak-security-spi:1.16.0",
+      "start-order":"15"
+    },
+    {
+      "id":"org.apache.jackrabbit:oak-store-composite:1.16.0",
+      "start-order":"15"
+    },
+    {
+      "id":"org.apache.jackrabbit:oak-store-document:1.16.0",
+      "start-order":"15"
+    },
+    {
+      "id":"org.apache.jackrabbit:oak-store-spi:1.16.0",
+      "start-order":"15"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.jcr.oak.server:1.2.2",
+      "start-order":"16"
+    },
+    {
+      "id":"org.apache.jackrabbit:oak-segment-tar:1.16.0",
+      "run-modes":"oak_tar",
+      "start-order":"15"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.jcr.repoinit:1.1.10",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.provisioning.model:1.8.4",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.repoinit.parser:1.2.4",
+      "start-order":"20"
+    },
+    {
+      "id":"org.antlr:antlr4-runtime:4.7.1",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.servicemix.bundles:org.apache.servicemix.bundles.rhino:1.7.10_1",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.scripting.api:2.2.0",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.scripting.core:2.0.56",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.scripting.el-api:1.0.0",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.scripting.javascript:3.0.4",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.scripting.jsp-api:1.0.0",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.scripting.jsp.taglib:2.4.0",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.scripting.jsp:2.3.4",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.scripting.sightly.compiler.java:1.1.2-1.4.0",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.scripting.sightly.compiler:1.1.2-1.4.0",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.scripting.sightly.js.provider:1.0.28",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.scripting.sightly.models.provider:1.0.8",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.scripting.sightly.repl:1.0.6",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.scripting.sightly.runtime:1.1.0-1.4.0",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.scripting.sightly:1.1.2-1.4.0",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.caconfig.api:1.1.2",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.caconfig.impl:1.4.14",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.caconfig.spi:1.3.4",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.discovery.api:1.0.4",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.discovery.base:2.0.8",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.discovery.commons:1.0.20",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.discovery.oak:1.2.28",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.discovery.support:1.0.4",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.event.dea:1.1.4",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.event:4.2.12",
+      "start-order":"20"
+    },
+    {
+      "id":"com.fasterxml.jackson.core:jackson-annotations:2.9.9",
+      "start-order":"20"
+    },
+    {
+      "id":"com.fasterxml.jackson.core:jackson-core:2.9.9",
+      "start-order":"20"
+    },
+    {
+      "id":"com.fasterxml.jackson.core:jackson-databind:2.9.9",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.models.jacksonexporter:1.0.8",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.validation.api:1.0.0",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.validation.core:1.0.4",
+      "start-order":"20"
+    },
+    {
+      "id":"commons-codec:commons-codec:1.12",
+      "start-order":"20"
+    },
+    {
+      "id":"commons-collections:commons-collections:3.2.2",
+      "start-order":"20"
+    },
+    {
+      "id":"javax.mail:mail:1.5.0-b01",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.commons:commons-collections4:4.2",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.commons:commons-math:2.2",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.geronimo.bundles:jstl:1.2_1",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.httpcomponents:httpclient-osgi:4.5.6",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.httpcomponents:httpcore-osgi:4.4.10",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.adapter:2.1.10",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.auth.form:1.0.14",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.bundleresource.impl:2.3.2",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.commons.classloader:1.4.4",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.commons.compiler:2.3.6",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.commons.fsclassloader:1.0.10",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.commons.mime:2.2.0",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.commons.osgi:2.4.0",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.commons.scheduler:2.7.2",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.commons.threads:3.2.18",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.engine:2.6.18",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.fsresource:2.1.14",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.i18n:2.5.14",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.installer.console:1.0.2",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.installer.hc:2.0.2",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.installer.provider.jcr:3.1.26",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.jcr.contentloader:2.3.0",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.jcr.resource:3.0.18",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.models.api:1.3.8",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.models.impl:1.4.10",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.resourceresolver:1.6.8",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.serviceuser.webconsole:1.0.0",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.serviceusermapper:1.4.4",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.servlets.get:2.1.40",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.servlets.post:2.3.30",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.servlets.resolver:2.5.2",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.xss:2.1.8",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.felix:org.apache.felix.metatype:1.2.2",
+      "start-order":"4"
+    },
+    {
+      "id":"org.apache.felix:org.apache.felix.scr:2.1.16",
+      "start-order":"4"
+    },
+    {
+      "id":"commons-fileupload:commons-fileupload:1.3.3",
+      "start-order":"5"
+    },
+    {
+      "id":"commons-io:commons-io:2.6",
+      "start-order":"5"
+    },
+    {
+      "id":"org.apache.aries.jmx:org.apache.aries.jmx.api:1.1.5",
+      "start-order":"5"
+    },
+    {
+      "id":"org.apache.aries.jmx:org.apache.aries.jmx.core:1.1.8",
+      "start-order":"5"
+    },
+    {
+      "id":"org.apache.aries.jmx:org.apache.aries.jmx.whiteboard:1.2.0",
+      "start-order":"5"
+    },
+    {
+      "id":"org.apache.felix:org.apache.felix.bundlerepository:2.0.10",
+      "start-order":"5"
+    },
+    {
+      "id":"org.apache.felix:org.apache.felix.http.whiteboard:4.0.0",
+      "start-order":"5"
+    },
+    {
+      "id":"org.apache.felix:org.apache.felix.inventory:1.0.6",
+      "start-order":"5"
+    },
+    {
+      "id":"org.apache.felix:org.apache.felix.prefs:1.1.0",
+      "start-order":"5"
+    },
+    {
+      "id":"org.apache.felix:org.apache.felix.webconsole.plugins.ds:2.1.0",
+      "start-order":"5"
+    },
+    {
+      "id":"org.apache.felix:org.apache.felix.webconsole.plugins.event:1.1.8",
+      "start-order":"5"
+    },
+    {
+      "id":"org.apache.felix:org.apache.felix.webconsole.plugins.memoryusage:1.0.8",
+      "start-order":"5"
+    },
+    {
+      "id":"org.apache.felix:org.apache.felix.webconsole.plugins.obr:1.0.4",
+      "start-order":"5"
+    },
+    {
+      "id":"org.apache.felix:org.apache.felix.webconsole.plugins.packageadmin:1.0.4",
+      "start-order":"5"
+    },
+    {
+      "id":"org.apache.felix:org.apache.felix.webconsole:4.3.8",
+      "start-order":"5"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.api:2.20.0",
+      "start-order":"5"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.auth.core:1.4.2",
+      "start-order":"5"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.commons.johnzon:1.1.2",
+      "start-order":"5"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.commons.log.webconsole:1.0.0",
+      "start-order":"5"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.extensions.threaddump:0.2.2",
+      "start-order":"5"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.extensions.webconsolebranding:1.0.2",
+      "start-order":"5"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.extensions.webconsolesecurityprovider:1.2.2",
+      "start-order":"5"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.starter.content:1.0.4",
+      "start-order":"5"
+    },
+    {
+      "id":"org.apache.felix:org.apache.felix.http.sslfilter:1.2.6",
+      "start-order":"10"
+    },
+    {
+      "id":"org.apache.pdfbox:fontbox:2.0.16",
+      "start-order":"10"
+    },
+    {
+      "id":"org.apache.pdfbox:jempbox:1.8.16",
+      "start-order":"10"
+    },
+    {
+      "id":"org.apache.pdfbox:pdfbox:2.0.16",
+      "start-order":"10"
+    },
+    {
+      "id":"org.apache.tika:tika-core:1.21",
+      "start-order":"10"
+    },
+    {
+      "id":"org.apache.tika:tika-parsers:1.21",
+      "start-order":"10"
+    },
+    {
+      "id":"com.google.guava:guava:15.0",
+      "start-order":"15"
+    },
+    {
+      "id":"io.dropwizard.metrics:metrics-core:3.2.6",
+      "start-order":"15"
+    },
+    {
+      "id":"org.apache.jackrabbit:jackrabbit-api:2.18.2",
+      "start-order":"15"
+    },
+    {
+      "id":"org.apache.jackrabbit:jackrabbit-data:2.18.2",
+      "start-order":"15"
+    },
+    {
+      "id":"org.apache.jackrabbit:jackrabbit-jcr-commons:2.18.2",
+      "start-order":"15"
+    },
+    {
+      "id":"org.apache.jackrabbit:jackrabbit-jcr-rmi:2.18.2",
+      "start-order":"15"
+    },
+    {
+      "id":"org.apache.jackrabbit:jackrabbit-spi-commons:2.18.2",
+      "start-order":"15"
+    },
+    {
+      "id":"org.apache.jackrabbit:jackrabbit-spi:2.18.2",
+      "start-order":"15"
+    },
+    {
+      "id":"org.apache.jackrabbit:jackrabbit-webdav:2.18.2",
+      "start-order":"15"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.commons.metrics:1.2.6",
+      "start-order":"15"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.jcr.api:2.4.0",
+      "start-order":"15"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.jcr.base:3.0.6",
+      "start-order":"15"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.jcr.davex:1.3.10",
+      "start-order":"15"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.jcr.jackrabbit.accessmanager:3.0.4",
+      "start-order":"15"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.jcr.jackrabbit.usermanager:2.2.8",
+      "start-order":"15"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.jcr.jcr-wrapper:2.0.0",
+      "start-order":"15"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.jcr.registration:1.0.6",
+      "start-order":"15"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.jcr.webconsole:1.0.2",
+      "start-order":"15"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.jcr.webdav:2.3.8",
+      "start-order":"15"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.resource.filter:1.0.0",
+      "start-order":"15"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.sample.slingshot:0.9.0",
+      "start-order":"20"
+    },
+    {
+      "id":"org.apache.felix:org.apache.felix.http.jetty:4.0.8",
+      "run-modes":":standalone",
+      "start-order":"5"
+    },
+    {
+      "id":"org.apache.felix:org.apache.felix.http.servlet-api:1.1.2",
+      "run-modes":":standalone",
+      "start-order":"5"
+    },
+    {
+      "id":"org.apache.sling:org.apache.sling.jcr.packageinit:0.0.1-SNAPSHOT",
+      "start-order":"20"
+    }
+  ],
+  "configurations":{
+    "org.apache.sling.jcr.base.internal.LoginAdminWhitelist.fragment~composum":{
+      "whitelist.bundles":[
+        "com.composum.core.commons",
+        "com.composum.core.pckgmgr",
+        "com.composum.core.pckginstall"
+      ],
+      "whitelist.name":"composum"
+    },
+    "org.apache.felix.hc.generalchecks.BundlesStartedCheck":{
+      "hc.tags":[
+        "bundles"
+      ]
+    },
+    "org.apache.felix.hc.generalchecks.CpuCheck":{
+      "hc.tags":[
+        "cpu",
+        "system-resources"
+      ],
+      "cpuPercentageThresholdWarn":95
+    },
+    "org.apache.felix.hc.generalchecks.DiskSpaceCheck":{
+      "hc.tags":[
+        "diskspace",
+        "system-resources"
+      ],
+      "diskPaths":[
+        "."
+      ]
+    },
+    "org.apache.felix.hc.generalchecks.FrameworkStartCheck":{
+      "hc.tags":[
+        "systemalive"
+      ],
+      "targetStartLevel:Integer":"30"
+    },
+    "org.apache.felix.hc.generalchecks.MemoryCheck":{
+      "hc.tags":[
+        "memory",
+        "system-resources"
+      ],
+      "heapUsedPercentageThresholdCritical":100,
+      "heapUsedPercentageThresholdWarn":95
+    },
+    "org.apache.felix.hc.generalchecks.ServicesCheck":{
+      "hc.tags":[
+        "systemalive"
+      ],
+      "services.list":[
+        "org.apache.sling.jcr.api.SlingRepository",
+        "org.apache.sling.engine.auth.Authenticator",
+        "org.apache.sling.api.resource.ResourceResolverFactory",
+        "org.apache.sling.api.servlets.ServletResolver",
+        "javax.script.ScriptEngineManager"
+      ]
+    },
+    "org.apache.felix.hc.generalchecks.ThreadUsageCheck":{
+      "hc.tags":[
+        "threads",
+        "cpu",
+        "system-resources"
+      ]
+    },
+    "org.apache.felix.hc.core.impl.filter.ServiceUnavailableFilter~startupandshutdown":{
+      "osgi.http.whiteboard.filter.regex":"(?!/system/).*",
+      "avoid404DuringStartup":true,
+      "service.ranking:Integer":"2147483647",
+      "includeExecutionResult":false,
+      "osgi.http.whiteboard.context.select":"(osgi.http.whiteboard.context.name=*)",
+      "tags":[
+        "systemalive"
+      ],
+      "autoDisableFilter":true,
+      "responseTextFor503":"classpath:org.apache.sling.starter.content:content/content/startup/index.html"
+    },
+    "org.apache.felix.hc.core.impl.servlet.HealthCheckExecutorServlet~default":{
+      "servletPath":"/system/health"
+    },
+    "org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended~hc-support":{
+      "user.mapping":[
+        "org.apache.sling.hc.support=sling-readall"
+      ]
+    },
+    "org.apache.felix.jaas.ConfigurationSpi":{
+      "jaas.defaultRealmName":"jackrabbit.oak",
+      "jaas.configProviderName":"FelixJaasProvider"
+    },
+    "org.apache.jackrabbit.oak.security.authentication.AuthenticationConfigurationImpl":{
+      "org.apache.jackrabbit.oak.authentication.configSpiName":"FelixJaasProvider"
+    },
+    "org.apache.jackrabbit.oak.security.user.RandomAuthorizableNodeName":{
+      "length:Integer":"21"
+    },
+    "org.apache.jackrabbit.oak.security.user.UserConfigurationImpl":{
+      "groupsPath":"/home/groups",
+      "defaultDepth":"1",
+      "importBehavior":"besteffort",
+      "usersPath":"/home/users"
+    },
+    "org.apache.jackrabbit.oak.spi.security.user.action.DefaultAuthorizableActionProvider":{
+      "userPrivilegeNames":[
+        "jcr:all"
+      ],
+      "groupPrivilegeNames":[
+        "jcr:read"
+      ],
+      "enabledActions":[
+        "org.apache.jackrabbit.oak.spi.security.user.action.AccessControlAction"
+      ]
+    },
+    "org.apache.felix.jaas.Configuration.factory~GuestLoginModule":{
+      "jaas.controlFlag":"optional",
+      "jaas.classname":"org.apache.jackrabbit.oak.spi.security.authentication.GuestLoginModule",
+      "jaas.ranking:Integer":"300"
+    },
+    "org.apache.felix.jaas.Configuration.factory~LoginModuleImpl":{
+      "jaas.controlFlag":"required",
+      "jaas.classname":"org.apache.jackrabbit.oak.security.authentication.user.LoginModuleImpl"
+    },
+    "org.apache.felix.jaas.Configuration.factory~TokenLoginModule":{
+      "jaas.controlFlag":"sufficient",
+      "jaas.classname":"org.apache.jackrabbit.oak.security.authentication.token.TokenLoginModule",
+      "jaas.ranking:Integer":"200"
+    },
+    "org.apache.jackrabbit.oak.segment.SegmentNodeStoreService":{
+      "name":"Default NodeStore"
+    },
+    "org.apache.sling.scripting.core.impl.ScriptCacheImpl":{
+      "org.apache.sling.scripting.cache.additional_extensions":[
+        "js"
+      ]
+    },
+    "org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended~scripting":{
+      "user.mapping":[
+        "org.apache.sling.scripting.core=sling-scripting",
+        "org.apache.sling.scripting.sightly.js.provider=sling-scripting"
+      ]
+    },
+    "org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended~sling-caconfig":{
+      "user.mapping":[
+        "org.apache.sling.caconfig.impl=sling-readall"
+      ]
+    },
+    "org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended~sling.discovery":{
+      "user.mapping":[
+        "org.apache.sling.discovery.commons=sling-discovery",
+        "org.apache.sling.discovery.base=sling-discovery",
+        "org.apache.sling.discovery.oak=sling-discovery"
+      ]
+    },
+    "org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended~sling.event":{
+      "user.mapping":[
+        "org.apache.sling.event=sling-event",
+        "org.apache.sling.event.dea=sling-event"
+      ]
+    },
+    "org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended~validation":{
+      "user.mapping":[
+        "org.apache.sling.validation.core=sling-validation"
+      ]
+    },
+    "org.apache.sling.commons.log.LogManager":{
+      "org.apache.sling.commons.log.packagingDataEnabled":true,
+      "org.apache.sling.commons.log.pattern":"%d{dd.MM.yyyy HH:mm:ss.SSS} *%level* [%thread] %logger %msg%n",
+      "org.apache.sling.commons.log.level":"info",
+      "org.apache.sling.commons.log.file":"logs/error.log",
+      "org.apache.sling.commons.log.file.number:Integer":"7",
+      "org.apache.sling.commons.log.file.size":"'.'yyyy-MM-dd"
+    },
+    "org.apache.sling.engine.impl.log.RequestLogger":{
+      "access.log.enabled":true,
+      "request.log.outputtype:Integer":"0",
+      "access.log.output":"log.access",
+      "request.log.output":"log.request",
+      "request.log.enabled":true,
+      "access.log.outputtype:Integer":"0"
+    },
+    "org.apache.sling.jcr.davex.impl.servlets.SlingDavExServlet":{
+      "alias":"/server"
+    },
+    "org.apache.sling.jcr.webdav.impl.servlets.SimpleWebDavServlet":{
+      "dav.root":"/dav"
+    },
+    "org.apache.sling.commons.log.LogManager.factory.config~access.log":{
+      "org.apache.sling.commons.log.pattern":"%msg%n",
+      "org.apache.sling.commons.log.names":[
+        "log.access"
+      ],
+      "org.apache.sling.commons.log.level":"info",
+      "org.apache.sling.commons.log.file":"logs/access.log"
+    },
+    "org.apache.sling.commons.log.LogManager.factory.config~request.log":{
+      "org.apache.sling.commons.log.pattern":"%msg%n",
+      "org.apache.sling.commons.log.names":[
+        "log.request"
+      ],
+      "org.apache.sling.commons.log.level":"info",
+      "org.apache.sling.commons.log.file":"logs/request.log"
+    },
+    "org.apache.sling.jcr.base.internal.LoginAdminWhitelist.fragment~sling":{
+      "whitelist.bundles":[
+        "org.apache.sling.discovery.commons",
+        "org.apache.sling.discovery.base",
+        "org.apache.sling.discovery.oak",
+        "org.apache.sling.extensions.webconsolesecurityprovider",
+        "org.apache.sling.i18n",
+        "org.apache.sling.jcr.base",
+        "org.apache.sling.jcr.contentloader",
+        "org.apache.sling.jcr.jackrabbit.usermanager",
+        "org.apache.sling.jcr.oak.server",
+        "org.apache.sling.jcr.repoinit",
+        "org.apache.sling.jcr.webconsole",
+        "org.apache.sling.servlets.post",
+        "org.apache.sling.serviceuser.webconsole"
+      ],
+      "whitelist.name":"sling"
+    },
+    "org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended~i18n":{
+      "user.mapping":[
+        "org.apache.sling.i18n=sling-i18n"
+      ]
+    },
+    "org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended~jcr-install":{
+      "user.mapping":[
+        "org.apache.sling.installer.provider.jcr=sling-jcr-install"
+      ]
+    },
+    "org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended~jcr-resource":{
+      "user.mapping":[
+        "org.apache.sling.jcr.resource:validation=sling-readall"
+      ]
+    },
+    "org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended~observation":{
+      "user.mapping":[
+        "org.apache.sling.jcr.resource:observation=sling-readall"
+      ]
+    },
+    "org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended~resourceresolver":{
+      "user.mapping":[
+        "org.apache.sling.resourceresolver:mapping=sling-mapping",
+        "org.apache.sling.resourceresolver:hierarchy=sling-readall",
+        "org.apache.sling.resourceresolver:observation=sling-readall",
+        "org.apache.sling.resourceresolver:console=sling-readall"
+      ]
+    },
+    "org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended~servletsresolver":{
+      "user.mapping":[
+        "org.apache.sling.servlets.resolver:console=sling-readall",
+        "org.apache.sling.servlets.resolver:scripts=sling-scripting"
+      ]
+    },
+    "org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended~xss":{
+      "user.mapping":[
+        "org.apache.sling.xss=sling-xss"
+      ]
+    },
+    "org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended~sling.slingshot":{
+      "user.mapping":[
+        "org.apache.sling.sample.slingshot=slingshot-service"
+      ]
+    }
+  },
+  "framework-properties":{
+    "sling.run.mode.install.options":"oak_tar,oak_mongo",
+    "sling.jre.java.xml":",javax.xml;version=\"2.1.0\",javax.xml.datatype;uses:=\"javax.xml.namespace\";version=\"2.1.0\",javax.xml.namespace;version=\"2.1.0\",javax.xml.parsers;uses:=\"javax.xml.validation,org.w3c.dom,org.xml.sax,org.xml.sax.helpers\";version=\"2.1.0\",javax.xml.stream;uses:=\"javax.xml.namespace,javax.xml.stream.events,javax.xml.stream.util,javax.xml.transform\";version=\"1.0.0\",javax.xml.stream.events;uses:=\"javax.xml.namespace,javax.xml.stream\";version=\"1.0.0\",j [...]
+    "felix.systempackages.calculate.uses":"true",
+    "localIndexDir":"${sling.home}/repository/index",
+    "org.osgi.framework.system.packages":"org.osgi.framework;version=\"1.9\",org.osgi.framework.dto;version=\"1.8\";uses:=\"org.osgi.dto\",org.osgi.framework.hooks.bundle;version=\"1.1\";uses:=\"org.osgi.framework\",org.osgi.framework.hooks.resolver;version=\"1.0\";uses:=\"org.osgi.framework.wiring\",org.osgi.framework.hooks.service;version=\"1.1\";uses:=\"org.osgi.framework\",org.osgi.framework.hooks.weaving;version=\"1.1\";uses:=\"org.osgi.framework.wiring\",org.osgi.framework.launch;v [...]
+    "repository.home":"${sling.home}/repository",
+    "felix.systempackages.substitution":"true",
+    "sling.jre-jpms":"{dollar}{felix.jpms.java.base}{dollar}{felix.jpms.java.compiler}{dollar}{felix.jpms.java.datatransfer}{dollar}{felix.jpms.java.desktop}{dollar}{felix.jpms.java.instrument}{dollar}{felix.jpms.java.logging}{dollar}{felix.jpms.java.management}{dollar}{felix.jpms.java.management.rmi}{dollar}{felix.jpms.java.naming}{dollar}{felix.jpms.java.net.http}{dollar}{felix.jpms.java.prefs}{dollar}{felix.jpms.java.rmi}{dollar}{felix.jpms.java.scripting}{dollar}{felix.jpms.java.se}{ [...]
+    "sling.jpms.java.xml":"{dollar}{sling.jre.java.xml},javax.xml.catalog;uses:=\"javax.xml.namespace\";version=\"1.0.0\"",
+    "sling.jre-1.8":",java.applet;version=\"{dollar}{felix.detect.java.version}\",java.awt;version=\"{dollar}{felix.detect.java.version}\",java.awt.color;version=\"{dollar}{felix.detect.java.version}\",java.awt.datatransfer;version=\"{dollar}{felix.detect.java.version}\",java.awt.dnd;version=\"{dollar}{felix.detect.java.version}\",java.awt.event;version=\"{dollar}{felix.detect.java.version}\",java.awt.font;version=\"{dollar}{felix.detect.java.version}\",java.awt.geom;version=\"{dollar}{f [...]
+  },
+  "assembled-features:ARTIFACTS|TRANSIENT":[
+    "org.apache.sling:org.apache.sling.feature.model.starter:slingfeature:boot:12-SNAPSHOT",
+    "org.apache.sling:org.apache.sling.feature.model.starter:slingfeature:composum_composum-nodes:12-SNAPSHOT",
+    "org.apache.sling:org.apache.sling.feature.model.starter:slingfeature:healthcheck:12-SNAPSHOT",
+    "org.apache.sling:org.apache.sling.feature.model.starter:slingfeature:launchpad:12-SNAPSHOT",
+    "org.apache.sling:org.apache.sling.feature.model.starter:slingfeature:oak:12-SNAPSHOT",
+    "org.apache.sling:org.apache.sling.feature.model.starter:slingfeature:repoinit:12-SNAPSHOT",
+    "org.apache.sling:org.apache.sling.feature.model.starter:slingfeature:scripting_sling:12-SNAPSHOT",
+    "org.apache.sling:org.apache.sling.feature.model.starter:slingfeature:sling-caconfig:12-SNAPSHOT",
+    "org.apache.sling:org.apache.sling.feature.model.starter:slingfeature:sling-discovery:12-SNAPSHOT",
+    "org.apache.sling:org.apache.sling.feature.model.starter:slingfeature:sling-event:12-SNAPSHOT",
+    "org.apache.sling:org.apache.sling.feature.model.starter:slingfeature:sling-models-jacksonexporter_models-jacksonexporter:12-SNAPSHOT",
+    "org.apache.sling:org.apache.sling.feature.model.starter:slingfeature:sling-validation:12-SNAPSHOT",
+    "org.apache.sling:org.apache.sling.feature.model.starter:slingfeature:sling:12-SNAPSHOT",
+    "org.apache.sling:org.apache.sling.feature.model.starter:slingfeature:sling_slingshot:12-SNAPSHOT",
+    "org.apache.sling:org.apache.sling.feature.model.starter:slingfeature:standalone:12-SNAPSHOT",
+    "org.apache.sling:org.apache.sling.feature.model.starter:slingfeature:webapp:12-SNAPSHOT",
+    "org.apache.sling:org.apache.sling.jcr.packageinit:slingosgifeature:jcr-packageinit:0.0.1-SNAPSHOT"
+  ],
+  "repoinit:TEXT|true":[
+    "# general",
+    "create path (sling:OrderedFolder) /content",
+    "set ACL for everyone",
+    "allow   jcr:read\ton /content",
+    "end",
+    "",
+    "# sling-mapping",
+    "create service user sling-mapping",
+    "",
+    "set ACL for sling-mapping",
+    "allow   jcr:read    on /",
+    "end",
+    "",
+    "# sling-readall",
+    "create service user sling-readall",
+    "",
+    "set ACL for sling-readall",
+    "allow   jcr:read    on /",
+    "end",
+    "",
+    "# sling-xss",
+    "create service user sling-xss",
+    "",
+    "create path (sling:Folder) /apps/sling/xss",
+    "",
+    "set ACL for sling-xss",
+    "allow   jcr:read    on /apps/sling/xss",
+    "end",
+    "",
+    "# sling-i18n",
+    "create service user sling-i18n",
+    "",
+    "set ACL for sling-i18n",
+    "allow   jcr:read    on /",
+    "end",
+    "",
+    "# sling-jcr-install",
+    "create service user sling-jcr-install",
+    "",
+    "# used for config OSGi writeback",
+    "create path (sling:Folder) /apps/sling/install",
+    "",
+    "set ACL for sling-jcr-install",
+    "allow\tjcr:read\ton\t/",
+    "allow\trep:write\ton /apps/sling/install",
+    "end",
+    "",
+    "#<<< SLING-5848 - Define service user and ACLs for Scripting",
+    "create service user sling-scripting",
+    "",
+    "create path (sling:Folder) /libs",
+    "create path (sling:Folder) /apps",
+    "",
+    "set ACL for sling-scripting",
+    "deny    jcr:all     on /",
+    "allow   jcr:read    on /libs,/apps",
+    "end",
+    "# SLING-5848 - Define service user and ACLs for Scripting >>>",
+    "",
+    "create path (sling:Folder) /conf",
+    "",
+    "create service user sling-discovery",
+    "",
+    "create path (sling:Folder) /var/discovery",
+    "create path (sling:Folder) /var/discovery/oak",
+    "",
+    "set ACL for sling-discovery",
+    "allow   jcr:read,rep:write    on /var/discovery",
+    "end",
+    "",
+    "create service user sling-event",
+    "",
+    "create path (sling:Folder) /var",
+    "create path (sling:Folder) /var/eventing",
+    "",
+    "set ACL for sling-event",
+    "allow   jcr:read,rep:write    on /var/eventing",
+    "end",
+    "",
+    "create service user sling-validation",
+    "",
+    "create path (sling:Folder) /apps",
+    "create path (sling:Folder) /libs",
+    "",
+    "set ACL for sling-validation",
+    "allow   jcr:read    on /apps",
+    "allow   jcr:read    on /libs",
+    "end",
+    "",
+    "create service user slingshot-service",
+    "create user slingshot1 with password slingshot1",
+    "create user slingshot2 with password slingshot2",
+    "",
+    "create path (sling:Folder) /content/slingshot",
+    "create path (sling:Folder) /content/slingshot/users",
+    "create path (sling:Folder) /content/slingshot/users/slingshot1",
+    "create path (sling:Folder) /content/slingshot/users/slingshot2",
+    "",
+    "set ACL for slingshot-service",
+    "allow   jcr:read,rep:write    on /content/slingshot",
+    "end",
+    "",
+    "set ACL for slingshot1",
+    "allow   jcr:read,rep:write    on /content/slingshot/users/slingshot1",
+    "end",
+    "",
+    "set ACL for slingshot2",
+    "allow   jcr:read,rep:write    on /content/slingshot/users/slingshot2",
+    "end"
+  ]
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/feature/starter/it/LaunchpadReadyRule.java b/src/test/java/org/apache/sling/feature/starter/it/LaunchpadReadyRule.java
new file mode 100644
index 0000000..f82bc3a
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/starter/it/LaunchpadReadyRule.java
@@ -0,0 +1,121 @@
+/*
+ * 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.feature.starter.it;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.net.ConnectException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.junit.rules.ExternalResource;
+
+public class LaunchpadReadyRule extends ExternalResource {
+
+    private static final int TRIES = 60;
+    private static final int WAIT_BETWEEN_TRIES_MILLIS = 1000;
+
+    private final List<Check> checks = new ArrayList<>();
+
+    public LaunchpadReadyRule(int launchpadPort) {
+
+        checks.add(new Check("http://localhost:" + launchpadPort + "/server/default/jcr:root/content"));
+        checks.add(new Check("http://localhost:" + launchpadPort + "/starter/index.html") {
+            @Override
+            public String runCheck(HttpResponse response) throws Exception {
+                try (InputStreamReader isr = new InputStreamReader(response.getEntity().getContent());
+                        BufferedReader reader = new BufferedReader(isr)) {
+
+                    String line;
+                    while ((line = reader.readLine()) != null) {
+                        if (line.contains("Do not remove this comment, used for Starter integration tests")) {
+                            return null;
+                        }
+                    }
+                }
+
+                return "Did not find 'ready' marker in the response body";
+            }
+        });
+    }
+
+    @Override
+    protected void before() throws Throwable {
+
+        try (CloseableHttpClient client = HttpClients.createDefault()) {
+            for (Check check : checks) {
+                runCheck(client, check);
+            }
+        }
+    }
+
+    private void runCheck(CloseableHttpClient client, Check check) throws Exception {
+
+        String lastFailure = null;
+        HttpGet get = new HttpGet(check.getUrl());
+        
+        for (int i = 0; i < TRIES; i++) {
+            try (CloseableHttpResponse response = client.execute(get)) {
+
+                if (response.getStatusLine().getStatusCode() != 200) {
+                    lastFailure = "Status code is " + response.getStatusLine();
+                    Thread.sleep(WAIT_BETWEEN_TRIES_MILLIS);
+                    continue;
+                }
+
+                lastFailure = check.runCheck(response);
+                if (lastFailure == null) {
+                    return;
+                }
+            } catch ( ConnectException e ) {
+                lastFailure = e.getClass().getName() + " : " + e.getMessage();
+            }
+
+            Thread.sleep(WAIT_BETWEEN_TRIES_MILLIS);
+        }
+        
+        throw new RuntimeException(String.format("Launchpad not ready. Failed check for URL %s with message '%s'",
+                check.getUrl(), lastFailure));
+    }
+
+    static class Check {
+        private String url;
+
+        public Check(String url) {
+            this.url = url;
+        }
+
+        public String getUrl() {
+            return url;
+        }
+
+        /**
+         * @param response the HttpResponse
+         * @return null if check check was successful, an error description otherwise
+         * @throws Exception
+         */
+        public String runCheck(HttpResponse response) throws Exception {
+            return null;
+        }
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/feature/starter/it/SmokeIT.java b/src/test/java/org/apache/sling/feature/starter/it/SmokeIT.java
new file mode 100644
index 0000000..785e29e
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/starter/it/SmokeIT.java
@@ -0,0 +1,195 @@
+/*
+ * 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.feature.starter.it;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.apache.felix.utils.json.JSONParser;
+import org.apache.http.Header;
+import org.apache.http.HttpHost;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.CredentialsProvider;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.impl.auth.BasicScheme;
+import org.apache.http.impl.client.BasicAuthCache;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.hamcrest.CoreMatchers;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+
+public class SmokeIT {
+
+    private static final int LAUNCHPAD_PORT = Integer.getInteger("launchpad.http.port", 8080);
+    private static final int EXPECTED_BUNDLES_COUNT = Integer.getInteger("IT.expected.bundles.count", Integer.MAX_VALUE);
+
+    @ClassRule
+    public static LaunchpadReadyRule LAUNCHPAD = new LaunchpadReadyRule(LAUNCHPAD_PORT);
+    private HttpClientContext httpClientContext;
+
+    @Before
+    public void prepareHttpContext() {
+
+        CredentialsProvider credsProvider = new BasicCredentialsProvider();
+        UsernamePasswordCredentials creds = new UsernamePasswordCredentials("admin", "admin");
+        credsProvider.setCredentials(new AuthScope("localhost", LAUNCHPAD_PORT), creds);
+
+        BasicAuthCache authCache = new BasicAuthCache();
+        BasicScheme basicAuth = new BasicScheme();
+        authCache.put(new HttpHost("localhost", LAUNCHPAD_PORT, "http"), basicAuth);
+
+        httpClientContext = HttpClientContext.create();
+        httpClientContext.setCredentialsProvider(credsProvider);
+        httpClientContext.setAuthCache(authCache);
+    }
+
+    private CloseableHttpClient newClient() {
+
+        return HttpClientBuilder.create()
+                .setDefaultCredentialsProvider(httpClientContext.getCredentialsProvider())
+                .build();
+    }
+
+    @Test
+    public void verifyAllBundlesStarted() throws Exception {
+
+        try ( CloseableHttpClient client = newClient() ) {
+
+            HttpGet get = new HttpGet("http://localhost:" + LAUNCHPAD_PORT + "/system/console/bundles.json");
+
+            // pass the context to ensure preemptive basic auth is used
+            // https://hc.apache.org/httpcomponents-client-ga/tutorial/html/authentication.html
+            try ( CloseableHttpResponse response = client.execute(get, httpClientContext) ) {
+
+                if ( response.getStatusLine().getStatusCode() != 200 ) {
+                    fail("Unexpected status line " + response.getStatusLine());
+                }
+
+                Header contentType = response.getFirstHeader("Content-Type");
+                assertThat("Content-Type header", contentType.getValue(), CoreMatchers.startsWith("application/json"));
+
+                Map<String, Object> obj = new JSONParser(response.getEntity().getContent()).getParsed();
+
+                @SuppressWarnings("unchecked")
+                List<Object> status = (List<Object>) obj.get("s");
+
+                @SuppressWarnings("unchecked")
+                List<Object> bundles = (List<Object>) obj.get("data");
+                if(bundles.size() < EXPECTED_BUNDLES_COUNT) {
+                    fail("Expected at least " + EXPECTED_BUNDLES_COUNT + " bundles, got " + bundles.size());
+                }
+
+                BundleStatus bs = new BundleStatus(status);
+
+                if ( bs.resolvedBundles != 0 || bs.installedBundles != 0 ) {
+
+                    StringBuilder out = new StringBuilder();
+                    out.append("Expected all bundles to be active, but instead got ")
+                        .append(bs.resolvedBundles).append(" resolved bundles, ")
+                        .append(bs.installedBundles).append(" installed bundlles: ");
+
+                    for ( int i = 0 ; i < bundles.size(); i++ ) {
+                        @SuppressWarnings("unchecked")
+                        Map<String, Object> bundle = (Map<String, Object>) bundles.get(i);
+
+                        String bundleState = (String) bundle.get("state");
+                        String bundleSymbolicName = (String) bundle.get("symbolicName");
+                        String bundleVersion = (String) bundle.get("version");
+
+                        switch ( bundleState ) {
+                            case "Active":
+                            case "Fragment":
+                                continue;
+
+                            default:
+                                out.append("\n- ").append(bundleSymbolicName).append(" ").append(bundleVersion).append(" is in state " ).append(bundleState);
+                        }
+                    }
+
+                    fail(out.toString());
+                }
+            }
+        }
+    }
+
+    @Test
+    public void ensureRepositoryIsStarted() throws Exception {
+        try ( CloseableHttpClient client = newClient() ) {
+
+            HttpGet get = new HttpGet("http://localhost:" + LAUNCHPAD_PORT + "/server/default/jcr:root/content");
+
+            try ( CloseableHttpResponse response = client.execute(get) ) {
+
+                if ( response.getStatusLine().getStatusCode() != 200 ) {
+                    fail("Unexpected status line " + response.getStatusLine());
+                }
+
+                Header contentType = response.getFirstHeader("Content-Type");
+                assertThat("Content-Type header", contentType.getValue(), equalTo("text/xml"));
+
+                DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+                dbf.setNamespaceAware(true);
+                DocumentBuilder db = dbf.newDocumentBuilder();
+                Document document = db.parse(response.getEntity().getContent());
+
+                Element docElement = document.getDocumentElement();
+                NamedNodeMap attrs = docElement.getAttributes();
+
+                Node nameAttr = attrs.getNamedItemNS("http://www.jcp.org/jcr/sv/1.0", "name");
+                assertThat("no 'name' attribute found", nameAttr, notNullValue());
+                assertThat("Invalid name attribute value", nameAttr.getNodeValue(), equalTo("content"));
+            }
+        }
+    }
+
+    static class BundleStatus {
+
+        long totalBundles;
+        long activeBundles;
+        long activeFragments;
+        long resolvedBundles;
+        long installedBundles;
+
+        public BundleStatus(List<Object> array) {
+
+            totalBundles = (Long)array.get(0);
+            activeBundles = (Long)array.get(1);
+            activeFragments = (Long)array.get(2);
+            resolvedBundles = (Long)array.get(3);
+            installedBundles = (Long)array.get(4);
+
+        }
+    }
+}
diff --git a/src/test/java/org/apache/sling/feature/starter/it/package-info.java b/src/test/java/org/apache/sling/feature/starter/it/package-info.java
new file mode 100644
index 0000000..f70e5e8
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/starter/it/package-info.java
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ */
+/**
+ * <h1>Smoke tests for the Sling Launchpad</h1>
+ * 
+ * <p>This package contains a minimal set of tests for the Sling launchpad. The
+ * tests validate that the launchpad is correctly assembled and that there are
+ * no obvious mistakes such as bundles that can't be wired. More extensive
+ * tests must be placed in specific test projects.</p>
+ * 
+ * <p>The launchpad tests don't depend on other Sling bundles,to prevent circular
+ * dependencies. As such, there is some duplication of code ( at least intent, if 
+ * not implementation ) with some of the testing projects. This is another 
+ * argument for keeping the tests minimal.</p>
+ */
+package org.apache.sling.launchpad;
\ No newline at end of file