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