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