View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.maven.plugin;
20  
21  import java.io.BufferedOutputStream;
22  import java.io.BufferedReader;
23  import java.io.File;
24  import java.io.FileOutputStream;
25  import java.io.IOException;
26  import java.io.InputStream;
27  import java.io.InputStreamReader;
28  import java.io.LineNumberReader;
29  import java.io.OutputStream;
30  import java.net.MalformedURLException;
31  import java.util.ArrayList;
32  import java.util.Arrays;
33  import java.util.HashMap;
34  import java.util.HashSet;
35  import java.util.Iterator;
36  import java.util.List;
37  import java.util.Locale;
38  import java.util.Map;
39  import java.util.Properties;
40  import java.util.Random;
41  import java.util.Set;
42  
43  import org.apache.maven.artifact.Artifact;
44  import org.apache.maven.plugin.MojoExecutionException;
45  import org.apache.maven.plugin.MojoFailureException;
46  import org.apache.maven.plugin.descriptor.PluginDescriptor;
47  import org.eclipse.jetty.annotations.AnnotationConfiguration;
48  import org.eclipse.jetty.server.Server;
49  import org.eclipse.jetty.util.IO;
50  import org.eclipse.jetty.util.resource.Resource;
51  import org.eclipse.jetty.util.resource.ResourceCollection;
52  import org.eclipse.jetty.util.thread.QueuedThreadPool;
53  
54  
55  /**
56   * This goal is used to deploy your unassembled webapp into a forked JVM.
57   * <p>
58   * 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>
59   * configuration element for this plugin. You may also need context xml file for any particularly complex webapp setup.
60   * about your webapp.
61   * <p>
62   * Unlike the other jetty goals, this does NOT support the <b>scanIntervalSeconds</b> parameter: the webapp will be deployed only once.
63   * <p>
64   * 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
65   * 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).
66   * 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
67   * you can use the <b>jetty:stop</b> goal to terminate the process.
68   * <p>
69   * 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.
70   * 
71   * @goal run-forked
72   * @requiresDependencyResolution test
73   * @execute phase="test-compile"
74   * @description Runs Jetty in forked JVM on an unassembled webapp
75   *
76   */
77  public class JettyRunForkedMojo extends JettyRunMojo
78  {    
79      /**
80       * The target directory
81       * 
82       * @parameter default-value="${project.build.directory}"
83       * @required
84       * @readonly
85       */
86      protected File target;
87      
88      /**
89       * The file into which to generate the quickstart web xml for the forked process to use
90       * 
91       * @parameter default-value="${project.build.directory}/fork-web.xml"
92       */
93      protected File forkWebXml;
94      
95      
96      /**
97       * Arbitrary jvm args to pass to the forked process
98       * @parameter property="jetty.jvmArgs"
99       */
100     private String jvmArgs;
101     
102     
103     /**
104      * @parameter default-value="${plugin.artifacts}"
105      * @readonly
106      */
107     private List pluginArtifacts;
108     
109     
110     /**
111      * @parameter default-value="${plugin}"
112      * @readonly
113      */
114     private PluginDescriptor plugin;
115     
116     
117     /**
118      * @parameter default-value="true"
119      */
120     private boolean waitForChild;
121 
122     /**
123      * @parameter default-value="50"
124      */
125     private int maxStartupLines;
126     
127     
128     /**
129      * Extra environment variables to be passed to the forked process
130      * 
131      * @parameter
132      */
133     private Map<String,String> env = new HashMap<String,String>();
134 
135     /**
136      * The forked jetty instance
137      */
138     private Process forkedProcess;
139     
140     
141     /**
142      * Random number generator
143      */
144     private Random random;    
145     
146  
147     
148     private Resource originalBaseResource;
149     private boolean originalPersistTemp;
150     
151     
152     /**
153      * ShutdownThread
154      *
155      *
156      */
157     public class ShutdownThread extends Thread
158     {
159         public ShutdownThread()
160         {
161             super("RunForkedShutdown");
162         }
163         
164         public void run ()
165         {
166             if (forkedProcess != null && waitForChild)
167             {
168                 forkedProcess.destroy();
169             }
170         }
171     }
172     
173 
174     
175     
176     /**
177      * ConsoleStreamer
178      * 
179      * Simple streamer for the console output from a Process
180      */
181     private static class ConsoleStreamer implements Runnable
182     {
183         private String mode;
184         private BufferedReader reader;
185 
186         public ConsoleStreamer(String mode, InputStream is)
187         {
188             this.mode = mode;
189             this.reader = new BufferedReader(new InputStreamReader(is));
190         }
191 
192 
193         public void run()
194         {
195             String line;
196             try
197             {
198                 while ((line = reader.readLine()) != (null))
199                 {
200                     System.out.println("[" + mode + "] " + line);
201                 }
202             }
203             catch (IOException ignore)
204             {
205                 /* ignore */
206             }
207             finally
208             {
209                 IO.close(reader);
210             }
211         }
212     }
213     
214     
215     
216     
217     
218     /**
219      * @see org.apache.maven.plugin.Mojo#execute()
220      */
221     public void execute() throws MojoExecutionException, MojoFailureException
222     {
223         Runtime.getRuntime().addShutdownHook(new ShutdownThread());
224         random = new Random();
225         super.execute();
226     }
227     
228     
229 
230 
231     @Override
232     public void startJetty() throws MojoExecutionException
233     {
234         //Only do enough setup to be able to produce a quickstart-web.xml file to
235         //pass onto the forked process to run     
236 
237         try
238         {
239             printSystemProperties();
240 
241             //do NOT apply the jettyXml configuration - as the jvmArgs may be needed for it to work 
242             if (server == null)
243                 server = new Server();
244 
245             //ensure handler structure enabled
246             ServerSupport.configureHandlers(server, null);
247             
248             ServerSupport.configureDefaultConfigurationClasses(server);
249                    
250             //ensure config of the webapp based on settings in plugin
251             configureWebApplication();
252             
253             //copy the base resource as configured by the plugin
254             originalBaseResource = webApp.getBaseResource();
255 
256             //get the original persistance setting
257             originalPersistTemp = webApp.isPersistTempDirectory();
258 
259             //set the webapp up to do very little other than generate the quickstart-web.xml
260             webApp.setCopyWebDir(false);
261             webApp.setCopyWebInf(false);
262             webApp.setGenerateQuickStart(true);
263 
264             if (webApp.getQuickStartWebDescriptor() == null)
265             {
266                 if (forkWebXml == null)
267                     forkWebXml = new File (target, "fork-web.xml");
268 
269                 if (!forkWebXml.getParentFile().exists())
270                     forkWebXml.getParentFile().mkdirs();
271                 if (!forkWebXml.exists())
272                     forkWebXml.createNewFile();
273 
274                 webApp.setQuickStartWebDescriptor(Resource.newResource(forkWebXml));
275             }
276             
277             //add webapp to our fake server instance
278             ServerSupport.addWebApplication(server, webApp);
279                        
280             //if our server has a thread pool associated we can do annotation scanning multithreaded,
281             //otherwise scanning will be single threaded
282             QueuedThreadPool tpool = server.getBean(QueuedThreadPool.class);
283             if (tpool != null)
284                 tpool.start();
285             else
286                 webApp.setAttribute(AnnotationConfiguration.MULTI_THREADED, Boolean.FALSE.toString());
287 
288             //leave everything unpacked for the forked process to use
289             webApp.setPersistTempDirectory(true);
290             
291             webApp.start(); //just enough to generate the quickstart           
292            
293             //save config of the webapp BEFORE we stop
294             File props = prepareConfiguration();
295             
296             webApp.stop();
297             
298             if (tpool != null)
299                 tpool.stop();
300             
301             List<String> cmd = new ArrayList<String>();
302             cmd.add(getJavaBin());
303             
304             if (jvmArgs != null)
305             {
306                 String[] args = jvmArgs.split(" ");
307                 for (int i=0;args != null && i<args.length;i++)
308                 {
309                     if (args[i] !=null && !"".equals(args[i]))
310                         cmd.add(args[i].trim());
311                 }
312             }
313             
314             String classPath = getContainerClassPath();
315             if (classPath != null && classPath.length() > 0)
316             {
317                 cmd.add("-cp");
318                 cmd.add(classPath);
319             }
320             cmd.add(Starter.class.getCanonicalName());
321             
322             if (stopPort > 0 && stopKey != null)
323             {
324                 cmd.add("--stop-port");
325                 cmd.add(Integer.toString(stopPort));
326                 cmd.add("--stop-key");
327                 cmd.add(stopKey);
328             }
329             if (jettyXml != null)
330             {
331                 cmd.add("--jetty-xml");
332                 cmd.add(jettyXml);
333             }
334         
335             if (contextXml != null)
336             {
337                 cmd.add("--context-xml");
338                 cmd.add(contextXml);
339             }
340             
341             cmd.add("--props");
342             cmd.add(props.getAbsolutePath());
343             
344             String token = createToken();
345             cmd.add("--token");
346             cmd.add(token);
347             
348             ProcessBuilder builder = new ProcessBuilder(cmd);
349             builder.directory(project.getBasedir());
350             
351             if (PluginLog.getLog().isDebugEnabled())
352                 PluginLog.getLog().debug(Arrays.toString(cmd.toArray()));
353             
354             PluginLog.getLog().info("Forked process starting");
355             
356             //set up extra environment vars if there are any
357             if (!env.isEmpty())
358             {
359                 builder.environment().putAll(env);
360             }
361             
362             if (waitForChild)
363             {
364                 forkedProcess = builder.start();
365                 startPump("STDOUT",forkedProcess.getInputStream());
366                 startPump("STDERR",forkedProcess.getErrorStream());
367                 int exitcode = forkedProcess.waitFor();            
368                 PluginLog.getLog().info("Forked execution exit: "+exitcode);
369             }
370             else
371             {   //merge stderr and stdout from child
372                 builder.redirectErrorStream(true);
373                 forkedProcess = builder.start();
374 
375                 //wait for the child to be ready before terminating.
376                 //child indicates it has finished starting by printing on stdout the token passed to it
377                 try
378                 {
379                     String line = "";
380                     try (InputStream is = forkedProcess.getInputStream();
381                             LineNumberReader reader = new LineNumberReader(new InputStreamReader(is)))
382                     {
383                         int attempts = maxStartupLines; //max lines we'll read trying to get token
384                         while (attempts>0 && line != null)
385                         {
386                             --attempts;
387                             line = reader.readLine();
388                             if (line != null && line.startsWith(token))
389                                 break;
390                         }
391 
392                     }
393 
394                     if (line != null && line.trim().equals(token))
395                         PluginLog.getLog().info("Forked process started.");
396                     else
397                     {
398                         String err = (line == null?"":(line.startsWith(token)?line.substring(token.length()):line));
399                         PluginLog.getLog().info("Forked process startup errors"+(!"".equals(err)?", received: "+err:""));
400                     }
401                 }
402                 catch (Exception e)
403                 {
404                     throw new MojoExecutionException ("Problem determining if forked process is ready: "+e.getMessage());
405                 }
406 
407             }
408         }
409         catch (InterruptedException ex)
410         {
411             if (forkedProcess != null && waitForChild)
412                 forkedProcess.destroy();
413             
414             throw new MojoExecutionException("Failed to start Jetty within time limit");
415         }
416         catch (Exception ex)
417         {
418             if (forkedProcess != null && waitForChild)
419                 forkedProcess.destroy();
420             
421             throw new MojoExecutionException("Failed to create Jetty process", ex);
422         }
423     }
424 
425     public List<String> getProvidedJars() throws MojoExecutionException
426     {  
427         //if we are configured to include the provided dependencies on the plugin's classpath
428         //(which mimics being on jetty's classpath vs being on the webapp's classpath), we first
429         //try and filter out ones that will clash with jars that are plugin dependencies, then
430         //create a new classloader that we setup in the parent chain.
431         if (useProvidedScope)
432         {
433             
434                 List<String> provided = new ArrayList<String>();        
435                 for ( Iterator<Artifact> iter = project.getArtifacts().iterator(); iter.hasNext(); )
436                 {                   
437                     Artifact artifact = iter.next();
438                     if (Artifact.SCOPE_PROVIDED.equals(artifact.getScope()) && !isPluginArtifact(artifact))
439                     {
440                         provided.add(artifact.getFile().getAbsolutePath());
441                         if (getLog().isDebugEnabled()) { getLog().debug("Adding provided artifact: "+artifact);}
442                     }
443                 }
444                 return provided;
445 
446         }
447         else
448             return null;
449     }
450     
451     public File prepareConfiguration() throws MojoExecutionException
452     {
453         try
454         {   
455             //work out the configuration based on what is configured in the pom
456             File propsFile = new File (target, "fork.props");
457             if (propsFile.exists())
458                 propsFile.delete();   
459 
460             propsFile.createNewFile();
461             //propsFile.deleteOnExit();
462 
463             Properties props = new Properties();
464 
465 
466             //web.xml
467             if (webApp.getDescriptor() != null)
468             {
469                 props.put("web.xml", webApp.getDescriptor());
470             }
471             
472             if (webApp.getQuickStartWebDescriptor() != null)
473             {
474                 props.put("quickstart.web.xml", webApp.getQuickStartWebDescriptor().getFile().getAbsolutePath());
475             }
476 
477             //sort out the context path
478             if (webApp.getContextPath() != null)
479             {
480                 props.put("context.path", webApp.getContextPath());       
481             }
482 
483             //tmp dir
484             props.put("tmp.dir", webApp.getTempDirectory().getAbsolutePath());
485             props.put("tmp.dir.persist", Boolean.toString(originalPersistTemp));
486 
487             //send over the original base resources before any overlays were added
488             if (originalBaseResource instanceof ResourceCollection)
489                 props.put("base.dirs.orig", toCSV(((ResourceCollection)originalBaseResource).getResources()));
490             else
491                 props.put("base.dirs.orig", originalBaseResource.toString());
492 
493             //send over the calculated resource bases that includes unpacked overlays, but none of the
494             //meta-inf resources
495             Resource postOverlayResources = (Resource)webApp.getAttribute(MavenWebInfConfiguration.RESOURCE_BASES_POST_OVERLAY);
496             if (postOverlayResources instanceof ResourceCollection)
497                 props.put("base.dirs", toCSV(((ResourceCollection)postOverlayResources).getResources()));
498             else
499                 props.put("base.dirs", postOverlayResources.toString());
500         
501             
502             //web-inf classes
503             if (webApp.getClasses() != null)
504             {
505                 props.put("classes.dir",webApp.getClasses().getAbsolutePath());
506             }
507             
508             if (useTestScope && webApp.getTestClasses() != null)
509             {
510                 props.put("testClasses.dir", webApp.getTestClasses().getAbsolutePath());
511             }
512 
513             //web-inf lib
514             List<File> deps = webApp.getWebInfLib();
515             StringBuffer strbuff = new StringBuffer();
516             for (int i=0; i<deps.size(); i++)
517             {
518                 File d = deps.get(i);
519                 strbuff.append(d.getAbsolutePath());
520                 if (i < deps.size()-1)
521                     strbuff.append(",");
522             }
523             props.put("lib.jars", strbuff.toString());
524 
525             //any war files
526             List<Artifact> warArtifacts = getWarArtifacts(); 
527             for (int i=0; i<warArtifacts.size(); i++)
528             {
529                 strbuff.setLength(0);           
530                 Artifact a  = warArtifacts.get(i);
531                 strbuff.append(a.getGroupId()+",");
532                 strbuff.append(a.getArtifactId()+",");
533                 strbuff.append(a.getFile().getAbsolutePath());
534                 props.put("maven.war.artifact."+i, strbuff.toString());
535             }
536           
537             
538             //any overlay configuration
539             WarPluginInfo warPlugin = new WarPluginInfo(project);
540             
541             //add in the war plugins default includes and excludes
542             props.put("maven.war.includes", toCSV(warPlugin.getDependentMavenWarIncludes()));
543             props.put("maven.war.excludes", toCSV(warPlugin.getDependentMavenWarExcludes()));
544             
545             
546             List<OverlayConfig> configs = warPlugin.getMavenWarOverlayConfigs();
547             int i=0;
548             for (OverlayConfig c:configs)
549             {
550                 props.put("maven.war.overlay."+(i++), c.toString());
551             }
552             
553             try (OutputStream out = new BufferedOutputStream(new FileOutputStream(propsFile)))
554             {
555                 props.store(out, "properties for forked webapp");
556             }
557             return propsFile;
558         }
559         catch (Exception e)
560         {
561             throw new MojoExecutionException("Prepare webapp configuration", e);
562         }
563     }
564     
565 
566     
567     
568   
569     
570     /**
571      * @return
572      * @throws MalformedURLException
573      * @throws IOException
574      */
575     private List<Artifact> getWarArtifacts()
576     throws MalformedURLException, IOException
577     {
578         List<Artifact> warArtifacts = new ArrayList<Artifact>();
579         for ( Iterator<Artifact> iter = project.getArtifacts().iterator(); iter.hasNext(); )
580         {
581             Artifact artifact = (Artifact) iter.next();  
582             
583             if (artifact.getType().equals("war"))
584                 warArtifacts.add(artifact);
585         }
586 
587         return warArtifacts;
588     }
589     
590     public boolean isPluginArtifact(Artifact artifact)
591     {
592         if (pluginArtifacts == null || pluginArtifacts.isEmpty())
593             return false;
594         
595         boolean isPluginArtifact = false;
596         for (Iterator<Artifact> iter = pluginArtifacts.iterator(); iter.hasNext() && !isPluginArtifact; )
597         {
598             Artifact pluginArtifact = iter.next();
599             if (getLog().isDebugEnabled()) { getLog().debug("Checking "+pluginArtifact);}
600             if (pluginArtifact.getGroupId().equals(artifact.getGroupId()) && pluginArtifact.getArtifactId().equals(artifact.getArtifactId()))
601                 isPluginArtifact = true;
602         }
603         
604         return isPluginArtifact;
605     }
606     
607     private Set<Artifact> getExtraJars()
608     throws Exception
609     {
610         Set<Artifact> extraJars = new HashSet<Artifact>();
611   
612         
613         List l = pluginArtifacts;
614         Artifact pluginArtifact = null;
615 
616         if (l != null)
617         {
618             Iterator itor = l.iterator();
619             while (itor.hasNext() && pluginArtifact == null)
620             {              
621                 Artifact a = (Artifact)itor.next();
622                 if (a.getArtifactId().equals(plugin.getArtifactId())) //get the jetty-maven-plugin jar
623                 {
624                     extraJars.add(a);
625                 }
626             }
627         }
628 
629         return extraJars;
630     }
631 
632     public String getContainerClassPath() throws Exception
633     {
634         StringBuilder classPath = new StringBuilder();
635         for (Object obj : pluginArtifacts)
636         {
637             Artifact artifact = (Artifact) obj;
638             if ("jar".equals(artifact.getType()))
639             {
640                 if (classPath.length() > 0)
641                 {
642                     classPath.append(File.pathSeparator);
643                 }
644                 classPath.append(artifact.getFile().getAbsolutePath());
645 
646             }
647         }
648         
649         //Any jars that we need from the plugin environment (like the ones containing Starter class)
650         Set<Artifact> extraJars = getExtraJars();
651         for (Artifact a:extraJars)
652         { 
653             classPath.append(File.pathSeparator);
654             classPath.append(a.getFile().getAbsolutePath());
655         }
656         
657         
658         //Any jars that we need from the project's dependencies because we're useProvided
659         List<String> providedJars = getProvidedJars();
660         if (providedJars != null && !providedJars.isEmpty())
661         {
662             for (String jar:providedJars)
663             {
664                 classPath.append(File.pathSeparator);
665                 classPath.append(jar);
666                 if (getLog().isDebugEnabled()) getLog().debug("Adding provided jar: "+jar);
667             }
668         }
669 
670         return classPath.toString();
671     }
672 
673     
674 
675     
676     /**
677      * @return
678      */
679     private String getJavaBin()
680     {
681         String javaexes[] = new String[]
682         { "java", "java.exe" };
683 
684         File javaHomeDir = new File(System.getProperty("java.home"));
685         for (String javaexe : javaexes)
686         {
687             File javabin = new File(javaHomeDir,fileSeparators("bin/" + javaexe));
688             if (javabin.exists() && javabin.isFile())
689             {
690                 return javabin.getAbsolutePath();
691             }
692         }
693 
694         return "java";
695     }
696     
697     public static String fileSeparators(String path)
698     {
699         StringBuilder ret = new StringBuilder();
700         for (char c : path.toCharArray())
701         {
702             if ((c == '/') || (c == '\\'))
703             {
704                 ret.append(File.separatorChar);
705             }
706             else
707             {
708                 ret.append(c);
709             }
710         }
711         return ret.toString();
712     }
713 
714     public static String pathSeparators(String path)
715     {
716         StringBuilder ret = new StringBuilder();
717         for (char c : path.toCharArray())
718         {
719             if ((c == ',') || (c == ':'))
720             {
721                 ret.append(File.pathSeparatorChar);
722             }
723             else
724             {
725                 ret.append(c);
726             }
727         }
728         return ret.toString();
729     }
730 
731     private String createToken ()
732     {
733         return Long.toString(random.nextLong()^System.currentTimeMillis(), 36).toUpperCase(Locale.ENGLISH);
734     }
735     
736     private void startPump(String mode, InputStream inputStream)
737     {
738         ConsoleStreamer pump = new ConsoleStreamer(mode,inputStream);
739         Thread thread = new Thread(pump,"ConsoleStreamer/" + mode);
740         thread.setDaemon(true);
741         thread.start();
742     }
743 
744     private String toCSV (List<String> strings)
745     {
746         if (strings == null)
747             return "";
748         StringBuffer strbuff = new StringBuffer();
749         Iterator<String> itor = strings.iterator();
750         while (itor.hasNext())
751         {
752             strbuff.append(itor.next());
753             if (itor.hasNext())
754                 strbuff.append(",");
755         }
756         return strbuff.toString();
757     }
758 
759     private String toCSV (Resource[] resources)
760     {
761         StringBuffer rb = new StringBuffer();
762 
763         for (Resource r:resources)
764         {
765             if (rb.length() > 0) rb.append(",");
766             rb.append(r.toString());
767         }        
768 
769         return rb.toString();
770     }
771 }