// // ======================================================================== // Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd. // ------------------------------------------------------------------------ // All rights reserved. This program and the accompanying materials // are made available under the terms of the Eclipse Public License v1.0 // and Apache License v2.0 which accompanies this distribution. // // The Eclipse Public License is available at // http://www.eclipse.org/legal/epl-v10.html // // The Apache License v2.0 is available at // http://www.opensource.org/licenses/apache2.0.php // // You may elect to redistribute this code under either of these licenses. // ======================================================================== // package org.eclipse.jetty.maven.plugin; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.LineNumberReader; import java.io.OutputStream; import java.net.MalformedURLException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.Random; import java.util.Set; import org.apache.maven.artifact.Artifact; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugin.descriptor.PluginDescriptor; import org.eclipse.jetty.annotations.AnnotationConfiguration; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.ResourceCollection; import org.eclipse.jetty.util.thread.QueuedThreadPool; /** * This goal is used to deploy your unassembled webapp into a forked JVM. * <p> * You need to define a jetty.xml file to configure connectors etc. You can use the normal setters of o.e.j.webapp.WebAppContext on the <b>webApp</b> * configuration element for this plugin. You may also need context xml file for any particularly complex webapp setup. * about your webapp. * <p> * Unlike the other jetty goals, this does NOT support the <b>scanIntervalSeconds</b> parameter: the webapp will be deployed only once. * <p> * The <b>stopKey</b>, <b>stopPort</b> configuration elements can be used to control the stopping of the forked process. By default, this plugin will launch * the forked jetty instance and wait for it to complete (in which case it acts much like the <b>jetty:run</b> goal, and you will need to Cntrl-C to stop). * By setting the configuration element <b>waitForChild</b> to <b>false</b>, the plugin will terminate after having forked the jetty process. In this case * you can use the <b>jetty:stop</b> goal to terminate the process. * <p> * See <a href="http://www.eclipse.org/jetty/documentation/">http://www.eclipse.org/jetty/documentation</a> for more information on this and other jetty plugins. * * @goal run-forked * @requiresDependencyResolution test * @execute phase="test-compile" * @description Runs Jetty in forked JVM on an unassembled webapp * */ public class JettyRunForkedMojo extends JettyRunMojo { /** * The target directory * * @parameter default-value="${project.build.directory}" * @required * @readonly */ protected File target; /** * The file into which to generate the quickstart web xml for the forked process to use * * @parameter default-value="${project.build.directory}/fork-web.xml" */ protected File forkWebXml; /** * Arbitrary jvm args to pass to the forked process * @parameter property="jetty.jvmArgs" */ private String jvmArgs; /** * @parameter default-value="${plugin.artifacts}" * @readonly */ private List pluginArtifacts; /** * @parameter default-value="${plugin}" * @readonly */ private PluginDescriptor plugin; /** * @parameter default-value="true" */ private boolean waitForChild; /** * @parameter default-value="50" */ private int maxStartupLines; /** * Extra environment variables to be passed to the forked process * * @parameter */ private Map<String,String> env = new HashMap<String,String>(); /** * The forked jetty instance */ private Process forkedProcess; /** * Random number generator */ private Random random; private Resource originalBaseResource; private boolean originalPersistTemp; /** * ShutdownThread * * */ public class ShutdownThread extends Thread { public ShutdownThread() { super("RunForkedShutdown"); } public void run () { if (forkedProcess != null && waitForChild) { forkedProcess.destroy(); } } } /** * ConsoleStreamer * * Simple streamer for the console output from a Process */ private static class ConsoleStreamer implements Runnable { private String mode; private BufferedReader reader; public ConsoleStreamer(String mode, InputStream is) { this.mode = mode; this.reader = new BufferedReader(new InputStreamReader(is)); } public void run() { String line; try { while ((line = reader.readLine()) != (null)) { System.out.println("[" + mode + "] " + line); } } catch (IOException ignore) { /* ignore */ } finally { IO.close(reader); } } } /** * @see org.apache.maven.plugin.Mojo#execute() */ public void execute() throws MojoExecutionException, MojoFailureException { Runtime.getRuntime().addShutdownHook(new ShutdownThread()); random = new Random(); super.execute(); } @Override public void startJetty() throws MojoExecutionException { //Only do enough setup to be able to produce a quickstart-web.xml file to //pass onto the forked process to run try { printSystemProperties(); //do NOT apply the jettyXml configuration - as the jvmArgs may be needed for it to work if (server == null) server = new Server(); //ensure handler structure enabled ServerSupport.configureHandlers(server, null); ServerSupport.configureDefaultConfigurationClasses(server); //ensure config of the webapp based on settings in plugin configureWebApplication(); //copy the base resource as configured by the plugin originalBaseResource = webApp.getBaseResource(); //get the original persistance setting originalPersistTemp = webApp.isPersistTempDirectory(); //set the webapp up to do very little other than generate the quickstart-web.xml webApp.setCopyWebDir(false); webApp.setCopyWebInf(false); webApp.setGenerateQuickStart(true); if (webApp.getQuickStartWebDescriptor() == null) { if (forkWebXml == null) forkWebXml = new File (target, "fork-web.xml"); if (!forkWebXml.getParentFile().exists()) forkWebXml.getParentFile().mkdirs(); if (!forkWebXml.exists()) forkWebXml.createNewFile(); webApp.setQuickStartWebDescriptor(Resource.newResource(forkWebXml)); } //add webapp to our fake server instance ServerSupport.addWebApplication(server, webApp); //if our server has a thread pool associated we can do annotation scanning multithreaded, //otherwise scanning will be single threaded QueuedThreadPool tpool = server.getBean(QueuedThreadPool.class); if (tpool != null) tpool.start(); else webApp.setAttribute(AnnotationConfiguration.MULTI_THREADED, Boolean.FALSE.toString()); //leave everything unpacked for the forked process to use webApp.setPersistTempDirectory(true); webApp.start(); //just enough to generate the quickstart //save config of the webapp BEFORE we stop File props = prepareConfiguration(); webApp.stop(); if (tpool != null) tpool.stop(); List<String> cmd = new ArrayList<String>(); cmd.add(getJavaBin()); if (jvmArgs != null) { String[] args = jvmArgs.split(" "); for (int i=0;args != null && i<args.length;i++) { if (args[i] !=null && !"".equals(args[i])) cmd.add(args[i].trim()); } } String classPath = getContainerClassPath(); if (classPath != null && classPath.length() > 0) { cmd.add("-cp"); cmd.add(classPath); } cmd.add(Starter.class.getCanonicalName()); if (stopPort > 0 && stopKey != null) { cmd.add("--stop-port"); cmd.add(Integer.toString(stopPort)); cmd.add("--stop-key"); cmd.add(stopKey); } if (jettyXml != null) { cmd.add("--jetty-xml"); cmd.add(jettyXml); } if (contextXml != null) { cmd.add("--context-xml"); cmd.add(contextXml); } cmd.add("--props"); cmd.add(props.getAbsolutePath()); String token = createToken(); cmd.add("--token"); cmd.add(token); ProcessBuilder builder = new ProcessBuilder(cmd); builder.directory(project.getBasedir()); if (PluginLog.getLog().isDebugEnabled()) PluginLog.getLog().debug(Arrays.toString(cmd.toArray())); PluginLog.getLog().info("Forked process starting"); //set up extra environment vars if there are any if (!env.isEmpty()) { builder.environment().putAll(env); } if (waitForChild) { forkedProcess = builder.start(); startPump("STDOUT",forkedProcess.getInputStream()); startPump("STDERR",forkedProcess.getErrorStream()); int exitcode = forkedProcess.waitFor(); PluginLog.getLog().info("Forked execution exit: "+exitcode); } else { //merge stderr and stdout from child builder.redirectErrorStream(true); forkedProcess = builder.start(); //wait for the child to be ready before terminating. //child indicates it has finished starting by printing on stdout the token passed to it try { String line = ""; try (InputStream is = forkedProcess.getInputStream(); LineNumberReader reader = new LineNumberReader(new InputStreamReader(is))) { int attempts = maxStartupLines; //max lines we'll read trying to get token while (attempts>0 && line != null) { --attempts; line = reader.readLine(); if (line != null && line.startsWith(token)) break; } } if (line != null && line.trim().equals(token)) PluginLog.getLog().info("Forked process started."); else { String err = (line == null?"":(line.startsWith(token)?line.substring(token.length()):line)); PluginLog.getLog().info("Forked process startup errors"+(!"".equals(err)?", received: "+err:"")); } } catch (Exception e) { throw new MojoExecutionException ("Problem determining if forked process is ready: "+e.getMessage()); } } } catch (InterruptedException ex) { if (forkedProcess != null && waitForChild) forkedProcess.destroy(); throw new MojoExecutionException("Failed to start Jetty within time limit"); } catch (Exception ex) { if (forkedProcess != null && waitForChild) forkedProcess.destroy(); throw new MojoExecutionException("Failed to create Jetty process", ex); } } public List<String> getProvidedJars() throws MojoExecutionException { //if we are configured to include the provided dependencies on the plugin's classpath //(which mimics being on jetty's classpath vs being on the webapp's classpath), we first //try and filter out ones that will clash with jars that are plugin dependencies, then //create a new classloader that we setup in the parent chain. if (useProvidedScope) { List<String> provided = new ArrayList<String>(); for ( Iterator<Artifact> iter = project.getArtifacts().iterator(); iter.hasNext(); ) { Artifact artifact = iter.next(); if (Artifact.SCOPE_PROVIDED.equals(artifact.getScope()) && !isPluginArtifact(artifact)) { provided.add(artifact.getFile().getAbsolutePath()); if (getLog().isDebugEnabled()) { getLog().debug("Adding provided artifact: "+artifact);} } } return provided; } else return null; } public File prepareConfiguration() throws MojoExecutionException { try { //work out the configuration based on what is configured in the pom File propsFile = new File (target, "fork.props"); if (propsFile.exists()) propsFile.delete(); propsFile.createNewFile(); //propsFile.deleteOnExit(); Properties props = new Properties(); //web.xml if (webApp.getDescriptor() != null) { props.put("web.xml", webApp.getDescriptor()); } if (webApp.getQuickStartWebDescriptor() != null) { props.put("quickstart.web.xml", webApp.getQuickStartWebDescriptor().getFile().getAbsolutePath()); } //sort out the context path if (webApp.getContextPath() != null) { props.put("context.path", webApp.getContextPath()); } //tmp dir props.put("tmp.dir", webApp.getTempDirectory().getAbsolutePath()); props.put("tmp.dir.persist", Boolean.toString(originalPersistTemp)); //send over the original base resources before any overlays were added if (originalBaseResource instanceof ResourceCollection) props.put("base.dirs.orig", toCSV(((ResourceCollection)originalBaseResource).getResources())); else props.put("base.dirs.orig", originalBaseResource.toString()); //send over the calculated resource bases that includes unpacked overlays, but none of the //meta-inf resources Resource postOverlayResources = (Resource)webApp.getAttribute(MavenWebInfConfiguration.RESOURCE_BASES_POST_OVERLAY); if (postOverlayResources instanceof ResourceCollection) props.put("base.dirs", toCSV(((ResourceCollection)postOverlayResources).getResources())); else props.put("base.dirs", postOverlayResources.toString()); //web-inf classes if (webApp.getClasses() != null) { props.put("classes.dir",webApp.getClasses().getAbsolutePath()); } if (useTestScope && webApp.getTestClasses() != null) { props.put("testClasses.dir", webApp.getTestClasses().getAbsolutePath()); } //web-inf lib List<File> deps = webApp.getWebInfLib(); StringBuffer strbuff = new StringBuffer(); for (int i=0; i<deps.size(); i++) { File d = deps.get(i); strbuff.append(d.getAbsolutePath()); if (i < deps.size()-1) strbuff.append(","); } props.put("lib.jars", strbuff.toString()); //any war files List<Artifact> warArtifacts = getWarArtifacts(); for (int i=0; i<warArtifacts.size(); i++) { strbuff.setLength(0); Artifact a = warArtifacts.get(i); strbuff.append(a.getGroupId()+","); strbuff.append(a.getArtifactId()+","); strbuff.append(a.getFile().getAbsolutePath()); props.put("maven.war.artifact."+i, strbuff.toString()); } //any overlay configuration WarPluginInfo warPlugin = new WarPluginInfo(project); //add in the war plugins default includes and excludes props.put("maven.war.includes", toCSV(warPlugin.getDependentMavenWarIncludes())); props.put("maven.war.excludes", toCSV(warPlugin.getDependentMavenWarExcludes())); List<OverlayConfig> configs = warPlugin.getMavenWarOverlayConfigs(); int i=0; for (OverlayConfig c:configs) { props.put("maven.war.overlay."+(i++), c.toString()); } try (OutputStream out = new BufferedOutputStream(new FileOutputStream(propsFile))) { props.store(out, "properties for forked webapp"); } return propsFile; } catch (Exception e) { throw new MojoExecutionException("Prepare webapp configuration", e); } } /** * @return * @throws MalformedURLException * @throws IOException */ private List<Artifact> getWarArtifacts() throws MalformedURLException, IOException { List<Artifact> warArtifacts = new ArrayList<Artifact>(); for ( Iterator<Artifact> iter = project.getArtifacts().iterator(); iter.hasNext(); ) { Artifact artifact = (Artifact) iter.next(); if (artifact.getType().equals("war")) warArtifacts.add(artifact); } return warArtifacts; } public boolean isPluginArtifact(Artifact artifact) { if (pluginArtifacts == null || pluginArtifacts.isEmpty()) return false; boolean isPluginArtifact = false; for (Iterator<Artifact> iter = pluginArtifacts.iterator(); iter.hasNext() && !isPluginArtifact; ) { Artifact pluginArtifact = iter.next(); if (getLog().isDebugEnabled()) { getLog().debug("Checking "+pluginArtifact);} if (pluginArtifact.getGroupId().equals(artifact.getGroupId()) && pluginArtifact.getArtifactId().equals(artifact.getArtifactId())) isPluginArtifact = true; } return isPluginArtifact; } private Set<Artifact> getExtraJars() throws Exception { Set<Artifact> extraJars = new HashSet<Artifact>(); List l = pluginArtifacts; Artifact pluginArtifact = null; if (l != null) { Iterator itor = l.iterator(); while (itor.hasNext() && pluginArtifact == null) { Artifact a = (Artifact)itor.next(); if (a.getArtifactId().equals(plugin.getArtifactId())) //get the jetty-maven-plugin jar { extraJars.add(a); } } } return extraJars; } public String getContainerClassPath() throws Exception { StringBuilder classPath = new StringBuilder(); for (Object obj : pluginArtifacts) { Artifact artifact = (Artifact) obj; if ("jar".equals(artifact.getType())) { if (classPath.length() > 0) { classPath.append(File.pathSeparator); } classPath.append(artifact.getFile().getAbsolutePath()); } } //Any jars that we need from the plugin environment (like the ones containing Starter class) Set<Artifact> extraJars = getExtraJars(); for (Artifact a:extraJars) { classPath.append(File.pathSeparator); classPath.append(a.getFile().getAbsolutePath()); } //Any jars that we need from the project's dependencies because we're useProvided List<String> providedJars = getProvidedJars(); if (providedJars != null && !providedJars.isEmpty()) { for (String jar:providedJars) { classPath.append(File.pathSeparator); classPath.append(jar); if (getLog().isDebugEnabled()) getLog().debug("Adding provided jar: "+jar); } } return classPath.toString(); } /** * @return */ private String getJavaBin() { String javaexes[] = new String[] { "java", "java.exe" }; File javaHomeDir = new File(System.getProperty("java.home")); for (String javaexe : javaexes) { File javabin = new File(javaHomeDir,fileSeparators("bin/" + javaexe)); if (javabin.exists() && javabin.isFile()) { return javabin.getAbsolutePath(); } } return "java"; } public static String fileSeparators(String path) { StringBuilder ret = new StringBuilder(); for (char c : path.toCharArray()) { if ((c == '/') || (c == '\\')) { ret.append(File.separatorChar); } else { ret.append(c); } } return ret.toString(); } public static String pathSeparators(String path) { StringBuilder ret = new StringBuilder(); for (char c : path.toCharArray()) { if ((c == ',') || (c == ':')) { ret.append(File.pathSeparatorChar); } else { ret.append(c); } } return ret.toString(); } private String createToken () { return Long.toString(random.nextLong()^System.currentTimeMillis(), 36).toUpperCase(Locale.ENGLISH); } private void startPump(String mode, InputStream inputStream) { ConsoleStreamer pump = new ConsoleStreamer(mode,inputStream); Thread thread = new Thread(pump,"ConsoleStreamer/" + mode); thread.setDaemon(true); thread.start(); } private String toCSV (List<String> strings) { if (strings == null) return ""; StringBuffer strbuff = new StringBuffer(); Iterator<String> itor = strings.iterator(); while (itor.hasNext()) { strbuff.append(itor.next()); if (itor.hasNext()) strbuff.append(","); } return strbuff.toString(); } private String toCSV (Resource[] resources) { StringBuffer rb = new StringBuffer(); for (Resource r:resources) { if (rb.length() > 0) rb.append(","); rb.append(r.toString()); } return rb.toString(); } }