1/* Copyright (C) 2003 Vladimir Roubtsov. All rights reserved.
2 *
3 * This program and the accompanying materials are made available under
4 * the terms of the Common Public License v1.0 which accompanies this distribution,
5 * and is available at http://www.eclipse.org/legal/cpl-v10.html
6 *
7 * $Id: AppRunner.java,v 1.1.1.1.2.2 2004/07/16 23:32:03 vlad_r Exp $
8 */
9package com.vladium.emma.rt;
10
11import java.io.File;
12import java.lang.reflect.InvocationTargetException;
13import java.lang.reflect.Method;
14import java.net.MalformedURLException;
15import java.util.HashMap;
16import java.util.Iterator;
17import java.util.ArrayList;
18import java.util.List;
19import java.util.Map;
20
21import com.vladium.logging.Logger;
22import com.vladium.util.Files;
23import com.vladium.util.IConstants;
24import com.vladium.util.IProperties;
25import com.vladium.util.Property;
26import com.vladium.util.SoftValueMap;
27import com.vladium.util.Strings;
28import com.vladium.util.asserts.$assert;
29import com.vladium.util.exception.Exceptions;
30import com.vladium.util.exit.ExitHookManager;
31import com.vladium.emma.AppLoggers;
32import com.vladium.emma.IAppConstants;
33import com.vladium.emma.IAppErrorCodes;
34import com.vladium.emma.EMMAProperties;
35import com.vladium.emma.EMMARuntimeException;
36import com.vladium.emma.Processor;
37import com.vladium.emma.filter.IInclExclFilter;
38import com.vladium.emma.data.CoverageOptionsFactory;
39import com.vladium.emma.data.IMetaData;
40import com.vladium.emma.data.ICoverageData;
41import com.vladium.emma.data.DataFactory;
42import com.vladium.emma.data.ISessionData;
43import com.vladium.emma.data.SessionData;
44import com.vladium.emma.report.AbstractReportGenerator;
45import com.vladium.emma.report.IReportGenerator;
46import com.vladium.emma.report.SourcePathCache;
47
48// ----------------------------------------------------------------------------
49/**
50 * @author Vlad Roubtsov, (C) 2003
51 */
52public
53final class AppRunner extends Processor
54                      implements IAppErrorCodes
55{
56    // public: ................................................................
57
58
59    public static AppRunner create (final ClassLoader delegate)
60    {
61        return new AppRunner (delegate);
62    }
63
64
65    public synchronized void run ()
66    {
67        validateState ();
68
69        // disable Runtime's own exit hook:
70        RTSettings.setStandaloneMode (false); // an optimization to disable RT's static init code [this line must precede any reference to RT]
71        RT.reset (true, false); // reset RT [RT creates 'cdata' and loads app properties]
72
73        // load tool properties:
74        final IProperties toolProperties;
75        {
76            IProperties appProperties = RT.getAppProperties (); // try to use app props consistent with RT's view of them
77            if (appProperties == null) appProperties = EMMAProperties.getAppProperties (); // don't use combine()
78
79            toolProperties = IProperties.Factory.combine (m_propertyOverrides, appProperties);
80        }
81        if ($assert.ENABLED) $assert.ASSERT (toolProperties != null, "toolProperties is null"); // can be empty, though
82
83        final Logger current = Logger.getLogger ();
84        final Logger log = AppLoggers.create (m_appName, toolProperties, current);
85
86        if (log.atTRACE1 ())
87        {
88            log.trace1 ("run", "complete tool properties:");
89            toolProperties.list (log.getWriter ());
90        }
91
92        try
93        {
94            Logger.push (log);
95            m_log = log;
96
97            _run (toolProperties);
98        }
99        finally
100        {
101            if (m_log != null)
102            {
103                Logger.pop (m_log);
104                m_log = null;
105            }
106        }
107    }
108
109
110    /**
111     * @param path [null is equivalent to empty array]
112     * @param canonical
113     */
114    public synchronized void setCoveragePath (String [] path, final boolean canonical)
115    {
116        if ((path == null) || (path.length == 0))
117            m_coveragePath = IConstants.EMPTY_FILE_ARRAY;
118        else
119            m_coveragePath = Files.pathToFiles (path, canonical);
120
121        m_canonical = canonical;
122    }
123
124    public synchronized void setScanCoveragePath (final boolean scan)
125    {
126        m_scanCoveragePath = scan;
127    }
128
129    /**
130     * @param path [null is equivalent to no source path]
131     */
132    public synchronized void setSourcePath (final String [] path)
133    {
134        if (path == null)
135            m_sourcePath = null;
136        else
137            m_sourcePath = Files.pathToFiles (path, true); // always canonicalize source path
138    }
139
140    /**
141     *
142     * @param specs [null is equivalent to no filtering (everything is included)]
143     */
144    public synchronized final void setInclExclFilter (final String [] specs)
145    {
146        if (specs == null)
147            m_coverageFilter = null;
148        else
149            m_coverageFilter = IInclExclFilter.Factory.create (specs);
150    }
151
152    /**
153     *
154     * @param className [may not be null or empty]
155     * @param args [null is equivalent to an empty array]
156     */
157    public synchronized void setAppClass (final String className, final String [] args)
158    {
159        if ((className == null) || (className.length () == 0))
160            throw new IllegalArgumentException ("null/empty input: className");
161
162        if (args != null)
163        {
164            final String [] _args = (String []) args.clone ();
165
166            for (int a = 0; a < _args.length; ++ a)
167                if (_args [a] == null) throw new IllegalArgumentException ("null input: args[" + a + "]");
168
169            m_appArgs = _args;
170        }
171        else
172        {
173            m_appArgs = IConstants.EMPTY_STRING_ARRAY;
174        }
175
176        m_appClassName = className;
177    }
178
179    public synchronized void setDumpSessionData (final boolean dump)
180    {
181        m_dumpSessionData = dump;
182    }
183
184    /**
185     *
186     * @param fileName [null unsets the previous override setting]
187     */
188    public synchronized final void setSessionOutFile (final String fileName)
189    {
190        if (fileName == null)
191            m_sdataOutFile = null;
192        else
193        {
194            final File _file = new File (fileName);
195
196            if (_file.exists () && ! _file.isFile ())
197                throw new IllegalArgumentException ("not a file: [" + _file.getAbsolutePath () + "]");
198
199            m_sdataOutFile = _file;
200        }
201    }
202
203    /**
204     *
205     * @param merge [null unsets the previous override setting]
206     */
207    public synchronized final void setSessionOutMerge (final Boolean merge)
208    {
209        m_sdataOutMerge = merge;
210    }
211
212    /**
213     *
214     * @param types [may not be null]
215     */
216    public synchronized void setReportTypes (final String [] types)
217    {
218        if (types == null) throw new IllegalArgumentException ("null input: types");
219
220        final String [] reportTypes = Strings.removeDuplicates (types, true);
221        if (reportTypes.length == 0) throw new IllegalArgumentException ("empty input: types");
222
223        if ($assert.ENABLED) $assert.ASSERT (reportTypes != null && reportTypes.length  > 0);
224
225
226        final IReportGenerator [] reportGenerators = new IReportGenerator [reportTypes.length];
227        for (int t = 0; t < reportTypes.length; ++ t)
228        {
229            reportGenerators [t] = AbstractReportGenerator.create (reportTypes [t]);
230        }
231
232        m_reportGenerators = reportGenerators;
233    }
234
235    // protected: .............................................................
236
237
238    protected void validateState ()
239    {
240        super.validateState ();
241
242        if ((m_appClassName == null) || (m_appClassName.length () == 0))
243            throw new IllegalStateException ("application class name not set");
244
245        if (m_appArgs == null)
246            throw new IllegalStateException ("application arguments not set");
247
248        if (m_coveragePath == null)
249            throw new IllegalStateException ("coverage path not set");
250
251        // [m_coverageFilter can be null]
252
253        // [m_sdataOutFile can be null]
254        // [m_sdataOutMerge can be null]
255
256        if ((m_reportGenerators == null) || (m_reportGenerators.length == 0))
257            throw new IllegalStateException ("report types not set");
258
259        // [m_sourcePath can be null/empty]
260
261        // [m_propertyOverrides can be null]
262    }
263
264
265    protected void _run (final IProperties toolProperties)
266    {
267        final Logger log = m_log;
268
269        final boolean verbose = log.atVERBOSE ();
270        if (verbose)
271        {
272            log.verbose (IAppConstants.APP_VERBOSE_BUILD_ID);
273
274            // [assertion: m_coveragePath != null]
275            log.verbose ("coverage path:");
276            log.verbose ("{");
277            for (int p = 0; p < m_coveragePath.length; ++ p)
278            {
279                final File f = m_coveragePath [p];
280                final String nonexistent = f.exists () ? "" : "{nonexistent} ";
281
282                log.verbose ("  " + nonexistent + f.getAbsolutePath ());
283            }
284            log.verbose ("}");
285
286            if ((m_sourcePath == null) || (m_sourcePath.length == 0))
287            {
288                log.verbose ("source path not set");
289            }
290            else
291            {
292                log.verbose ("source path:");
293                log.verbose ("{");
294                for (int p = 0; p < m_sourcePath.length; ++ p)
295                {
296                    final File f = m_sourcePath [p];
297                    final String nonexistent = f.exists () ? "" : "{nonexistent} ";
298
299                    log.verbose ("  " + nonexistent + f.getAbsolutePath ());
300                }
301                log.verbose ("}");
302            }
303        }
304
305        // get the data out settings [note: this is not conditioned on m_dumpRawData]:
306        File sdataOutFile = m_sdataOutFile;
307        Boolean sdataOutMerge = m_sdataOutMerge;
308        {
309            if (sdataOutFile == null)
310                sdataOutFile = new File (toolProperties.getProperty (EMMAProperties.PROPERTY_SESSION_DATA_OUT_FILE,
311                                                                     EMMAProperties.DEFAULT_SESSION_DATA_OUT_FILE));
312
313            if (sdataOutMerge == null)
314            {
315                final String _dataOutMerge = toolProperties.getProperty (EMMAProperties.PROPERTY_SESSION_DATA_OUT_MERGE,
316                                                                         EMMAProperties.DEFAULT_SESSION_DATA_OUT_MERGE.toString ());
317                sdataOutMerge = Property.toBoolean (_dataOutMerge) ? Boolean.TRUE : Boolean.FALSE;
318            }
319        }
320
321        if (verbose && m_dumpSessionData)
322        {
323            log.verbose ("session data output file: " + sdataOutFile.getAbsolutePath ());
324            log.verbose ("session data output merge mode: " + sdataOutMerge);
325        }
326
327        // get instr class loader delegation filter settings:
328        final IInclExclFilter forcedDelegationFilter
329            = IInclExclFilter.Factory.create (toolProperties.getProperty (InstrClassLoader.PROPERTY_FORCED_DELEGATION_FILTER),
330                                              COMMA_DELIMITERS, FORCED_DELEGATION_FILTER_SPECS);
331        final IInclExclFilter throughDelegationFilter
332            = IInclExclFilter.Factory.create (toolProperties.getProperty (InstrClassLoader.PROPERTY_THROUGH_DELEGATION_FILTER),
333                                              COMMA_DELIMITERS, null);
334
335
336        // TODO: consider injecting Runtime straight into appLoader namespace...
337        // TODO: create a thread group for all exit hooks?
338
339
340        // get a handle to exit hook manager singleton:
341        ExitHookManager runnerExitHookManager = null;
342        try
343        {
344            runnerExitHookManager = ExitHookManager.getSingleton (); // can throw
345        }
346        catch (Exception e)
347        {
348            // TODO: log/handle/warn
349            e.printStackTrace (System.out);
350        }
351
352        AppRunnerExitHook runnerExitHook = null;
353        RuntimeException failure = null;
354
355        try
356        {
357            SourcePathCache srcpathCache = null;
358            if (m_sourcePath != null) srcpathCache = new SourcePathCache (m_sourcePath, true); // ignore non-existent source dirs
359
360            // create session data containers:
361            ICoverageData cdata = RT.getCoverageData ();
362            if ($assert.ENABLED) $assert.ASSERT (cdata != null, "cdata is null");
363
364            IMetaData mdata = DataFactory.newMetaData (CoverageOptionsFactory.create (toolProperties));
365
366            runnerExitHook = new AppRunnerExitHook (log, m_dumpSessionData, sdataOutFile, sdataOutMerge.booleanValue (), mdata, cdata, m_reportGenerators, srcpathCache, toolProperties);
367
368            if (runnerExitHookManager != null)
369                runnerExitHookManager.addExitHook (runnerExitHook);
370
371            // --------------[ start of exit hook-protected section ]--------------
372
373            Map classIOCache = null;
374
375            // scan the classpath to populate the initial metadata:
376            if (m_scanCoveragePath)
377            {
378                if (USE_SOFT_CACHE)
379                    classIOCache = new SoftValueMap (INIT_CACHE_CAPACITY, 0.75F, SOFT_CACHE_READ_CHK_FREQUENCY, SOFT_CACHE_WRITE_CHK_FREQUENCY);
380                else
381                    classIOCache = new HashMap (INIT_CACHE_CAPACITY, 0.75F);
382
383                final ClassPathProcessorST processor = new ClassPathProcessorST (m_coveragePath, m_canonical, mdata, m_coverageFilter, classIOCache);
384
385                // with a bit of work [ClassPathProcessorST needs to lock on the
386                // metadata, etc] this could be run concurrently with the app
387                // itself to improve perceived performance, however, I am not
388                // going to invest time in this;
389
390                // populate 'cache' [optional] and 'mdata':
391                processor.run ();
392
393                if (log.atTRACE1 ())
394                {
395                    log.trace1 ("run", "class cache size after cp scan: " + classIOCache.size ());
396                    log.trace1 ("run", "metadata size after cp scan: " + mdata.size ());
397                }
398            }
399
400
401            // app runner does not need these handles anymore [only the exit hook runner maintains them]:
402            srcpathCache = null;
403            cdata = null;
404
405            final ClassLoader appLoader;
406            {
407                final IClassLoadHook loadHook = new InstrClassLoadHook (m_coverageFilter, mdata);
408
409                try
410                {
411                    appLoader = new InstrClassLoader (m_delegate, m_coveragePath, forcedDelegationFilter, throughDelegationFilter, loadHook, classIOCache);
412                }
413                catch (SecurityException se)
414                {
415                    throw new EMMARuntimeException (SECURITY_RESTRICTION, new String [] {IAppConstants.APP_NAME}, se);
416                }
417                catch (MalformedURLException mue)
418                {
419                    throw new EMMARuntimeException (mue);
420                }
421            }
422
423            // app runner does not need these handles anymore:
424            mdata = null;
425            classIOCache = null;
426
427
428            final ClassLoader contextLoader;
429            boolean contextLoaderSet = false;
430            if (SET_CURRENT_CONTEXT_LOADER)
431            {
432                try
433                {
434                    final Thread currentThread = Thread.currentThread ();
435
436                    // TODO: rethink if this is the right place to do this
437                    contextLoader = currentThread.getContextClassLoader ();
438                    currentThread.setContextClassLoader (appLoader);
439
440                    contextLoaderSet = true;
441                }
442                catch (SecurityException se)
443                {
444                    throw new EMMARuntimeException (SECURITY_RESTRICTION, new String [] {IAppConstants.APP_NAME}, se);
445                }
446            }
447
448
449            ThreadGroup appThreadGroup = null;
450            try
451            {
452                // load [and possibly initialize] the app class:
453
454                final Class appClass;
455                try
456                {
457                    // load [and force early initialization if INIT_AT_LOAD_TIME is 'true']:
458                    appClass = Class.forName (m_appClassName, INIT_AT_LOAD_TIME, appLoader);
459                }
460                catch (ClassNotFoundException cnfe)
461                {
462                    // TODO: dump the classloader tree into the error message as well
463                    throw new EMMARuntimeException (MAIN_CLASS_NOT_FOUND, new String [] {m_appClassName}, cnfe);
464                }
465                catch (ExceptionInInitializerError eiie) // this should not happen for INIT_AT_LOAD_TIME=false
466                {
467                    final Throwable cause = eiie.getException ();
468
469                    throw new EMMARuntimeException (MAIN_CLASS_LOAD_FAILURE, new String [] {m_appClassName, cause.toString ()}, cause);
470                }
471                catch (Throwable t)
472                {
473                    throw new EMMARuntimeException (MAIN_CLASS_NOT_FOUND, new String [] {m_appClassName}, t);
474                }
475
476                // ensure that the app is bootstrapped using appLoader:
477                {
478                    final ClassLoader actualLoader = appClass.getClassLoader ();
479                    if (actualLoader != appLoader)
480                    {
481                        final String loaderName = actualLoader != null ?  actualLoader.getClass ().getName () : "<PRIMORDIAL>";
482
483                        throw new EMMARuntimeException (MAIN_CLASS_BAD_DELEGATION, new String [] {IAppConstants.APP_NAME, m_appClassName, loaderName});
484                    }
485                }
486
487                // run the app's main():
488
489                final Method appMain;
490                try
491                {
492                    // this causes initialization on some non-Sun-compatible JVMs [ignore]:
493                    appMain = appClass.getMethod ("main", MAIN_TYPE); // Sun JVMs do not seem to require the method to be declared
494                }
495                catch (Throwable t)
496                {
497                    throw new EMMARuntimeException (MAIN_METHOD_NOT_FOUND, new String [] {m_appClassName}, t);
498                }
499
500                Invoker invoker = new Invoker (appMain, null, new Object [] {m_appArgs});
501
502                appThreadGroup = new ThreadGroup (IAppConstants.APP_NAME + " thread group [" + m_appClassName + "]");
503                appThreadGroup.setDaemon (true);
504
505                Thread appThread = new Thread (appThreadGroup, invoker, IAppConstants.APP_NAME + " main() thread");
506                appThread.setContextClassLoader (appLoader);
507
508                // --- [app start] ----
509
510                appThread.start ();
511
512                try {appThread.join (); } catch (InterruptedException ignore) {}
513                appThread = null;
514
515                joinNonDeamonThreads (appThreadGroup);
516
517                // --- [app end] ----
518
519                if (log.atTRACE1 ())
520                {
521                    if (appLoader instanceof InstrClassLoader) ((InstrClassLoader) appLoader).debugDump (log.getWriter ());
522                }
523
524                final Throwable mainFailure = invoker.getFailure ();
525                invoker = null;
526
527                if (mainFailure != null)
528                {
529                    if (mainFailure instanceof InvocationTargetException)
530                    {
531                        final Throwable cause = ((InvocationTargetException) mainFailure).getTargetException ();
532
533                        throw new EMMARuntimeException (MAIN_METHOD_FAILURE, new String [] {m_appClassName, cause.toString ()}, cause);
534                    }
535                    else if (mainFailure instanceof ExceptionInInitializerError)
536                    {
537                        // this catch block is never entered if INIT_AT_LOAD_TIME is 'true'
538                        final Throwable cause = ((ExceptionInInitializerError) mainFailure).getException ();
539
540                        throw new EMMARuntimeException (MAIN_METHOD_FAILURE, new String [] {m_appClassName, cause.toString ()}, cause);
541                    }
542                    else if ((mainFailure instanceof IllegalAccessException)   ||
543                             (mainFailure instanceof IllegalArgumentException) ||
544                             (mainFailure instanceof NullPointerException))
545                    {
546                        throw new EMMARuntimeException (MAIN_METHOD_NOT_FOUND, new String [] {m_appClassName}, mainFailure);
547                    }
548                    else
549                    {
550                        throw new EMMARuntimeException (MAIN_METHOD_FAILURE, new String [] {m_appClassName, mainFailure.toString ()}, mainFailure);
551                    }
552                }
553            }
554            catch (SecurityException se)
555            {
556                throw new EMMARuntimeException (SECURITY_RESTRICTION, new String [] {IAppConstants.APP_NAME}, se);
557            }
558            finally
559            {
560                if (SET_CURRENT_CONTEXT_LOADER && contextLoaderSet)
561                {
562                    try
563                    {
564                        Thread.currentThread ().setContextClassLoader (contextLoader);
565                    }
566                    catch (Throwable ignore) {}
567                }
568
569                if ((appThreadGroup != null) && ! appThreadGroup.isDestroyed ())
570                try
571                {
572                    appThreadGroup.destroy ();
573                    appThreadGroup = null;
574                }
575                catch (Throwable ignore) {}
576            }
577        }
578        catch (RuntimeException re)
579        {
580            failure = re; // should be EMMARuntimeException only if there are no errors above
581        }
582        finally
583        {
584            RT.reset (false, false);
585        }
586
587        if ($assert.ENABLED) $assert.ASSERT (runnerExitHook != null, "reportExitHook = null");
588        runnerExitHook.run (); // that this may be a noop (if the shutdown sequence got there first)
589
590        // [assertion: the report exit hook is done]
591
592        if (runnerExitHookManager != null)
593        {
594            runnerExitHookManager.removeExitHook (runnerExitHook); // Ok if this fails
595            runnerExitHookManager = null;
596        }
597
598        // ---------------[ end of exit hook-protected section ]---------------
599
600
601        final Throwable exitHookDataDumpFailure = runnerExitHook.getDataDumpFailure ();
602        final List /* Throwable */ exitHookReportFailures = runnerExitHook.getReportFailures ();
603        runnerExitHook = null;
604
605        if (failure != null) // 'failure' takes precedence over any possible exit hook's problems
606        {
607            throw wrapFailure (failure);
608        }
609        else if ((exitHookDataDumpFailure != null) || (exitHookReportFailures != null))
610        {
611            if (exitHookDataDumpFailure != null)
612                log.log (Logger.SEVERE, "exception while persisting raw session data:", exitHookDataDumpFailure);
613
614            Throwable firstReportFailure = null;
615            if (exitHookReportFailures != null)
616            {
617                for (Iterator i = exitHookReportFailures.iterator (); i.hasNext (); )
618                {
619                    final Throwable reportFailure = (Throwable) i.next ();
620                    if (firstReportFailure == null) firstReportFailure = reportFailure;
621
622                    log.log (Logger.SEVERE, "exception while creating a report:", reportFailure);
623                }
624            }
625
626            if (exitHookDataDumpFailure != null)
627                throw wrapFailure (exitHookDataDumpFailure);
628            else if (firstReportFailure != null) // redundant check
629                throw wrapFailure (firstReportFailure);
630        }
631
632    }
633
634    // package: ...............................................................
635
636    // private: ...............................................................
637
638
639    private static final class Invoker implements Runnable
640    {
641        Invoker (final Method method, final Object target, final Object [] args)
642        {
643            if (method == null) throw new IllegalArgumentException ("null input: method");
644            if (args == null) throw new IllegalArgumentException ("null input: args");
645
646            m_method = method;
647            m_target = target;
648            m_args = args;
649        }
650
651        public void run ()
652        {
653            try
654            {
655                m_method.invoke (m_target, m_args);
656            }
657            catch (Throwable t)
658            {
659                m_failure = t;
660            }
661        }
662
663        Throwable getFailure ()
664        {
665            return m_failure;
666        }
667
668
669        private final Method m_method;
670        private final Object m_target;
671        private final Object [] m_args;
672        private Throwable m_failure;
673
674    } // end of nested class
675
676
677    private static final class AppRunnerExitHook implements Runnable
678    {
679        public synchronized void run ()
680        {
681            try
682            {
683                if (! m_done)
684                {
685                    // grab data snapshots:
686
687                    final IMetaData mdataSnashot = m_mdata.shallowCopy ();
688                    m_mdata = null;
689                    final ICoverageData cdataSnapshot = m_cdata.shallowCopy ();
690                    m_cdata = null;
691
692                    if (mdataSnashot.isEmpty ())
693                    {
694                        m_log.warning ("no metadata collected at runtime [no reports generated]");
695
696                        return;
697                    }
698
699                    if (cdataSnapshot.isEmpty ())
700                    {
701                        m_log.warning ("no coverage data collected at runtime [all reports will be empty]");
702                    }
703
704                    final ISessionData sdata = new SessionData (mdataSnashot, cdataSnapshot);
705
706                    // if requested, dump raw data before running report generators:
707                    // [note that the raw dumps and reports will be consistent wrt
708                    // the session data they represent]
709
710                    if (m_dumpRawData && (m_sdataOutFile != null))
711                    {
712                       try
713                        {
714                            final boolean info = m_log.atINFO ();
715
716                            final long start = info ? System.currentTimeMillis () : 0;
717                            {
718                                DataFactory.persist (sdata, m_sdataOutFile, m_sdataOutMerge);
719                            }
720                            if (info)
721                            {
722                                final long end = System.currentTimeMillis ();
723
724                                m_log.info ("raw session data " + (m_sdataOutMerge ? "merged into" : "written to") + " [" + m_sdataOutFile.getAbsolutePath () + "] {in " + (end - start) + " ms}");
725                            }
726                        }
727                        catch (Throwable t)
728                        {
729                            m_dataDumpFailure = t;
730                        }
731                    }
732
733                    for (int g = 0; g < m_generators.length; ++ g)
734                    {
735                        final IReportGenerator generator = m_generators [g];
736
737                        if (generator != null)
738                        {
739                            try
740                            {
741                                generator.process (mdataSnashot, cdataSnapshot, m_cache, m_properties);
742                            }
743                            catch (Throwable t)
744                            {
745                                if (m_reportFailures == null) m_reportFailures = new ArrayList ();
746                                m_reportFailures.add (t);
747
748                                continue;
749                            }
750                            finally
751                            {
752                                try { generator.cleanup (); } catch (Throwable ignore) {}
753                                m_generators [g] = null;
754                            }
755                        }
756                    }
757                }
758            }
759            finally
760            {
761                m_generators = null;
762                m_mdata = null;
763                m_cdata = null;
764                m_properties = null;
765                m_cache = null;
766
767                m_done = true;
768            }
769        }
770
771        // note: because ExitHookManager is a lazily created static singleton the
772        // correct thing to do is to pass an explicit Logger into each exit hook runner
773        // instead of relying on thread inheritance:
774
775        AppRunnerExitHook (final Logger log,
776                           final boolean dumpRawData, final File sdataOutFile, final boolean sdataOutMerge,
777                           final IMetaData mdata, final ICoverageData cdata,
778                           final IReportGenerator [] generators,
779                           final SourcePathCache cache, final IProperties properties)
780        {
781            if (log == null) throw new IllegalArgumentException ("null input: log");
782            if ((generators == null) || (generators.length == 0)) throw new IllegalArgumentException ("null/empty input: generators");
783            if (mdata == null) throw new IllegalArgumentException ("null input: mdata");
784            if (cdata == null) throw new IllegalArgumentException ("null input: cdata");
785            if (properties == null) throw new IllegalArgumentException ("null input: properties");
786
787            m_log = log;
788
789            m_dumpRawData = dumpRawData;
790            m_sdataOutFile = sdataOutFile;
791            m_sdataOutMerge = sdataOutMerge;
792
793            m_generators = (IReportGenerator []) generators.clone ();
794            m_mdata = mdata;
795            m_cdata = cdata;
796            m_cache = cache;
797            m_properties = properties;
798        }
799
800
801        synchronized Throwable getDataDumpFailure ()
802        {
803            return m_dataDumpFailure;
804        }
805
806        synchronized List /* Throwable */ getReportFailures ()
807        {
808            return m_reportFailures;
809        }
810
811
812        private final Logger m_log;
813        private final boolean m_dumpRawData;
814        private final File m_sdataOutFile;
815        private final boolean m_sdataOutMerge;
816
817        private IReportGenerator [] m_generators;
818        private IMetaData m_mdata;
819        private ICoverageData m_cdata;
820        private SourcePathCache m_cache;
821        private IProperties m_properties;
822        private boolean m_done;
823        private Throwable m_dataDumpFailure;
824        private List /* Throwable */ m_reportFailures;
825
826    } // end of nested class
827
828
829    private AppRunner (final ClassLoader delegate)
830    {
831        m_delegate = delegate;
832        m_coveragePath = IConstants.EMPTY_FILE_ARRAY;
833    }
834
835
836    private static void joinNonDeamonThreads (final ThreadGroup group)
837    {
838        if (group == null) throw new IllegalArgumentException ("null input: group");
839
840        final List threads = new ArrayList ();
841        while (true)
842        {
843            threads.clear ();
844
845            // note: group.activeCount() is only an estimate as more threads
846            // could get created while we are doing this [if 'aliveThreads'
847            // array is too short, the extra threads are silently ignored]:
848
849            Thread [] aliveThreads;
850            final int aliveCount;
851
852            // enumerate [recursively] all threads in 'group':
853            synchronized (group)
854            {
855                aliveThreads = new Thread [group.activeCount () << 1];
856                aliveCount = group.enumerate (aliveThreads, true);
857            }
858
859            for (int t = 0; t < aliveCount; t++)
860            {
861                if (! aliveThreads [t].isDaemon ())
862                    threads.add (aliveThreads [t]);
863            }
864            aliveThreads = null;
865
866            if (threads.isEmpty ())
867                break; // note: this logic does not work if daemon threads are spawning non-daemon ones
868            else
869            {
870                for (Iterator i = threads.iterator (); i.hasNext (); )
871                {
872                    try
873                    {
874                        ((Thread) i.next ()).join ();
875                    }
876                    catch (InterruptedException ignore) {}
877                }
878            }
879        }
880    }
881
882    private static RuntimeException wrapFailure (final Throwable t)
883    {
884        if (Exceptions.unexpectedFailure (t, EXPECTED_FAILURES))
885            return new EMMARuntimeException (UNEXPECTED_FAILURE,
886                                            new Object [] {t.toString (), IAppConstants.APP_BUG_REPORT_LINK},
887                                            t);
888        else if (t instanceof RuntimeException)
889            return (RuntimeException) t;
890        else
891            return new EMMARuntimeException (t);
892    }
893
894
895    // caller-settable state [scoped to this runner instance]:
896
897    private final ClassLoader m_delegate;
898
899    private String m_appClassName;      // required to be non-null for run()
900    private String [] m_appArgs;        // required to be non-null for run()
901
902    private File [] m_coveragePath;     // required to be non-null/non-empty for run()
903    private boolean m_canonical;
904    private boolean m_scanCoveragePath;
905    private IInclExclFilter m_coverageFilter; // can be null for run()
906
907    private boolean m_dumpSessionData;
908    private File m_sdataOutFile; // user override; can be null for run()
909    private Boolean m_sdataOutMerge; // user override; can be null for run()
910
911    private IReportGenerator [] m_reportGenerators; // required to be non-null for run()
912    private File [] m_sourcePath;                   // can be null/empty for run()
913
914    // it is attractive to detect errors at load time, but this may allow
915    // threads created by <clinit> code to escape; on the other hand, classes
916    // that do not override main() will not get initialized this way and will
917    // not register with our runtime [which seems a minor problem at this point]:
918    private static final boolean INIT_AT_LOAD_TIME = false;
919
920    // setting the context loader on AppRunner's thread should not
921    // be necessary since the app is run in a dedicated thread group;
922    // however, if INIT_AT_LOAD_TIME=true the app's <clinit> code
923    // should run with an adjusted context loader:
924    private static final boolean SET_CURRENT_CONTEXT_LOADER = INIT_AT_LOAD_TIME;
925
926    // a soft cache is ideal for managing class definitions that are read during
927    // the initial classpath scan; however, the default LRU policy parameters for
928    // clearing SoftReferences in Sun's J2SDK 1.3+ render them next to useless
929    // in the client HotSpot JVM (in which this tool will probably run most often);
930    // using a hard cache guarantees 100% cache hit rate but can also raise the
931    // memory requirements significantly beyond the needs of the original app.
932    // [see bug refs 4471453, 4806720, 4888056, 4239645]
933    //
934    // resolution for now: use a soft cache anyway and doc that to make it useful
935    // for non-trivial apps the user should use -Xms or -XX:SoftRefLRUPolicyMSPerMB
936    // JVM options or use a server HotSpot JVM
937    private static final boolean USE_SOFT_CACHE = true;
938
939    private static final int INIT_CACHE_CAPACITY = 2003; // prime
940    private static final int SOFT_CACHE_READ_CHK_FREQUENCY = 100;
941    private static final int SOFT_CACHE_WRITE_CHK_FREQUENCY = 100;
942
943    private static final String [] FORCED_DELEGATION_FILTER_SPECS; // set in <clinit>
944    private static final Class [] MAIN_TYPE = new Class [] {String [].class};
945
946    private static final Class [] EXPECTED_FAILURES; // set in <clinit>
947
948    protected static final String COMMA_DELIMITERS    = "," + Strings.WHITE_SPACE;
949    protected static final String PATH_DELIMITERS     = ",".concat (File.pathSeparator);
950
951    static
952    {
953        EXPECTED_FAILURES = new Class []
954        {
955            EMMARuntimeException.class,
956            IllegalArgumentException.class,
957            IllegalStateException.class,
958        };
959
960        FORCED_DELEGATION_FILTER_SPECS = new String [] {"+" + IAppConstants.APP_PACKAGE + ".*"};
961    }
962
963} // end of class
964// ----------------------------------------------------------------------------