View Javadoc

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