This is an automated email from the ASF dual-hosted git repository. rombert pushed a commit to annotated tag slingstart-maven-plugin-1.1.0 in repository https://gitbox.apache.org/repos/asf/sling-slingstart-maven-plugin.git
commit b0c766a2725ce3a57f48ccfbb4a401b59d0b1949 Author: Carsten Ziegeler <[email protected]> AuthorDate: Sat Mar 14 12:44:44 2015 +0000 SLING-4474 : Provide a way to start/stop an instance through maven mojos git-svn-id: https://svn.apache.org/repos/asf/sling/trunk/tooling/maven/slingstart-maven-plugin@1666678 13f79535-47bb-0310-9956-ffa450edef68 --- .../sling/maven/slingstart/launcher/Launcher.java | 95 +++++ .../maven/slingstart/launcher/LauncherMBean.java | 34 ++ .../sling/maven/slingstart/launcher/Main.java | 105 +++++ .../maven/slingstart/run/ControlListener.java | 160 ++++++++ .../maven/slingstart/run/LauncherCallable.java | 328 +++++++++++++++ .../maven/slingstart/run/LaunchpadEnvironment.java | 140 +++++++ .../sling/maven/slingstart/run/PortHelper.java | 49 +++ .../maven/slingstart/run/ProcessDescription.java | 81 ++++ .../slingstart/run/ProcessDescriptionProvider.java | 106 +++++ .../maven/slingstart/run/ServerConfiguration.java | 157 ++++++++ .../sling/maven/slingstart/run/StartMojo.java | 444 +++++++++++++++++++++ .../sling/maven/slingstart/run/StopMojo.java | 89 +++++ 12 files changed, 1788 insertions(+) diff --git a/src/main/java/org/apache/sling/maven/slingstart/launcher/Launcher.java b/src/main/java/org/apache/sling/maven/slingstart/launcher/Launcher.java new file mode 100644 index 0000000..8b72dc0 --- /dev/null +++ b/src/main/java/org/apache/sling/maven/slingstart/launcher/Launcher.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */package org.apache.sling.maven.slingstart.launcher; + +import java.io.BufferedReader; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.ArrayList; +import java.util.List; + +public class Launcher implements LauncherMBean { + + private final int listenerPort; + + public Launcher(final int listenerPort) { + this.listenerPort = listenerPort; + } + + @Override + public void startupFinished() { + final List<String> hosts = new ArrayList<String>(); + hosts.add("localhost"); + hosts.add("127.0.0.1"); + + boolean done = false; + int index = 0; + while ( !done && index < hosts.size() ) { + final String hostName = hosts.get(index); + final int twoMinutes = 2 * 60 * 1000; + + Socket clientSocket = null; + DataOutputStream out = null; + BufferedReader in = null; + try { + clientSocket = new Socket(); + clientSocket.connect(new InetSocketAddress(hostName, listenerPort), twoMinutes); + // without that, read() call on the InputStream associated with this Socket is infinite + clientSocket.setSoTimeout(twoMinutes); + + out = new DataOutputStream(clientSocket.getOutputStream()); + in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); + out.writeBytes("started\n"); + in.readLine(); + done = true; + } catch (final Throwable ignore) { + // catch Throwable because InetSocketAddress and Socket#connect throws unchecked exceptions + // we ignore this for now + } finally { + if ( in != null ) { + try { + in.close(); + } catch ( final IOException ioe) { + // ignore + } + } + if ( out != null ) { + try { + out.close(); + } catch ( final IOException ioe) { + // ignore + } + } + if ( clientSocket != null ) { + try { + clientSocket.close(); + } catch (final IOException e) { + // ignore + } + } + } + index++; + } + } + + @Override + public void startupProgress(Float ratio) { + // nothing to do + } +} diff --git a/src/main/java/org/apache/sling/maven/slingstart/launcher/LauncherMBean.java b/src/main/java/org/apache/sling/maven/slingstart/launcher/LauncherMBean.java new file mode 100644 index 0000000..1247bfa --- /dev/null +++ b/src/main/java/org/apache/sling/maven/slingstart/launcher/LauncherMBean.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.maven.slingstart.launcher; + +/** + * The launcher MBean interface. + */ +public interface LauncherMBean { + + /** + * Notify the launcher about the finish of the startup. + */ + void startupFinished(); + + /** + * Notify the launcher about the progress of the startup. + * @param ratio Startup progress ratio + */ + void startupProgress(Float ratio); +} diff --git a/src/main/java/org/apache/sling/maven/slingstart/launcher/Main.java b/src/main/java/org/apache/sling/maven/slingstart/launcher/Main.java new file mode 100644 index 0000000..3a72704 --- /dev/null +++ b/src/main/java/org/apache/sling/maven/slingstart/launcher/Main.java @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.maven.slingstart.launcher; + +import java.io.File; +import java.lang.management.ManagementFactory; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; + +import javax.management.MBeanServer; +import javax.management.ObjectName; + +/** + * Main class for launching Apache Sling. + * + */ +public class Main { + + /** Arguments to pass to the real main class */ + private final String[] startupArgs; + + /** Verbose flag */ + private final boolean verbose; + + /** App jar */ + private final File appJar; + + /** Listener port. */ + private final int listenerPort; + + /** Main class default value */ + private final static String MAIN_CLASS_DEF = "org.apache.sling.launchpad.app.Main"; + + /** Delimeter string */ + private final static String DELIM = + "-------------------------------------------------------------------"; + + /** + * Create a new launcher + * First argument is the launchpad jar + * Second argument is the listener port + * Third argument is verbose + */ + public Main(final String[] args) { + if ( args == null || args.length < 3 ) { + throw new IllegalArgumentException("Missing configuration: " + args); + } + this.appJar = new File(args[0]); + this.listenerPort = Integer.valueOf(args[1]); + this.verbose = Boolean.valueOf(args[2]); + this.startupArgs = new String[args.length-3]; + System.arraycopy(args, 3, this.startupArgs, 0, this.startupArgs.length); + } + + /** + * Startup + */ + public void run() throws Exception { + if (verbose) { + System.out.println(DELIM); + System.out.println("Slingstart application: " + this.appJar); + System.out.println("Main class: " + MAIN_CLASS_DEF); + System.out.println("Listener Port: " + String.valueOf(this.listenerPort)); + System.out.println(DELIM); + } + + final ClassLoader cl = new URLClassLoader(new URL[] {this.appJar.toURI().toURL()}); + Thread.currentThread().setContextClassLoader(cl); + + // create and register mbean + final MBeanServer jmxServer = ManagementFactory.getPlatformMBeanServer(); + jmxServer.registerMBean(new Launcher(this.listenerPort), + new ObjectName("org.apache.sling.launchpad:type=Launcher")); + + final Class<?> mainClass = cl.loadClass(MAIN_CLASS_DEF); + final Method mainMethod = mainClass.getDeclaredMethod("main", String[].class); + mainMethod.invoke(null, (Object)this.startupArgs); + } + + public static void main(final String[] args) { + try { + final Main m = new Main(args); + m.run(); + } catch ( final Exception e) { + e.printStackTrace(); + System.exit(1); + } + } +} + diff --git a/src/main/java/org/apache/sling/maven/slingstart/run/ControlListener.java b/src/main/java/org/apache/sling/maven/slingstart/run/ControlListener.java new file mode 100644 index 0000000..9aee56a --- /dev/null +++ b/src/main/java/org/apache/sling/maven/slingstart/run/ControlListener.java @@ -0,0 +1,160 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.maven.slingstart.run; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; + +/** + * Control listener. + * This class listens for the startup of a launchpad instance. + */ +public class ControlListener implements Runnable { + + // command sent by the client to notify startup + private static final String COMMAND_STARTED = "started"; + + private static final String RESPONSE_OK = "ok"; + + // The default interface to listen on + private static final String DEFAULT_LISTEN_INTERFACE = "127.0.0.1"; + + // The port to listen on + private final int port; + + private volatile boolean started = false; + + private volatile boolean stopped = false; + + private volatile ServerSocket server; + + public ControlListener(final int p) { + this.port = p; + final Thread listener = new Thread(this); + listener.setDaemon(true); + listener.setName("Launchapd startup listener"); + listener.start(); + } + + public int getPort() { + return this.port; + } + + public boolean isStarted() { + return this.started; + } + + public void stop() { + stopped = true; + if ( server != null ) { + try { + server.close(); + } catch (IOException e) { + // ignore + } + } + } + + /** + * Implements the server thread receiving commands from clients and acting + * upon them. + */ + @Override + public void run() { + final InetSocketAddress socketAddress = getSocketAddress(this.port); + try { + server = new ServerSocket(); + server.bind(socketAddress); + } catch (final IOException ioe) { + return; + } + + try { + while (!stopped) { + + final Socket s = server.accept(); + + try { + final String commandLine = readLine(s); + if (commandLine == null) { + final String msg = "ERR: missing command"; + writeLine(s, msg); + continue; + } + + final String command = commandLine; + + if (COMMAND_STARTED.equals(command)) { + writeLine(s, RESPONSE_OK); + this.started = true; + this.stopped = true; + break; + + } else { + final String msg = "ERR:" + command; + writeLine(s, msg); + + } + } finally { + try { + s.close(); + } catch (IOException ignore) { + } + } + } + } catch (final IOException ioe) { + // ignore + } finally { + try { + server.close(); + } catch (final IOException ignore) { + // ignore + } + } + } + + private String readLine(final Socket socket) throws IOException { + final BufferedReader br = new BufferedReader(new InputStreamReader( + socket.getInputStream(), "UTF-8")); + return br.readLine(); + } + + private void writeLine(final Socket socket, final String line) throws IOException { + final BufferedWriter bw = new BufferedWriter(new OutputStreamWriter( + socket.getOutputStream(), "UTF-8")); + bw.write(line); + bw.write("\r\n"); + bw.flush(); + } + + private static InetSocketAddress getSocketAddress(final int port) { + final String address = DEFAULT_LISTEN_INTERFACE; + + final InetSocketAddress addr = new InetSocketAddress(address, port); + if (!addr.isUnresolved()) { + return addr; + } + + return null; + } +} diff --git a/src/main/java/org/apache/sling/maven/slingstart/run/LauncherCallable.java b/src/main/java/org/apache/sling/maven/slingstart/run/LauncherCallable.java new file mode 100644 index 0000000..6ac2b13 --- /dev/null +++ b/src/main/java/org/apache/sling/maven/slingstart/run/LauncherCallable.java @@ -0,0 +1,328 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.maven.slingstart.run; + +import java.io.BufferedReader; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.LineNumberReader; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; + +import org.apache.commons.io.IOUtils; +import org.apache.maven.plugin.logging.Log; +import org.apache.sling.maven.slingstart.launcher.Main; + +/** + * A callable for launchpad an instance + */ +public class LauncherCallable implements Callable<ProcessDescription> { + + private final LaunchpadEnvironment environment; + private final ServerConfiguration configuration; + private final Log logger; + + public LauncherCallable(final Log logger, + final ServerConfiguration configuration, + final LaunchpadEnvironment environment) { + this.logger = logger; + this.configuration = configuration; + this.environment = environment; + } + + /** + * @see java.util.concurrent.Callable#call() + */ + @Override + public ProcessDescription call() throws Exception { + + // fail if launchpad with this id is already started + if (!ProcessDescriptionProvider.getInstance().isRunConfigurationAvailable(configuration.getId())) { + throw new Exception("Launchpad with id " + configuration.getId() + " is not available"); + } + + // get the launchpad jar + final File launchpad = this.environment.prepare(this.configuration.getFolder()); + + // Lock the launchpad id + final String launchpadKey = ProcessDescriptionProvider.getInstance().getId(configuration.getId()); + + // start launchpad + ProcessDescription cfg = this.start(launchpad); + + // Add thread hook to shutdown launchpad + if (environment.isShutdownOnExit()) { + cfg.installShutdownHook(); + } + + // Add configuration to the config provider + ProcessDescriptionProvider.getInstance().addRunConfiguration(cfg, launchpadKey); + + boolean started = false; + try { + final long endTime = System.currentTimeMillis() + this.environment.getReadyTimeOutSec() * 1000; + boolean finished = false; + while ( !started && !finished && System.currentTimeMillis() < endTime ) { + Thread.sleep(5000); + started = cfg.getControlListener().isStarted(); + try { + // if we get an exit value, the process has stopped + cfg.getProcess().exitValue(); + finished = true; + } catch ( final IllegalThreadStateException itse) { + // everything as expected + } + } + + if ( finished ) { + throw new Exception("Launchpad did exit unexpectedly."); + } + if ( !started ) { + throw new Exception("Launchpad did not start successfully in " + this.environment.getReadyTimeOutSec() + " seconds."); + } + this.logger.info("Started Launchpad " + configuration.getId() + + " [" + configuration.getRunmode() + ", " + configuration.getPort() + "]"); + } finally { + // stop control port + cfg.getControlListener().stop(); + + // call launchpad stop routine if not properly started + if (!started) { + stop(this.logger, cfg); + ProcessDescriptionProvider.getInstance().removeRunConfiguration(cfg.getId()); + cfg = null; + } + } + + return cfg; + } + + public boolean isRunning() { + return getControlPortFile(this.configuration.getFolder()).exists(); + } + + private void add(final List<String> args, final String value) { + if ( value != null ) { + final String[] single = value.trim().split(" "); + for(final String v : single) { + if ( v.trim().length() > 0 ) { + args.add(v.trim()); + } + } + } + } + + private ProcessDescription start(final File jar) throws Exception { + final ProcessDescription cfg = new ProcessDescription(this.configuration.getId(), this.configuration.getFolder()); + + final ProcessBuilder builder = new ProcessBuilder(); + final List<String> args = new ArrayList<String>(); + + args.add("java"); + add(args, this.configuration.getVmOpts()); + + args.add("-cp"); + args.add("bin"); + args.add(Main.class.getName()); + // first three arguments: jar, listener port, verbose + args.add(jar.getPath()); + args.add(String.valueOf(cfg.getControlListener().getPort())); + args.add("true"); + + // from here on launchpad properties + add(args, this.configuration.getOpts()); + + final String contextPath = this.configuration.getContextPath(); + if ( contextPath != null && contextPath.length() > 0 && !contextPath.equals("/") ) { + args.add("-r"); + args.add(contextPath); + } + + if ( this.configuration.getPort() != null ) { + args.add("-p"); + args.add(this.configuration.getPort()); + } + + if ( this.configuration.getRunmode() != null ) { + args.add("-Dsling.run.modes=" + this.configuration.getRunmode()); + } + + builder.command(args.toArray(new String[args.size()])); + builder.directory(this.configuration.getFolder()); + builder.redirectErrorStream(true); +// builder.redirectOutput(Redirect.INHERIT); +// builder.redirectError(Redirect.INHERIT); + + logger.info("Starting Launchpad " + this.configuration.getId() + "..."); + logger.debug("Launchpad cmd: " + builder.command()); + logger.debug("Launchpad dir: " + builder.directory()); + + try { + cfg.setProcess(builder.start()); + } catch (final IOException e) { + if (cfg.getProcess() != null) { + cfg.getProcess().destroy(); + cfg.setProcess(null); + } + throw new Exception("Could not start the Launchpad", e); + } + + return cfg; + } + + public static void stop(final Log LOG, final ProcessDescription cfg) throws Exception { + boolean isNew = false; + + if (cfg.getProcess() != null || isNew ) { + LOG.info("Stopping Launchpad " + cfg.getId()); + boolean destroy = true; + final int twoMinutes = 2 * 60 * 1000; + final File controlPortFile = getControlPortFile(cfg.getDirectory()); + LOG.debug("Control port file " + controlPortFile + " exists: " + controlPortFile.exists()); + if ( controlPortFile.exists() ) { + // reading control port + int controlPort = -1; + String secretKey = null; + LineNumberReader lnr = null; + String serverName = null; + try { + lnr = new LineNumberReader(new FileReader(controlPortFile)); + final String portLine = lnr.readLine(); + final int pos = portLine.indexOf(':'); + controlPort = Integer.parseInt(portLine.substring(pos + 1)); + if ( pos > 0 ) { + serverName = portLine.substring(0, pos); + } + secretKey = lnr.readLine(); + } catch ( final NumberFormatException ignore) { + // we ignore this + LOG.debug("Error reading control port file " + controlPortFile, ignore); + } catch ( final IOException ignore) { + // we ignore this + LOG.debug("Error reading control port file " + controlPortFile, ignore); + } finally { + IOUtils.closeQuietly(lnr); + } + + if ( controlPort != -1 ) { + final List<String> hosts = new ArrayList<String>(); + if ( serverName != null ) { + hosts.add(serverName); + } + hosts.add("localhost"); + hosts.add("127.0.0.1"); + LOG.debug("Found control port " + controlPort); + int index = 0; + while ( destroy && index < hosts.size() ) { + final String hostName = hosts.get(index); + + Socket clientSocket = null; + DataOutputStream out = null; + BufferedReader in = null; + try { + LOG.debug("Trying to connect to " + hostName + ":" + controlPort); + clientSocket = new Socket(); + // set a socket timeout + clientSocket.connect(new InetSocketAddress(hostName, controlPort), twoMinutes); + // without that, read() call on the InputStream associated with this Socket is infinite + clientSocket.setSoTimeout(twoMinutes); + + LOG.debug(hostName + ":" + controlPort + " connection estabilished, sending the 'stop' command..."); + + out = new DataOutputStream(clientSocket.getOutputStream()); + in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); + if (secretKey != null) { + out.writeBytes(secretKey); + out.write(' '); + } + out.writeBytes("stop\n"); + in.readLine(); + destroy = false; + LOG.debug("'stop' command sent to " + hostName + ":" + controlPort); + } catch (final Throwable ignore) { + // catch Throwable because InetSocketAddress and Socket#connect throws unchecked exceptions + // we ignore this for now + LOG.debug("Error sending 'stop' command to " + hostName + ":" + controlPort + " due to: " + ignore.getMessage()); + } finally { + IOUtils.closeQuietly(in); + IOUtils.closeQuietly(out); + IOUtils.closeQuietly(clientSocket); + } + index++; + } + } + } + if ( cfg.getProcess() != null ) { + final Process process = cfg.getProcess(); + + if (!destroy) { + // as shutdown might block forever, we use a timeout + final long now = System.currentTimeMillis(); + final long end = now + twoMinutes; + + LOG.debug("Waiting for process to stop..."); + + while (isAlive(process) && (System.currentTimeMillis() < end)) { + try { + Thread.sleep(2500); + } catch (InterruptedException e) { + // ignore + } + } + if (isAlive( process)) { + LOG.debug("Process timeout out after 2 minutes"); + destroy = true; + } else { + LOG.debug("Process stopped"); + } + } + + if (destroy) { + LOG.debug("Destroying process..."); + process.destroy(); + LOG.debug("Process destroyed"); + } + + cfg.setProcess(null); + } + } else { + LOG.warn("Launchpad already stopped"); + } + } + + private static boolean isAlive(Process process) { + try { + process.exitValue(); + return false; + } catch (IllegalThreadStateException e) { + return true; + } + } + + private static File getControlPortFile(final File directory) { + final File launchpadDir = new File(directory, LaunchpadEnvironment.WORK_DIR_NAME); + final File confDir = new File(launchpadDir, "conf"); + final File controlPortFile = new File(confDir, "controlport"); + return controlPortFile; + } +} \ No newline at end of file diff --git a/src/main/java/org/apache/sling/maven/slingstart/run/LaunchpadEnvironment.java b/src/main/java/org/apache/sling/maven/slingstart/run/LaunchpadEnvironment.java new file mode 100644 index 0000000..cca6ac4 --- /dev/null +++ b/src/main/java/org/apache/sling/maven/slingstart/run/LaunchpadEnvironment.java @@ -0,0 +1,140 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.maven.slingstart.run; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.codehaus.plexus.util.FileUtils; + +/** + * Common settings for all launchpad instances. + */ +public class LaunchpadEnvironment { + + /** The work directory created by starting launchpad. */ + public static final String WORK_DIR_NAME = "sling"; + + private final File launchpadJar; + private final boolean cleanWorkingDirectory; + private final boolean shutdownOnExit; + private final int readyTimeOutSec; + + public LaunchpadEnvironment(final File launchpadJar, + final boolean cleanWorkingDirectory, + final boolean shutdownOnExit, + final int readyTimeOutSec) { + this.launchpadJar = launchpadJar; + this.cleanWorkingDirectory = cleanWorkingDirectory; + this.shutdownOnExit = shutdownOnExit; + this.readyTimeOutSec = readyTimeOutSec; + } + + public boolean isShutdownOnExit() { + return this.shutdownOnExit; + } + + public int getReadyTimeOutSec() { + return this.readyTimeOutSec; + } + + /** + * Check if the launchpad folder exists. + */ + private void ensureFolderExists(final File folder) { + if (!folder.exists()) { + folder.mkdirs(); + } + if (this.cleanWorkingDirectory) { + final File work = new File(folder, WORK_DIR_NAME); + org.apache.commons.io.FileUtils.deleteQuietly(work); + } + } + + private File installLaunchpad(final File folder) throws IOException { + if (this.launchpadJar.getParentFile().getAbsolutePath().equals(folder.getAbsolutePath())) { + return this.launchpadJar; + } + try { + FileUtils.copyFileToDirectory(this.launchpadJar, folder); + return new File(folder, this.launchpadJar.getName()); + } catch (final IOException ioe) { + throw new IOException("Unable to copy " + this.launchpadJar + " to " + folder, ioe); + } + } + + private void installLauncher(final File folder) throws IOException { + final File binDir = new File(folder, "bin"); + copyResource("org/apache/sling/maven/slingstart/launcher/Launcher.class", binDir); + copyResource("org/apache/sling/maven/slingstart/launcher/LauncherMBean.class", binDir); + copyResource("org/apache/sling/maven/slingstart/launcher/Main.class", binDir); + } + + /** + * Prepare a new instance. + * @param folder The target folder for the instance + * @return The launchpad jar + * @throws IOException if an error occurs. + */ + public File prepare(final File folder) throws IOException { + this.ensureFolderExists(folder); + + // copy launchpadJar + final File launchpad = this.installLaunchpad(folder); + + // install launcher + this.installLauncher(folder); + + return launchpad; + } + + private void copyResource(final String resource, + final File dir) + throws IOException { + final int lastSlash = resource.lastIndexOf('/'); + final File baseDir; + if ( lastSlash > 0 ) { + final String filePath = resource.substring(0, lastSlash).replace('/', File.separatorChar); + baseDir = new File(dir, filePath); + } else { + baseDir = dir; + } + baseDir.mkdirs(); + final File file = new File(baseDir, resource.substring(lastSlash + 1)); + final InputStream is = LaunchpadEnvironment.class.getClassLoader().getResourceAsStream(resource); + if ( is == null ) { + throw new IOException("Resource not found: " + resource); + } + final FileOutputStream fos = new FileOutputStream(file); + final byte[] buffer = new byte[2048]; + int l; + try { + while ( (l = is.read(buffer)) > 0 ) { + fos.write(buffer, 0, l); + } + } finally { + if ( fos != null ) { + fos.close(); + } + if ( is != null ) { + is.close(); + } + } + } +} diff --git a/src/main/java/org/apache/sling/maven/slingstart/run/PortHelper.java b/src/main/java/org/apache/sling/maven/slingstart/run/PortHelper.java new file mode 100644 index 0000000..d6aa33d --- /dev/null +++ b/src/main/java/org/apache/sling/maven/slingstart/run/PortHelper.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.maven.slingstart.run; + +import java.io.IOException; +import java.net.ServerSocket; +import java.util.HashSet; +import java.util.Set; + +import org.apache.maven.plugin.MojoExecutionException; + +/** + * Simple helper class to find a new port. + */ +public class PortHelper { + + private static final Set<Integer> USED_PORTS = new HashSet<Integer>(); + + public static synchronized int getNextAvailablePort() + throws MojoExecutionException { + int unusedPort = 0; + do { + try { + final ServerSocket socket = new ServerSocket( 0 ); + unusedPort = socket.getLocalPort(); + socket.close(); + } catch ( final IOException e ) { + throw new MojoExecutionException( "Error getting an available port from system", e ); + } + } while ( USED_PORTS.contains(unusedPort)); + USED_PORTS.add(unusedPort); + + return unusedPort; + } +} diff --git a/src/main/java/org/apache/sling/maven/slingstart/run/ProcessDescription.java b/src/main/java/org/apache/sling/maven/slingstart/run/ProcessDescription.java new file mode 100644 index 0000000..4aff1c4 --- /dev/null +++ b/src/main/java/org/apache/sling/maven/slingstart/run/ProcessDescription.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.maven.slingstart.run; + +import java.io.File; + +import org.apache.maven.plugin.MojoExecutionException; + +/** + * A running launchpad process. + */ +public class ProcessDescription { + + private final String id; + private final File directory; + private final ControlListener listener; + private volatile Process process; + + public ProcessDescription(final String id, final File directory) throws MojoExecutionException { + this.id = id; + this.directory = directory; + this.listener = new ControlListener(PortHelper.getNextAvailablePort()); + } + + public String getId() { + return id; + } + + public File getDirectory() { + return directory; + } + + public ControlListener getControlListener() { + return this.listener; + } + + public Process getProcess() { + return process; + } + + public void setProcess(final Process process) { + this.process = process; + } + + /** + * Install a shutdown hook + */ + public void installShutdownHook() { + final ProcessDescription cfg = this; + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + if ( cfg.getProcess() != null ) { + System.out.println("Terminating launchpad " + cfg.getId()); + cfg.getProcess().destroy(); + cfg.setProcess(null); + } + } + }); + } + + @Override + public String toString() { + return "RunningProcessDescription [id=" + id + ", directory=" + + directory + ", process=" + process + "]"; + } +} \ No newline at end of file diff --git a/src/main/java/org/apache/sling/maven/slingstart/run/ProcessDescriptionProvider.java b/src/main/java/org/apache/sling/maven/slingstart/run/ProcessDescriptionProvider.java new file mode 100644 index 0000000..7b28219 --- /dev/null +++ b/src/main/java/org/apache/sling/maven/slingstart/run/ProcessDescriptionProvider.java @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.maven.slingstart.run; + +import java.util.HashMap; +import java.util.Map; + +/** + * A singleton which is responsible to provide {@link ProcessDescription}s + */ +public class ProcessDescriptionProvider { + + private static final String DEFAULT_KEY = "DEFAULT_LAUNCHPAD"; + + private static ProcessDescriptionProvider ourInstance = new ProcessDescriptionProvider(); + private final Map<String, ProcessDescription> configs = new HashMap<String, ProcessDescription>(); + private final Map<String, String> lockedIds = new HashMap<String, String>(); + + private ProcessDescriptionProvider() { + // private constructor + } + + public static ProcessDescriptionProvider getInstance() { + return ourInstance; + } + + /** + * Prepare an ID for a launchpad that will be started, before saving the config. + * @param launchpadId the id of the launchpad to lock + * @return id key used to add to configs + */ + public synchronized String getId(final String launchpadId) throws Exception { + final String id = (launchpadId == null ? DEFAULT_KEY : launchpadId); + if (configs.containsKey(id) || lockedIds.containsKey(id)) { + throw new Exception("Launchpad Id " + id + " is already in use"); + } + + String ts = String.valueOf(System.currentTimeMillis()); + lockedIds.put(id, ts); + return ts; + } + + /** + * + * @param launchpadId + * @param unlockKey + * @return + */ + public synchronized boolean cancelId(final String launchpadId, final String unlockKey) { + final String id = (launchpadId == null ? DEFAULT_KEY : launchpadId); + if (lockedIds.containsKey(id) && lockedIds.get(id).equals(unlockKey)) { + lockedIds.remove(id); + return true; + } else { + return false; + } + } + + /** + * + * @param launchpadId + * @return + */ + public synchronized ProcessDescription getRunConfiguration(final String launchpadId) { + final String id = (launchpadId == null ? DEFAULT_KEY : launchpadId); + return configs.get(id); + } + + /** + * + * @param launchpadId + * @return + */ + public synchronized boolean isRunConfigurationAvailable(final String launchpadId) { + return getRunConfiguration(launchpadId) == null && !lockedIds.containsKey(launchpadId); + } + + public synchronized void addRunConfiguration(ProcessDescription cfg, final String unlockKey) throws Exception { + String id = cfg.getId() == null ? DEFAULT_KEY : cfg.getId(); + if (!lockedIds.containsKey(id) || !lockedIds.get(id).equals(unlockKey)) { + throw new Exception("Cannot add configuration. Id " + id + " doesn't exist"); + } + lockedIds.remove(cfg.getId()); + configs.put(cfg.getId(), cfg); + } + + public synchronized void removeRunConfiguration(final String launchpadId) { + final String id = (launchpadId == null ? DEFAULT_KEY : launchpadId); + configs.remove(id); + } + +} diff --git a/src/main/java/org/apache/sling/maven/slingstart/run/ServerConfiguration.java b/src/main/java/org/apache/sling/maven/slingstart/run/ServerConfiguration.java new file mode 100644 index 0000000..7b8ccd8 --- /dev/null +++ b/src/main/java/org/apache/sling/maven/slingstart/run/ServerConfiguration.java @@ -0,0 +1,157 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.maven.slingstart.run; + +import java.io.File; +import java.io.Serializable; + +/** + * A server configuration + */ +public class ServerConfiguration implements Serializable { + + private static final long serialVersionUID = 1922175510880318125L; + + private static final String DEFAULT_VM_OPTS = "-Xmx1024m -XX:MaxPermSize=256m -Djava.awt.headless=true"; + + /** The unique id. */ + private String id; + + /** The run mode string. */ + private String runmode; + + /** The port to use. */ + private String port; + + /** The context path. */ + private String contextPath; + + /** The vm options. */ + private String vmOpts = DEFAULT_VM_OPTS; + + /** Additional application options. */ + private String opts; + + /** Number of instances. */ + private int instances = 1; + + /** The folder to use. */ + private File folder; + + /** + * Get the instance id + * @return The instance id + */ + public String getId() { + return id; + } + + /** + * Set the instance id + * @param id New instance id + */ + public void setId(String id) { + this.id = id; + } + + public String getRunmode() { + return runmode; + } + + public void setRunmode(final String runmode) { + this.runmode = runmode; + } + + public String getPort() { + return port; + } + + public void setPort(final String port) { + this.port = port; + } + + public String getContextPath() { + return contextPath; + } + + public void setContextPath(final String contextPath) { + this.contextPath = contextPath; + } + + public String getVmOpts() { + return vmOpts; + } + + public void setVmOpts(final String vmOpts) { + this.vmOpts = vmOpts; + } + + public String getOpts() { + return opts; + } + + public void setOpts(final String opts) { + this.opts = opts; + } + + public int getInstances() { + return this.instances; + } + + public void setInstances(final int value) { + this.instances = value; + } + + public File getFolder() { + return folder; + } + + public void setFolder(final File folder) { + this.folder = folder.getAbsoluteFile(); + } + + /** + * Get the server + * @return The server + */ + public String getServer() { + // hard coded for now + return "localhost"; + } + + public ServerConfiguration copy() { + final ServerConfiguration copy = new ServerConfiguration(); + // we do not copy the id + copy.setRunmode(this.getRunmode()); + copy.setPort(this.getPort()); + copy.setContextPath(this.getContextPath()); + copy.setVmOpts(this.getVmOpts()); + copy.setOpts(this.getOpts()); + copy.setInstances(1); + copy.setFolder(this.getFolder()); + + return copy; + } + + @Override + public String toString() { + return "LaunchpadConfiguration [id=" + id + ", runmode=" + runmode + + ", port=" + port + ", contextPath=" + contextPath + + ", vmOpts=" + vmOpts + ", opts=" + opts + ", instances=" + + instances + ", folder=" + folder + "]"; + } +} diff --git a/src/main/java/org/apache/sling/maven/slingstart/run/StartMojo.java b/src/main/java/org/apache/sling/maven/slingstart/run/StartMojo.java new file mode 100644 index 0000000..e496d58 --- /dev/null +++ b/src/main/java/org/apache/sling/maven/slingstart/run/StartMojo.java @@ -0,0 +1,444 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.maven.slingstart.run; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.maven.artifact.Artifact; +import org.apache.maven.artifact.DefaultArtifact; +import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager; +import org.apache.maven.artifact.resolver.ArtifactNotFoundException; +import org.apache.maven.artifact.resolver.ArtifactResolutionException; +import org.apache.maven.artifact.resolver.ArtifactResolver; +import org.apache.maven.artifact.versioning.VersionRange; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.model.Dependency; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Component; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; +import org.apache.sling.maven.slingstart.BuildConstants; + +/** + * Mojo for starting launchpad. + */ +@Mojo( + name = "start", + defaultPhase = LifecyclePhase.PRE_INTEGRATION_TEST, + threadSafe = true + ) +public class StartMojo extends AbstractMojo { + + /** + * Set this to "true" to skip starting the launchpad + * + */ + @Parameter(property = "launchpad.skip", defaultValue = "false") + protected boolean skipLaunchpad; + + /** + * Parameter containing the list of server configurations + */ + @Parameter + private List<ServerConfiguration> servers; + + /** + * Ready timeout in seconds. If the launchpad has not been started in this + * time, it's assumed that the startup failed. + */ + @Parameter(property = "launchpad.ready.timeout", defaultValue = "600") + private int launchpadReadyTimeOutSec; + + /** + * The launchpad jar. This option has precedence over "launchpadDependency". + */ + @Parameter(property = "launchpad.jar") + private File launchpadJar; + + /** + * The launchpad jar as a dependency. This is only used if "launchpadJar" is not + * specified. + */ + @Parameter + private Dependency launchpadDependency; + + /** + * Clean the working directory before start. + */ + @Parameter(property = "launchpad.clean.workdir", defaultValue = "false") + private boolean cleanWorkingDirectory; + + /** + * Keep the launchpad running. + */ + @Parameter(property = "launchpad.keep.running", defaultValue = "false") + private boolean keepLaunchpadRunning; + + /** + * Set the execution of launchpad instances to be run in parallel (threads) + */ + @Parameter(property = "launchpad.parallelExecution", defaultValue = "true") + private boolean parallelExecution; + + /** + * The system properties file will contain all started instances with their ports etc. + */ + @Parameter(defaultValue = "${project.build.directory}/launchpad-runner.properties") + protected File systemPropertiesFile; + + /** + * The Maven project. + */ + @Parameter(property = "project", readonly = true, required = true) + private MavenProject project; + + /** + * The Maven session. + */ + @Parameter(property = "session", readonly = true, required = true) + private MavenSession mavenSession; + + @Component + private ArtifactHandlerManager artifactHandlerManager; + + /** + * Used to look up Artifacts in the remote repository. + * + */ + @Component + private ArtifactResolver resolver; + + /** + * Get a resolved Artifact from the coordinates provided + * + * @return the artifact, which has been resolved. + * @throws MojoExecutionException + */ + private Artifact getArtifact(final Dependency d) + throws MojoExecutionException { + final Artifact prjArtifact = new DefaultArtifact(d.getGroupId(), + d.getArtifactId(), + VersionRange.createFromVersion(d.getVersion()), + d.getScope(), + d.getType(), + d.getClassifier(), + this.artifactHandlerManager.getArtifactHandler(d.getType())); + try { + this.resolver.resolve(prjArtifact, this.project.getRemoteArtifactRepositories(), this.mavenSession.getLocalRepository()); + } catch (final ArtifactResolutionException e) { + throw new MojoExecutionException("Unable to get artifact for " + d, e); + } catch (ArtifactNotFoundException e) { + throw new MojoExecutionException("Unable to get artifact for " + d, e); + } + + return prjArtifact; + } + + /** + * @see org.apache.maven.plugin.Mojo#execute() + */ + @Override + public void execute() throws MojoExecutionException, MojoFailureException { + if (this.skipLaunchpad) { + this.getLog().info("Executing of the start launchpad mojo is disabled by configuration."); + return; + } + + // delete properties + if ( systemPropertiesFile != null && systemPropertiesFile.exists() ) { + FileUtils.deleteQuietly(this.systemPropertiesFile); + } + + // get configurations + final Collection<ServerConfiguration> configurations = getLaunchpadConfigurations(); + + // create the common environment + final LaunchpadEnvironment env = new LaunchpadEnvironment(this.findLaunchpadJar(), + this.cleanWorkingDirectory, + !this.keepLaunchpadRunning, + this.launchpadReadyTimeOutSec); + + // create callables + final Collection<LauncherCallable> tasks = new LinkedList<LauncherCallable>(); + + for (final ServerConfiguration launchpadConfiguration : configurations) { + validateConfiguration(launchpadConfiguration); + + tasks.add(createTask(launchpadConfiguration, env)); + } + + // create the launchpad runner properties + this.createLaunchpadRunnerProperties(configurations); + + if (parallelExecution) { + // ExecutorService for starting launchpad instances in parallel + final ExecutorService executor = Executors.newCachedThreadPool(); + try { + final List<Future<ProcessDescription>> resultsCollector = executor.invokeAll(tasks); + for (final Future<ProcessDescription> future : resultsCollector) { + try { + if (null == future.get()) { + throw new MojoExecutionException("Cannot start all the instances"); + } + } catch (final ExecutionException e) { + throw new MojoExecutionException(e.getLocalizedMessage(), e); + } + } + } catch ( final InterruptedException e) { + throw new MojoExecutionException(e.getLocalizedMessage(), e); + } + } else { + for (final LauncherCallable task : tasks) { + try { + if (null == task.call()) { + throw new MojoExecutionException("Cannot start all the instances"); + } + } catch (final Exception e) { + throw new MojoExecutionException(e.getLocalizedMessage(), e); + } + } + } + if (this.keepLaunchpadRunning) { + getLog().info("Press CTRL-C to stop launchpad instance(s)..."); + while ( true && this.isRunning(tasks)) { + try { + Thread.sleep(5000); + } catch (final InterruptedException ie) { + break; + } + } + } + } + + /** + * Are all launchpads still running? + */ + private boolean isRunning(final Collection<LauncherCallable> tasks) { + for(final LauncherCallable task : tasks) { + if ( !task.isRunning() ) { + return false; + } + } + return true; + } + + private void createLaunchpadRunnerProperties(final Collection<ServerConfiguration> configurations) + throws MojoExecutionException { + // create properties + OutputStream writer = null; + final Properties props = new Properties(); + try { + writer = new FileOutputStream(this.systemPropertiesFile); + + // disable sling startup check + props.put("launchpad.skip.startupcheck", "true"); + + // write out all instances + int index = 0; + for (final ServerConfiguration launchpadConfiguration : configurations) { + index++; + props.put("launchpad.instance.id." + String.valueOf(index), launchpadConfiguration.getId()); + String runMode = launchpadConfiguration.getRunmode(); + if ( runMode == null ) { + runMode = ""; + } + props.put("launchpad.instance.runmode." + String.valueOf(index), runMode); + props.put("launchpad.instance.server." + String.valueOf(index), launchpadConfiguration.getServer()); + props.put("launchpad.instance.port." + String.valueOf(index), launchpadConfiguration.getPort()); + props.put("launchpad.instance.contextPath." + String.valueOf(index), launchpadConfiguration.getContextPath()); + final String url = createServerUrl(launchpadConfiguration); + props.put("launchpad.instance.url." + String.valueOf(index), url); + } + props.put("launchpad.instances", String.valueOf(index)); + + props.store(writer, null); + } catch (final IOException e) { + throw new MojoExecutionException(e.getLocalizedMessage(), e); + } finally { + IOUtils.closeQuietly(writer); + } + } + + private static String createServerUrl(final ServerConfiguration qc) { + final StringBuilder sb = new StringBuilder(); + sb.append("http://"); + sb.append(qc.getServer()); + if ( !qc.getPort().equals("80") ) { + sb.append(':'); + sb.append(qc.getPort()); + } + final String contextPath = qc.getContextPath(); + if ( contextPath != null && contextPath.trim().length() > 0 && !contextPath.equals("/") ) { + if ( !contextPath.startsWith("/") ) { + sb.append('/'); + } + if ( contextPath.endsWith("/") ) { + sb.append(contextPath, 0, contextPath.length()-1); + } else { + sb.append(contextPath); + } + } + return sb.toString(); + } + + /** + * @param launchpadConfiguration + */ + private LauncherCallable createTask(final ServerConfiguration launchpadConfiguration, + final LaunchpadEnvironment env) + throws MojoExecutionException, MojoFailureException { + final String id = launchpadConfiguration.getId(); + getLog().debug(new StringBuilder("Starting ").append(id). + append(" with runmode ").append(launchpadConfiguration.getRunmode()). + append(" on port ").append(launchpadConfiguration.getPort()). + append(" in folder ").append(launchpadConfiguration.getFolder().getAbsolutePath()).toString()); + + // create task + return new LauncherCallable(this.getLog(), launchpadConfiguration, env); + + } + + /** + * Validate a configuration + * @param launchpadConfiguration The launchpad configuration + * @throws MojoExecutionException + */ + private void validateConfiguration(final ServerConfiguration launchpadConfiguration) + throws MojoExecutionException { + if ( launchpadConfiguration.getPort() == null ) { + launchpadConfiguration.setPort(String.valueOf(PortHelper.getNextAvailablePort())); + } + + // set the id of the launchpad + if ( launchpadConfiguration.getId() == null || launchpadConfiguration.getId().trim().length() == 0 ) { + String runMode = launchpadConfiguration.getRunmode(); + if ( runMode == null ) { + runMode = "_"; + } + final String id = new StringBuilder(runMode.replace(',', '_')).append('-').append(launchpadConfiguration.getPort()).toString(); + launchpadConfiguration.setId(id); + } + + // populate folder if not set + if (launchpadConfiguration.getFolder() == null) { + final File folder = new File(new StringBuilder(this.project.getBuild().getDirectory()).append('/').append(launchpadConfiguration.getId()).toString()); + launchpadConfiguration.setFolder(folder); + } + // context path should not be null + if ( launchpadConfiguration.getContextPath() == null ) { + launchpadConfiguration.setContextPath(""); + } + + if ( launchpadConfiguration.getInstances() < 0 ) { + launchpadConfiguration.setInstances(1); + } + } + + /** + * Finds the launchpad.jar artifact of the project being built. + * + * @return the launchpad.jar artifact + * @throws MojoFailureException if a launchpad.jar artifact was not found + */ + private File findLaunchpadJar() throws MojoFailureException, MojoExecutionException { + + // If a launchpad JAR is specified, use it + if (launchpadJar != null) { + return launchpadJar; + } + + // If a launchpad dependency is configured, resolve it + if (launchpadDependency != null) { + return getArtifact(launchpadDependency).getFile(); + } + + // If the current project is a slingstart project, use its JAR artifact + if (this.project.getPackaging().equals(BuildConstants.PACKAGING_SLINGSTART)) { + final File jarFile = new File(this.project.getBuild().getDirectory(), this.project.getBuild().getFinalName() + ".jar"); + if (jarFile.exists()) { + return jarFile; + } + } + + // Last chance: use the first declared dependency with type "slingstart" + final Set<Artifact> dependencies = this.project.getDependencyArtifacts(); + for (final Artifact dep : dependencies) { + if (BuildConstants.PACKAGING_SLINGSTART.equals(dep.getType())) { + final Dependency d = new Dependency(); + d.setGroupId(dep.getGroupId()); + d.setArtifactId(dep.getArtifactId()); + d.setVersion(dep.getVersion()); + d.setScope(Artifact.SCOPE_RUNTIME); + d.setType(BuildConstants.TYPE_JAR); + return getArtifact(d).getFile(); + } + } + + // Launchpad has not been found, throw an exception + throw new MojoFailureException("Could not find the launchpad jar. " + + "Either specify the 'launchpadJar' configuration or use this inside a slingstart project."); + } + + /** + * Get all configurations + * @return Collection of configurations. + */ + private Collection<ServerConfiguration> getLaunchpadConfigurations() { + final List<ServerConfiguration> configs = new ArrayList<ServerConfiguration>(); + if ( this.servers != null && !this.servers.isEmpty() ) { + for(final ServerConfiguration config : this.servers) { + // if instances is set to 0, no instance is added + if ( config.getInstances() != 0 ) { + configs.add(config); + for(int i=2; i<=config.getInstances();i++) { + final ServerConfiguration replicaConfig = config.copy(); + replicaConfig.setPort(null); + final File folder = replicaConfig.getFolder(); + if ( folder != null ) { + replicaConfig.setFolder(new File(folder.getParentFile(), folder.getName() + '-' + String.valueOf(i))); + } + configs.add(replicaConfig); + } + config.setInstances(1); + } + } + } else { + // use single default instance + configs.add(new ServerConfiguration()); + } + return configs; + } +} diff --git a/src/main/java/org/apache/sling/maven/slingstart/run/StopMojo.java b/src/main/java/org/apache/sling/maven/slingstart/run/StopMojo.java new file mode 100644 index 0000000..0c58f2a --- /dev/null +++ b/src/main/java/org/apache/sling/maven/slingstart/run/StopMojo.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.maven.slingstart.run; + +import java.io.FileReader; +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import org.apache.commons.io.IOUtils; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; + +/** + * Stops the running launchpad instances. + * + */ +@Mojo( + name = "stop", + defaultPhase = LifecyclePhase.POST_INTEGRATION_TEST, + threadSafe = true +) +public class StopMojo extends StartMojo { + + @Override + public void execute() throws MojoExecutionException { + if (this.skipLaunchpad) { + this.getLog().info("Executing of the stop-multiple launchpad mojo is disabled by configuration."); + return; + } + // read configurations + final Properties launchpadConfigProps = new Properties(); + Reader reader = null; + try { + reader = new FileReader(this.systemPropertiesFile); + launchpadConfigProps.load(reader); + } catch ( final IOException ioe) { + throw new MojoExecutionException("Unable to read launchpad runner configuration properties.", ioe); + } finally { + IOUtils.closeQuietly(reader); + } + + final int instances = Integer.valueOf(launchpadConfigProps.getProperty("launchpad.instances")); + final List<ProcessDescription> configurations = new ArrayList<ProcessDescription>(); + for(int i=1;i<=instances;i++) { + final String id = launchpadConfigProps.getProperty("launchpad.instance.id." + String.valueOf(i)); + + final ProcessDescription config = ProcessDescriptionProvider.getInstance().getRunConfiguration(id); + if ( config == null ) { + getLog().warn("No launchpad configuration found for instance " + id); + } else { + configurations.add(config); + } + } + + if (configurations.size() > 0) { + getLog().info(new StringBuilder("Stopping ").append(configurations.size()).append(" Launchpad instances").toString()); + + for (final ProcessDescription cfg : configurations) { + + try { + LauncherCallable.stop(this.getLog(), cfg); + ProcessDescriptionProvider.getInstance().removeRunConfiguration(cfg.getId()); + } catch (Exception e) { + throw new MojoExecutionException("Could not stop launchpad " + cfg.getId(), e); + } + } + } else { + getLog().warn("No stored configuration file was found at " + this.systemPropertiesFile + " - no Launchapd will be stopped"); + } + } +} -- To stop receiving notification emails like this one, please contact "[email protected]" <[email protected]>.
