View Javadoc

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